Unit Testing in .NET: the right way

Rémi Despelchin
3 min readSep 12, 2022

🟩🟦🟦 Beginner

Disclaimer: this article is focused on the unit tests and not on a higher layer of the “Test Pyramid”.

Choose your weapons

The .NET ecosystem provides a large number of testing libraries.
In my humble opinion, there are five among them you need to know.

  • xUnit: the best test runner, compatible with all IDE and providing great flexibility across test use cases.
  • Moq: a straightforward mocking library with good support of async/await.
  • FluentAssertions: a considerable time saving, 95% of your assertion needs are present in this package (to compare values, objects, or arrays).
  • AutoFixture: randomize everything! Improve your test reliability by avoiding the use of static test variables (aka. fixture).
  • ArchUnitNET: a “frame” for your software architecture. It can be a great help, especially in a team with junior developers.

Warmup

This is not an article on unit testing best practices (there are already many of them) but, if you have to remember only one thing: writing unit tests should not be a pain point.

Consider an existing solution composed of an empty “console app” project named Service.csproj.
Add to it a new “xUnit Test Project” from the project template list, naming it “Tests.csproj”. (xUnit team offers setup for other IDE like Rider here).

Edit Tests.csproj file to add a reference to the existing “Service” project and add the four missing packages (xUnit is already added).

Then, add some business logic inside your Service: a panda renamer 🐼.

💢 WHAT?! That’s not TDD!
Yeah, that’s irrelevant to the purpose of this article, but I intentionally left an interface unimplemented.

From this code, we could highlight two test cases.

  1. Providing an empty name to the Execute function, check if it threw an ArgumentNullException then verify if pandaFetcher has not been called.
  2. Providing a valid name to the function, check if the new panda name is equal to the “newName” parameter.

Combine forces

Now, we are ready to create our first unit test file named “PandaRenamerTest.cs” inside the “Tests” project.

We’ll use the power of these four packages (xUnit, FluentAssertions, Moq and AutoFixture) to reduce the code complexity and increase reliability.

That’s all! Few lines of code, no fixture, and good readability.
From now on, I’m sure you’ll enjoy reaching the 80% code coverage.

Hold on!

You’ve reached 80% of code coverage, great! You could now invest some time to ensure that the architecture will be followed throughout the project.

ArchUnitNET was built for this purpose if you want to check that all of your use cases avoid using code from “Service.Mordor.*” namespace, you could!

If, unfortunately, a developer uses some code from the “Service.Mordor.*” namespace inside a “Core” use case, the unit test will fail with a clear error message.

Conclusion

I regret to inform you that 80% code coverage is not enough 🙃, but it’s a good start!
Indeed, the next step is to check if your code works well in its entirety.
We’ll use integration tests to reach ~90% code coverage in the next chapter.

You can access the entire source code of this post on GitHub, stay tuned!

Edit #1

As xeladu pointed out: “A code coverage metric of 80% or similar doesn’t indicate that your testing is good.” and he’s right.
You should test your tests with mutation testing to increase theirs reliability, Stryker is a great tool!

Edit #2

Updated to .NET 7.0 !

--

--