What is unit testing?
Testing your application will mean creating a battery of individual test cases. When unit testing, each test case is a unit test. A single unit test checks that a single unit of code works. A unit is the smallest testable part of an application. It generally involves a class, a procedure, or a function in your system. Unit tests can be automated and run repeatedly throughout the development process to provide feedback on the code based on what it is expected to do.
Identify the benefits of unit testing
Most of the code you write will be written in collaboration with others, and sometimes it will be used by developers who weren't involved initially. Therefore, following good unit testing practices has several benefits, including:
Maintainability: 🛠 By testing your code and running the test suites regularly, you're completely covered, from basic maintenance like code cleaning (duplication, removing dead code) to more drastic scenarios where decisions on the code need to be made (dealing with unperformant code, code smells, anti-patterns). In the long-term, well-maintained code is easier to extend and modify.
Early bug detection: 🐞 Testing allows you to identify bugs in your code early in development, long before it gets released to users. If the unit fails the test, you can quickly correct your code while everything is still fresh in your mind, significantly reducing the need for hot fixing.
Code confidence: 😎 Test suites are intended to enforce business rules and provide feedback on what the program should do. Good testing practices allow you to be more confident that your code will do what it was intended to do.
Self-validation & documentation: ♼ Code that follows testing practices allows you to establish a solid base from the rules that the system should follow and the decisions that it should perform.
Write good unit tests following the F.I.R.S.T. principles
A good unit test is defined by the F.I.R.S.T. principles of unit testing. Whether you are beginning your first test suite or working on a big team, following these principles will help your code become and remain more robust, independent, and focused.
F.I.R.S.T. stands for:
[F]ast
[I]solated
[R]epeatable
[S]elf-validating
[T]imely
Let's look at each principle:
Good tests are fast
To keep your development fast and covered, your unit tests should be able to provide you feedback about changes introduced in your codebase without significantly slowing down your productivity. The value of your test suite diminishes if you have to spend a significant amount of time waiting for the results.
There isn't a specific, defined timeframe, but standard tests take milliseconds to run, pass or fail. Sounds pretty fast, right? But remember that you'll have thousands of tests to run! If you have, for example, 2000 unit tests, and an average test takes 200 milliseconds to run, then it will take 6.5 minutes to run your complete suite to cover your code and reveal if recent changes have created any obstacles to running a specific process or expected output. You don't want your tests to eat up much more of your time than that.
Fast unit tests allow you to get a quick diagnostic, and to get it often. By running your test suite regularly, you can stay healthy in your test execution. If a particular test case takes longer to execute after a change, you might consider a code refactor or improvement to optimize accordingly.
Good tests are isolated
Don't use tests that depend on other test cases to execute. Otherwise, you may have to spend time troubleshooting and correcting extra tests that are dependent on one another. By keeping tests isolated, it’s easy to locate the failing function, allowing you to know exactly what has gone wrong and where.
Good tests are repeatable
Ideally, your tests should run multiple times with the same results. This ensures your test results are reliable. When you write unit tests, keep in mind that they can be automated to execute whenever you want, and the more, the better.
In big organizations and teams where multiple developers are working on the same portion of a codebase, the code is likely to change frequently or even become reduced as it gets simplified and improved. Your tests should be able to expose paths where the code may behave incorrectly or unexpectedly. You can identify these potential problems by repeatedly running your test suites to get alerted when something goes out of bounds. This may result in discussions about the need to change the code either in the software itself or in the test suites that cover the system.
Good tests are self-validating
Unit tests are intended to be deterministic. They fail or succeed given a specific condition declared. Given a different input, the code should still behave in the same way, guaranteeing the correctness of the system, or its alignment with expectations. There should be no human interaction with them, which could lead to incorrect interpretation of results.
Good tests are timely
Unit tests can be written at any time, but the earlier, the better! You'll be doing yourself a disservice if you write them after your software is developed (or asked to write them). By following either TDD or BDD, and writing your tests before you write the software itself, you will be thinking ahead and supporting your development at every step of the way.
With enough cases, your test suite will reveal and support new changes needed in your software. If a bug is found or a new scenario is discovered, it's a good time (and hopefully not too late) to write a test case that helps reproduce the problem and then to improve the code. The same applies as you add new features to your software. Red test cases (problems) or green test cases (code is doing what it's supposed to do) provide you with feedback on your existing codebase whether you're fixing a bug or introducing a new feature.
By keeping these principles in mind, you'll be set to write reliable unit tests and thus build stable code. Reliable tests are essential not just for ensuring the value of the results that come back to you, but also because other developers who will be incorporating new functionalities into the application in the future will need to rely on the tests you've put in place.
Let's recap!
Unit tests:
Prove that individual units of code work as expected.
Can cover a component, class, interface, or set of methods to be tested.
Are intended to be used regularly to validate feedback changes during development.
Provide higher maintainability, early bug detection, code confidence, self-validation, and code documentation at every stage of development.
Should follow the F.I.R.S.T. principles of good unit testing.