Working with unit tests
A common understanding of unit testing is the testing of the smallest possible part of software, such as a single method, a small set of related methods, or a class.
In reality, we do not test methods; we test a logical unit and its behavior instead. Logical units can extend to a single method, to an entire class, or a collaboration of multiple classes.
For example, a standard calculator program can have an add method for adding two numbers. We can verify the add behavior by invoking the add method, or we can design the calculator program to have a simple calculate API, which can take two numbers and an operation (add, subtract, divide, and so on). Depending on the operand type (integer, double, and so on), the calculator may delegate the calculation to a collaborator class, such as a double calculator or a long calculator. We can still unit test the add behavior, but multiple classes (units) are involved now.
A unit test verifies an assumption about the behavior of the system. Unit tests should be automated to create a safety net so that the assumptions are verified continuously and a quick feedback can be provided if anything goes wrong.
The following are the benefits of test automation:
- Behavior is continually verified: We refactor code (change the internal structure of the code without affecting the behavior of the system) to improve the code's quality, such as maintainability, readability, or extensibility. We can refactor code with confidence if automated unit tests are running and giving feedback.
- The side effects of code changes are detected immediately: This is useful for a fragile, tightly-coupled system, where a change in one module breaks another module.
- Saves time; no need for immediate regression testing: Suppose that you are adding a scientific computational behavior to an existing calculator program and modifying the code; after every piece of change, you do a regression testing to verify the integrity of the system. Manual regression testing is tedious and time-consuming, but if you have an automated unit test suite, then you can delay the regression testing until the functionality is done. This is because the automated suite will inform you at every stage if you break an existing feature.
A unit test should exhibit the following characteristics:
- It should be automated, as explained in the preceding section.
- It should have a fast test execution. To be precise, a test should not take more than a few milliseconds to finish execution (they should be fast; the faster, the better). A system can have thousands of unit tests. If they take time to execute, then the overall test execution time will go up; as a result, no one will be interested in running the tests. It will impact the feedback cycle.
- A test should not depend on the result of another test or rather test execution order. Unit test frameworks can execute tests in any order. So, if a test depends on another test, then the test may fail any time and provide wrong feedback. You want tests to be standalone so that you can look at them and quickly see what they're actually testing, without having to understand the rest of the test code.
- A test should not depend on database access, file access, or any long running task. Rather, an appropriate test double should isolate the external dependencies.
- A test result should be consistent and time-and-location transparent. A test should not fail if it is executed at midnight, or it should not fail if it is executed in a different time zone.
- Tests should be meaningful. A class can have getter and setter methods; you should not write tests for the getters and setters because they should be tested in the process of other more meaningful tests. If they're not, then either you're not testing the functionality or your getters and setters aren't being used at all; so, they're pointless.
- Tests are system documentation. Tests should be readable and expressive; for example, a test that verifies the unauthorized access could be written as
testUnauthorizedAccess()
or ratherwhen_an_unauthorized_user_accesses_the_system_then_raises_secuirty_error()
. The latter is more readable and expresses the intent of the test. - Tests should be short and tests should not be treated as second-class citizens. Code is refactored to improve the quality; similarly, unit tests should be refactored to improve the quality. A test class of 300 lines is not maintainable; we can rather create new test classes, move the tests to the new classes, and create a maintainable suite.
As per the preceding best practices, a test should be executed as fast as possible. Then what should you do if you need to test data access logic or file download code? Simple, do not include the tests in an automated test suite. Consider such tests as slow tests or integration tests. Otherwise, your continuous integration cycle will run for hours. Slow tests should still be automated. However, they may not run all the time, or rather they should be run out of the continuous integration feedback loop.
You cannot automate a unit test if your API class depends on slow external entities, such as data access objects or JNDI lookup. Then, you need test doubles to isolate the external dependencies and automate the unit test.
The next section covers test doubles.