5 steps of test-driven development

In this article, I introduce you to the basic concepts of test-driven development (TDD). If you are an agile software developer, TDD is a best practice you should include in your software development life cycle. Learn what test-driven development is, understand the basic flow, and discover how unit tests are the cornerstone of TDD. You’ll leave with an understanding of why you should be using test-driven development in your processes.

What is test-driven development?

Test-driven development reverses traditional development and testing. So, instead of writing your code first and then retroactively fitting a test to validate the piece of code you just wrote, test-driven development dictates that you write the test first and then implement code changes until your code passes the test you already wrote.

In TDD, you write your unit test first, watch it fail, and then implement code changes until the test passes. Sounds backwards, right? But the code you produce when you use this testing methodology is cleaner and less prone to breaking in the long run.

A unit test is simply a test that covers a small portion of logic, like an algorithm, for example. Unit tests should be deterministic. When I say “deterministic” I mean that unit tests should never have side-effects like calls to external APIs that deliver random or changing data. Instead, you’d use mock data in place of data that could potentially change over time.

Five steps of test-driven development

There are 5 steps in the TDD flow:

  1. Read, understand, and process the feature or bug request.
  2. Translate the requirement by writing a unit test. If you have hot reloading set up, the unit test will run and fail as no code is implemented yet.
  3. Write and implement the code that fulfills the requirement. Run all tests and they should pass, if not repeat this step.
  4. Clean up your code by refactoring.
  5. Rinse, lather and repeat.

Figure 1 shows these steps and their agile, cyclical, and iterative nature: Red green refactoring in TDD

This workflow is sometimes called Red-Green-Refactoring, which comes from the status of the tests within the cycle.

  • The red phase indicates that code does not work.
  • The green phase indicates that everything is working, but not necessary in the most optimal way.
  • The blue phase indicates that the tester is refactoring the code, but is confident their code is covered with tests which gives the tester confidence to change and improve our code.

Test-driven development and CI/CD

The unit tests that come out of TDD are also an integral part of the continuous integration/continuous delivery (CI/CD) process. TDD relates specifically to unit tests and continuous integration/continuous delivery pipelines like CircleCI, GoCD, or Travis CI which run all the unit tests at commit time.

The tests are run in the deployment pipeline. If all tests pass, integration and deployment will happen. On the other hand, if any tests fail, the process is halted, thus ensuring the build is not broken.

Set up your tools, toolchain, and IDE first

In order to do test-driven development, you need to setup your tools, toolchain, and IDE first. In our [code pattern], we are developing a Node.js example, so here are the key tools we set up:

  • nvm (Node Version Manager) for Node.js and NPM: NVM allows you to run the Node.js version you want and change it without affecting the system node.
  • npm libraries for development:

How to write unit tests that fail

There are a couple different ways to write unit tests that fail.

  1. Write a test that references a function in the code that doesn’t exist yet. This will cause the test to fail with a non-found error (for instance, a 404 error).

  2. Alter the assert statement to make it fail. An assert statement says what value the code being tested is expected to return; this kind of statement is a key aspect of a unit test. The assert statement should reflect the feature or bug fix request.

So, to make it fail, you would write an asset statement that returns an unexpected value in, say, a data structure you want to enrich. For example, your JSON returns a person’s name, but your new requirement says to include the person’s cellphone number. You would first write the assert statement to only include the person’s name, which would cause it to fail. Then you would add the code to include the person phone number as well.

Or, in real life coding: Your assert statement could be: assert actualResult == {‘track’:‘foo fighters’}. Once the code (function) is hooked up, the 404 goes away, but the actual result could be an empty object like {}. You then hard code the result in the function to be {‘track’:‘foo fighters’}.

The test will now pass (Green!). The code is obviously just a sub for now, but you can get the basic understanding. The test is wired up to a point in the code correctly. From there you can implement actual business logic, for example, read a file/db/call an external API.

Deciding when to write unit tests

In general, there are two cases for when you’d write unit tests:

Case A: You write a unit test for a concise story representing a feature request. For example, a feature request might be to count the number of countries that a particular currency exchange supports. The first thing I do is write a unit test and see it fail. Then, I change the code iteratively until the unit test passes.

Case B: A piece of buggy code in production breaks. This bug triggers an issue that requires a fix/patch to be implemented. Returning to the currency exchange example, the code, when run manually, the user expects that $USD are used in many countries but the behavior is wrong, only one country returns.

The first thing I do is write a unit test and see it fail. Then, I correct my implementation code until the test passes. Not only does this fix the code and remove the bug, but it also gives me a unit test that I can use repeatedly to ensure this piece of code remains integrous.

Conclusion

Most programmers don’t write code using test-driven development, but they should. Test-driven development creates better code that is more fault-tolerant. Hopefully you understand the philosophy of TDD from this blog post and incorporate it into your software development practice.

Next steps

Stay tuned for new blog posts for how to do test-driven development in Node.js, Java, and Python.

Resources

  • Pytest: An open source and easy to learn tool that makes it easy to conduct unit tests
  • Java: Junit 5: Programmer-friendly testing framework for Java
  • NodeJS: Jest: a delightful JavaScript Testing Framework, with a focus on simplicity

Helpful books and articles