Unit Tests - Keeping them clean and simple
Tests should be split up by behaviour
One behaviour, one test. This means don't test more than one thing in a test. Ideally there should be Acceptance Criteria clarifying each expected behaviour. This gives you a solid starting point, but Unit tests can expand on these scenarios by covering edge cases.
Telltale signs of breaking this guideline include:
- Multiple "Act" steps (this is a big sign)
- Multiple asserts throughout a test
- Multiple unrelated asserts at the end of a test
- Data is being changed after the Act step
- Using the word "correct" in your test name - indicates vague expectations
Don't be afraid to create lots of test methods for a single scenario or unit under test, testing various behaviours. When writing unit tests, you can make a new class for each scenario, this gives you power over the TestInitialize/Cleanup methods, allowing you to keep your tests cleaner.
Readability is Everything
The number one thing to remember when writing tests is that they need to be clear. This means they should be simple and concise, and separated by requirement.
Place the same importance on readability as you would for production code. We spend 10 times more time reading code than writing it. It needs to be extremely easy to read in order to be maintainable.
By making the tests readable, you should immediately be able to see what your tests are doing, and easily see how the tests in a class differ from each other. So bear this in mind, especially in tests that require large amounts of data setup.
Think about what the test does and ensure this is communicated clearly. You can do this in several ways:
Name of the Test
The industry standard format for naming tests is:
The reason for this is so that we can see at a glance what is being tested. We should be able to quickly discern what the test is testing, what the scenario is, and the expectation.
Using the word "Correct" in the expectation part is not ideal, because it does not make it clear what "correct" means. Be specific about what your test expects.
Test Description Tag
A clear name for the test is paramount, but if not enough information can be conveyed in the name, consider use of some kind of [Description] attribute or other labeling on the test to provide more information.
Throughout your test, consider the use of comments to make it clear why you are setting up data in that way, to highlight when you are performing the action, or anything else that may not be immediately clear.
Consider the format of a test:
- Arrange: This is where you set up the data/objects that are required to perform the action.
- Act: This is generally one line, where you call the method / unit under test, or perform the required action.
- Assert: This is where you check that the expectation has been fulfilled.
You don't necessarily need to write // Arrange/Act/Assert as comments but it should be clear where each step is happening. At the very least, leave an empty line between each of these sections.
If you keep this arrangement in mind, you will find it easier to follow good testing practices and split tests correctly by behaviour. All tests should have an action and an expectation, not all require setup.
Abstract out large "Arrange" blocks
Keeping data setup areas short is a good way to improve readability. Nobody wants to look at large blocks of constuctors/initialisers. Importantly, it should be clear what is different about this data compared with the next test in the class.
To keep arrange steps short and clean, feel free to create extension methods or other helper classes to carry out frequently performed data creation strategies.
Unit tests should be clean and simple. Most importantly, they should clearly define the action or scenario, and the expectation.
Remembering this will ensure that your tests are useful, easy to maintain, and help to document your code.