Unit tests are often treated like second class citizens and not given the same level of polish and refactoring as our production code. As a result, they can wind up brittle, unclear, and hard to maintain.
In this article, I’m going to show you a few tricks to keep your unit tests useful, maintainable, and relevant.
For this article, we’ll be working with tests that test a fictitious resume processing application. We’ll start with a single test, expand it, then refactor it to keep things usable.
Sample Unit Test
In this test we use the XUnit testing framework to run a single action against the system under test and then make an assert around it. Note that we follow an arrange, act, assert pattern to differentiate setup, execution, and verification.
Even with a simple test like this, there are some things that bug me.
Using Shouldly for Assertions
First of all, I hate the syntax for assertions. It doesn’t read well and I often confuse which parameter is the expected value and which is the actual value.
I prefer to install the Shouldly NuGet package which lets me write cleaner assertions. This lets us change the code to the following:
Much easier to read, isn’t it? There’s a wide variety of methods available via Shouldy for equality, reference equality, and collection testing.
Note: Many people swear by the more popular FluentAssertions library, but I generally prefer Shouldly’s more concise syntax.
Using Bogus to hide meaningless values
Looking at the prior test, it’s not clear what values in the Arrange step are actually relevant to the test. In this particular test, the behavior under test is actually the rule that if the Resume is for “Matt Eland”, the system should return a maximum score (hey, it’s my sample application, I’ve got to have a little fun here).
The Bogus library can help with that by providing randomized values for the aspects of a test that are not relevant.
Bogus has a wide variety of random data generators from random numbers to names to zip codes and addresses to company names, business jargon, and hacker phrases.
Here’s our test case using Bogus to hide the irrelevant:
Extracting Methods to Hide Setup Details
Now that the meaningless values in our test have been hidden, the actual test is clearer, but the setup code is getting unruly. Let’s extract a method for adding a random job.
That’s much more clear! While we’re at it, we can extract a method for creating a resume analyzer and analyzing the resume.
Now we’re down to a very concise and readable test method. As an added bonus, if we change the signature of Analyzer or want to use a different provider for tests, we can substitute it in one method instead of having to maintain it in each individual test case.
It’s almost time to expand our tests, but before we do that, let’s introduce a base class that other test classes can inherit from.
Extracting Abstract Classes to Improve Tests
We can increase the visibility of our two private methods and pull them into a new abstract class called
ResumeTestsBase, then have
SpecialCaseTests inherit from it.
Here’s our base class:
Note that we moved Faker to a lazily instantiated property so we can reuse it in other methods in the future. We also made the
CreateMonthsInJob method parameterized to help a future test.
This base class allows us to have a very minimal and focused test class:
Now we can add in a new class to test new aspects of the system under test.
Okay, that’s fine, but we should test more than just one value. Scaling it up begins to present problems:
The duplication factor is starting to present itself again. Thankfully all modern .NET testing frameworks support parameterized tests. In XUnit this is called a Theory and it looks like this:
Theory tests we have a four clearer tests using one method in fewer lines of code than two tests using the
The test runner will see this test case as four separate tests and run each individually, passing in the
InlineData to the parameters for the method.
These are just some basics on creating clear, concise, and maintainable unit tests. There are many other libraries and techniques out there, but these basic techniques will help you build a solid test suite that shines in simplicity, utility, and maintainability.