Image for post
Image for post

The Law of Testing: The degree to which you know how your software behaves is the degree to which you have accurately tested it.
— “Code Simplicity”

In order to verify the correctness of an application we use different kinds of tests: some check the logic of a small function or class, others check all the system layers from UI to databases and external services. Some kinds of tests could be somewhere in between calling the application API and using stubs for external services. The number of tested elements defines the scope of testing: fewer elements — smaller scope, more elements — bigger scope. The bigger the scope of tests, the more computational resources are required and the more time is needed for running the tests. Also, tests with a bigger scope are harder to maintain and they were made for a longer feedback cycle.

Today we will talk about Unit Tests, which are placed at the bottom of the testing pyramid and have the shortest feedback cycle.

Unit Tests

Image for post
Image for post
Twitter @ismonkeyuser

So, what is a Unit Test? It’s a code that can check that another code works as expected. One of the important qualities of a Unit Test are the following:

  • it tests the functionality of the elements of the application — units — classes and functions;
  • it’s written by developers while working on code;
  • it’s easy to run without having to set up an additional environment;
  • it requires small time to run;
  • it can be easily integrated with CI because it does not require additional environment.

Good Unit Tests follow five rules, which form the acronym F.I.R.S.T..

Fast — tests should be quick. When tests run slowly, you don’t want to run them frequently. And if you don’t run them frequently, you won’t find problems early enough to fix them easily. You won’t feel as free to clean up the code.

Independent (or Isolated) — tests should not depend on each other. Developer should be able to run tests in any order you like(even in parallel). When tests depend on each other, the first one to fail causes a cascade of downstream failures, making diagnosis difficult and hiding downstream defects.

The Single Responsibility Principle (SRP) of SOLID principles​ says that classes should be small and single-purpose. This can be applied to your tests as well. If one of your test methods can break for more than one reason, consider splitting it into separate tests.

Repeatable — tests should be repeatable in any environment: in the production environment, in the QA environment, and even on developer laptop. Test results must be the same every time and at every location.

Self-validating — each test must be able to determine that the output is expected or not. They either pass or fail. You should not have to read through a log file or compare different files to see whether the tests pass. If the tests aren’t self-validating, then failure can become subjective and running the tests can require a long manual evaluation.

Thorough/Timely — tests should be written at the proper time, with feature implementation. Testing post-facto requires developers to refactor working code and make an additional effort to have tests fulfilling these FIRST principles.

Why testing?

Maintainability is often described as the ability to understand, change and test an application easily. Unit Tests contribute to all three of these:

  • When Unit Tests are an implementation of test scenarios, they help to understand component functionality;
  • Unit tests can quickly tell you if change breaks the existing functionality;
  • Development with Unit Tests force the developer to design more testable components (quite often it means better code).

The most common argument against Unit Tests is that they increase development time. This is true for the very beginning of a project, when only the initial set of features has been implemented and almost every piece of code is freshly written. But as the project proceeds, you will find yourself rewriting old code more and more often. You will start spending time on trying to understand how the old code works and ensuring that your change hasn’t broken it.

That’s exactly where Unit Tests can help you. Yes, skipping Unit Tests lets you develop faster in the beginning, but if the application is going to be used, for example, for several years, introducing more features along the way, you have to keep the application maintainable and write Unit Tests.

Code coverage

It makes code coverage more of a developer tool for understanding that more test scenarios are required, than a tool to be used as a continuous integration quality gate. It’s still a good idea to agree on some minimum level of code coverage for identifying hot spots, but you can’t blindly trust high code coverage and we should care about the quality of Unit Tests.

TDD

Image for post
Image for post
TDD process: right red test -> make it green -> make it work

Let’s talk about the TDD process. In a test-driven development, each new feature begins with writing a test for it. To write a test, the developer must understand clearly the feature’s specifications and requirements. This is a differentiating feature of test-driven development versus writing Unit Tests after the code: It makes the developer focus on the test scenarios before writing the code — a subtle but important difference.

For the second step, we run all tests and see if the new test fails. Then, we write the code.

The next step is to write some code that results in the test passing. The new code written at this stage is not perfect and may, for example, pass the test in an inelegant way.

At this point, the only purpose of the written code is to pass the test. The programmer must not write code that’s beyond the functionality that the test checks.

Next, we run tests.

If all test cases now pass, the programmer can be confident that the new code meets the test requirements, and does not break or degrade any existing features.

If they do not pass, the new code must be adjusted until they do.

For the step after this, we refactor code.

If necessary we repeat this process, starting with another new test, and the cycle is then repeated to push forward the functionality.
TDD approach has some limitations, for example, it’s hard to apply it to big data & data science projects, because of data-driven part of those applications, it also hard to apply to GUI or frontend development. But this is the theme for another post.

Conclusion

Thank you for reading!

Any questions? Leave your comment below to start fantastic discussions!

Check out my blog or come to say hi 👋 on Twitter or subscribe to my telegram channel.

Plan your best!

Written by

helping robots conquer the earth and trying not to increase entropy using Python, Big Data, Machine Learning. Check out my blog — luminousmen.com

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store