Unit Testing Best Practices
Best Practices
- Try not to introduce dependencies on infrastructure when writing unit tests.
- These make the tests slow and brittle and should be reserved for integration tests.
- You can avoid these dependencies in your application by following the Explicit Dependencies Principle and using Dependency Injection.
- You can also keep your unit tests in a separate project from your integration tests. This ensures your unit test project doesn’t have references to or dependencies on infrastructure packages.
Naming Your Test
The name of your test should consist of three parts:
- The name of the method being tested.
- The scenario under which it’s being tested.
- The expected behavior when the scenario is invoked.
Why?
- Naming standards are important because they explicitly express the intent of the test.
Tests are more than just making sure your code works, they also provide documentation. Just by looking at the suite of unit tests, you should be able to infer the behavior of your code without even looking at the code itself. Additionally, when tests fail, you can see exactly which scenarios do not meet your expectations.
Bad:
Better:
Arrange Your Tests
Arrange, Act, Assert is a common pattern when unit testing. As the name implies, it consists of three main actions:
- Arrange your objects, creating and setting them up as necessary.
- Act on an object.
- Assert that something is as expected.
Why?
- Clearly separates what is being tested from the arrange and assert steps.
- Less chance to intermix assertions with “Act” code.
Readability is one of the most important aspects when writing a test. Separating each of these actions within the test clearly highlights the dependencies required to call your code, how your code is being called, and what you are trying to assert. While it may be possible to combine some steps and reduce the size of your test, the primary goal is to make the test as readable as possible.
Write minimally passing tests
The input to be used in a unit test should be the simplest possible in order to verify the behavior that you are currently testing.
Why?
- Tests become more resilient to future changes in the codebase.
- Closer to testing behavior over implementation.
Tests that include more information than required to pass the test have a higher chance of introducing errors into the test and can make the intent of the test less clear. When writing tests, you want to focus on the behavior. Setting extra properties on models or using non-zero values when not required, only detracts from what you are trying to prove.
Avoid Logic in tests
When writing your unit tests avoid manual string concatenation and logical conditions such as if, while, for, switch, etc.
Why?
- Less chance to introduce a bug inside of your tests.
- Focus on the end result, rather than implementation details.
When you introduce logic into your test suite, the chance of introducing a bug into it increases dramatically. The last place that you want to find a bug is within your test suite. You should have a high level of confidence that your tests work, otherwise, you will not trust them. Tests that you do not trust, do not provide any value. When a test fails, you want to have a sense that something is actually wrong with your code and that it cannot be ignored.
If logic in your test seems unavoidable, consider splitting the test up into two or more different tests.
Bad:
Better:
Avoid Multiple Asserts
When writing your tests, try to only include one Assert per test. Common approaches to using only one assert include:
- Create a separate test for each assert.
- Use parameterized tests.
Why?
- If one Assert fails, the subsequent Asserts will not be evaluated.
- Ensures you are not asserting multiple cases in your tests.
- Gives you the entire picture as to why your tests are failing.
When introducing multiple asserts into a test case, it is not guaranteed that all of the asserts will be executed. In most unit testing frameworks, once an assertion fails in a unit test, the proceeding tests are automatically considered to be failing. This can be confusing as functionality that is actually working, will be shown as failing.
A common exception to this rule is when asserting against an object. In this case, it is generally acceptable to have multiple asserts against each property to ensure the object is in the state that you expect it to be in.
Validate private methods by unit testing public methods
In most cases, there should not be a need to test a private method. Private methods are an implementation detail. You can think of it this way: private methods never exist in isolation. At some point, there is going to be a public-facing method that calls the private method as part of its implementation. What you should care about is the end result of the public method that calls into the private one.
Consider the following case
Your first reaction may be to start writing a test for TrimInput because you want to make sure that the method is working as expected. However, it is entirely possible that ParseLogLine manipulates sanitizedInput in such a way that you do not expect, rendering a test against TrimInput useless.
The real test should be done against the public-facing method ParseLogLine because that is what you should ultimately care about.
With this viewpoint, if you see a private method, find the public method and write your tests against that method. Just because a private method returns the expected result, does not mean the system that eventually calls the private method uses the result correctly.