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 by Max Kanat-Alexander
To check the correctness of the application developers utilize different types of tests — some of them check the function logic, others — all system levels from UI to databases and external services. Some types of tests may be somewhere between calling the application API and using stubs for external services. The number of elements tested determines the scope of testing: fewer parts — smaller scope, bigger parts — larger scope. The larger the scope of testing, the more computational resources are required and the more time it takes to run tests but you can validate the bigger chunks of functionality. Also, tests with a large scope were made for a longer feedback loop and are difficult to maintain.
Today we will talk about Unit Tests, which are placed at the bottom of the testing pyramid and have the shortest feedback cycle.
So, what's a Unit test? It's a test code that can verify that another code is working as expected. One of the important qualities of the Unit Test is the following:
- it verifies the functionality of the application elements — measurement units — classes and functions;
- it is written by the developers while working on the code;
- it is easy to run without having to set up an additional environment;
- it takes little time to execute;
- it easily integrates with Continuous Integration, as it does not require anything else except the code itself.
Good Unit Tests follow five rules, which form the acronym F.I.R.S.T.
Fast — the tests have to be fast. When tests are slow, developers don't want to run them often. And if they don't run them often, they won't find problems early enough to fix them easily. You won't feel safe enough to refactor the code.
Independent (or isolated) — tests should not depend on each other. Developers should be able to run tests in any order (preferably in parallel). When tests are dependent on each other, the first failure or error leads to a cascade of failures, making it difficult to locate the problem and it hides defects downstream.
The Single Responsibility Principle (SRP) SOLID Principles states that classes should be small and single-purpose. This can be applied to your tests as well. If one of your tests may fail for several reasons, consider splitting it into separate tests.
Repeatable — tests should be repeatable in any environment — on a production environment, on a staging environment, and even on a developer's laptop. Test results should be the same every time.
Self-validating — each test should be able to determine whether or not the result is the one you expect. They either pass or fail. You do not need to read the log file or compare different files to see if the tests are passed. If the tests are not self-validating, you may become subjective, and running the tests may require a long manual evaluation.
Thorough/Timely — tests should be written at the right time, with the feature implementation. Post facto testing requires developers to refactor working code and extra effort to have tests that follow these FIRST principles. Also, the context and edge cases are drifting out of the head over time.
Once we know what's being tested, the next question is why? The answer is simple — Unit Tests help us test the logic of the smallest parts of code and find defects. This is true, but there is a less obvious and more serious reason — they help us keep the application running and help us add new features and fix errors faster.
Ease of maintenance is often described as the ability to easily understand, modify and test an application. Unit tests contribute to all three of them:
- Unit Tests if implemented with a common use case in mind, help to understand the functionality of the application/unit;
- Unit Tests can quickly yell if a new code affects existing functionality;
- Unit Tests forces a developer to design more testable components (often this means a better code).
The most common argument against writing Unit Tests is that they increase development time. It refers to the very beginning of a project when only the initial set of capabilities is implemented and almost all of the code is written anew. But as the project evolves, you will rewrite the old code more and more often. You'll start spending time trying to figure out how the old code works and make sure that your change didn't break it. This is exactly where the Unit Tests can help developers. No matter what anyone says, the running and maintenance time of your application is incomparably longer than the development time itself.
Write unit tests, and you will be in a safer place in the future.
Code coverage is a measure used to describe the extent to which a program source code is tested. Usually, a program with high code coverage was tested more thoroughly and has fewer chances to contain errors than a program with low code coverage. But you cannot rely entirely on this metric, because even if 100% of the code is covered, we still know nothing about the quality of tests. Some tests may cover the code, but they are useless, have no necessary statements, or are just badly designed. It sounds counter-intuitive, but knowing which code is covered is more important. Uncovered code means that there are no Unit Tests to check that the code works as expected and that's it.
This makes code coverage more of a developer's tool to understand where we need more test cases than the tool that will be used as a quality gateway to Continuous Integration. It's still a good idea to agree on a minimum level of code coverage to identify hot spots, but developers can't blindly trust high code coverage and developers have to take care of unit test quality.
In test-driven development, each new functionality starts with writing a test for it. To write a test, a developer must have a clear understanding of the specifications, requirements of a function, and possible edge cases. This is a distinctive feature of test-driven development. It makes the developer focus on test cases before writing the code — a very subtle but important difference.
- Hence, the first step of the development process — write a test.
- In the second stage, run all the tests and check if any test has failed.
- The next step is to write code that will result in a test pass. At this stage, the only purpose of the written code is to pass the test. New code written at this stage can be not perfect and may, for example, ridiculously pass the test. The programmer should not write code that goes beyond the functionality of the test checks.
- Then we run all the tests again. If all the test cases pass, the developer can be sure that the new code meets the test requirements and does not break or affect the existing functionality. If they don't pass, the new code must be modified until it does.
- The last step is code refactoring. You may choose a better algorithm here, clean up the code, change the code structure, and so on.
If necessary, we repeat this process starting with the new test and then the loop is repeated to extend the functionality.
TDD approach has some limitations. For example, it's hard to apply it to Big Data & Data Science projects, because of the data-driven part of those applications, it could also be hard to apply it to GUI or frontend development. Other then that Unit Tests are perfect for any software development life cycle.
So, in conclusion, I can say that unit-testing is a powerful technique that allows you to quickly check the code according to test scenarios, speed up development in the long run and keep the code in working condition.