Bugs or flaws in production code are a given. They simply aren’t going to go away. Which is why any developer should know there is no proper coding without unit testing. However, some developers don’t realize that unit testing is one of the essential parts of any software development cycle or process.
And it’s the reason why getting it right is so critical. Everything from when to test, to whether to mock or not is essential. These and a few other factors we will discuss will help determine what constitutes a good versus a poor unit test.
Unit Testing: The Fundamentals
A unit is essentially the smallest collection of code which can be tested usefully. Roy Osherove, the author of The Art of Unit Testing said, “A unit test is an automated piece of code that invokes a unit of work in the system and then checks a single assumption about the behavior of that unit of work.”
It is the initial level of software testing. There are two important aspects of unit testing that must be considered. The unit test case should, first and foremost, focus specifically on functionality. Secondly, it should do so in isolation. Externalities of any type should not be part of the unit test.
Unit Testing: How Much Is Needed?
The amount of testing undertaken can be an important contributing factor to how good or poor a unit test will be. The ratio between the production code and the test code could be anywhere between 1:1 and 1:3. For example, one to three lines of test code per every line of production code. However, the ratio can sometimes get as high as 1:10.
The mark of a good unit test is that it should offer invaluable feedback on the modularity and design of the code being developed.
When To Mock
When to mock may depend on what “school” a developer prefers. The London school, which is also know as the “mockist” school believes all mutable dependencies or collaborators should be replaced with mocks. The classical school, also known as the “Detroit” school believes only shared (mutable out-of-process) dependencies should be mocked.
Mocking should be done sparingly since mocks can:
- Result in violations of the DRY (Don’t Repeat Yourself) principle
- Make refactoring more difficult
- Reduce the simplicity of the code design
When Not To Mock
Knowing when not to mock requires asking a simple question: Will the mocking replace a dependency in a unit under test with a stand-in for that dependency?
Examples of when not to mock could include the following scenarios:
- When the mock might override the logic of the mocked class
- When the mock has attributes, methods, or arguments that the real object does not
- When the mock has side-effects and behavior that differ to those of the real object
- When it’s the business domain logic that is being mocked
- When it’s mocking any infrastructure dependency that is directly attributable to the given business domain
When To Write Tests
When to write a unit test can be answered by asking this question: What is it that needs to be achieved for the code to be maximized? Here are three typical possibilities:
- The number of bugs needs to be reduced early, which is the most important reason why unit testing may be needed
- Code design needs to be improved, which is especially true in test-driven developments (TDDs)
- Proof of the coding process is required, and a unit test provides documentation regarding the system and its functionality
Furthermore, unit testing can help improve teamwork, particularly within an Agile context. Collaboration benefits from insight, which happens when team members can review the logic behind code.
Are Unit Tests For GUI Interactions A Good Idea?
GUIs are increasingly critical components of software, and these days their testing is imperative. Unit testing can ensure that testing a GUI is more thorough. Here’s how that is accomplished:
- Unit testing is more robust than GUI testing, which tends to be slower and more fragile
- Unit testing reduces lost time, both in the writing and execution of test cases
- Unit tests focus solely on functionality, meaning defects thereof are easily detected, whereas GUI tests focus more on the integration of functionalities
There is one important caveat however, and that is unit tests are not viable for testing how an application behaves under real-world conditions. For that, functional testing via the UI is the more effective testing methodology.
What Is A Good Unit Test?
A good unit test encompasses many different attributes or practices, including that it should:
- Be small and isolated
- Be about something highly specific regarding the code
- Have a ratio of testing to assertion near 1, thereby making it easier to identify any failed assertion
- Be singular and therefore with its own build-up and tear-down
- Focus on the behavior of code, making any failure thereof easier to test and assess
- Keep mocking to a minimum, since too many fakes can break when the code is altered
- Ensure there is a clear naming convention at all times, so the code is kept comprehensible.
More Attributes Of A Good Unit Test
A good, value-adding unit test should encompass even more, meaning the test should:
- Run in memory, for example with no DB or file access
- Consistently return the same result
- Full control is needed over all tested units; and
- Mocks or stubs should be used in isolation when needed.
Furthermore, a unit test should be fully automated, fast, readable, maintainable and trustworthy. A unit test is ultimately about catching bugs in the code. A test suite that never goes red equates to unit testing that is actually not working and is therefore of limited or no value.
An example of good unit testing can be seen with this partial extract of test code:
This unit test will work for a variety of reasons, including:
- This class is testable
- It has two public methods
- It has no complex dependencies
- It is highly flexible, for example, testing can be done for different jurisdictions or countries where date or currency formats are altered
In its best practices for writing unit tests, Microsoft suggests that minimally passing tests are best. Microsoft claims that behavior being verified (tested) should be as simple as possible, thereby making the unit test more resilient to future alterations in the codebase and more aligned to actual behavior.
So, What Is A Poor Unit Test?
Likewise, a poor unit test can arise due to different attributes or practices, which should be avoided and may include:
- Non-deterministic factors in the code-base are problematic, since they are difficult to test; for example, time as an authentication factor in code can fail due to different time zones
- Side-effecting methods are also difficult and problematic to test, resulting in unwarranted complexities in the code
- Anti-patterns, secret dependencies, and other “back-door” scenarios are also highly problematic
An example of poor unit testing can be seen with this code:
There are three problems with this unit testing:
- The name is too vague – what is the unit test actually about?
- What specifically is being tested – the sorter or the case modifier?
- Is it to be assumed that two issues are being tested simultaneously?
A simple example of poor unit testing practice is that which doesn’t consider future code. Adrian Bolboacă discussed this at the 2017 European Testing Conference. Business domain language can be critically important, including making a clear test name. He cited how vague test names like “ExceptionOnOverflow” or “CustomerTest” could be highly problematic for future testing, collaborations, and clients. Hyper-specific names are needed, such as “WhenTooManyPlayersAreAddedAnErrorIsReturned” or “InvalidCustomerIsRejectedByOrder”.
Writing a viable, value-adding unit test is as difficult as writing good production code. Done right they should be both effective in discovering defects as well as being inexpensive to maintain.