Article
23 Apr
2023

Effective Unit Testing in .NET

What is unit testing and what are the benefits?
Paulo Martins
|
5
min read
effective-unit-testing-in-net

Unit testing is an important part of software development which is often overlooked since, at first glance, it doesn't seem to directly impact project development.

Besides the time spent training the development team to write unit tests, there is also the time spent maintaining those which will inevitably break as the codebase evolves. It might seem like a lot of work. So why should we bother?

Effective Unit Testing in .NET

What is Unit Testing?

Unit testing is a type of a software testing method where small components (units) are tested to determine if they behave as expected. These tests should not cross their own unit boundary as this turns them into integration tests - another important testing method, with its own advantages and disadvantages.

Benefits of Unit Testing

There are a number of key benefits to unit testing. As a project grows in size and complexity, it becomes increasingly hard to modify a part of the system without the risk of another part breaking. And while unit testing is not a silver bullet, it provides us with many advantages:

  • Higher quality code - Unit testing promotes the use of good software design patterns. This is because it requires the code to be decoupled, with dependencies between different modules kept to a minimum.
  • Fewer bugs – Regressions are found early in the development cycle with unit testing. Plus, it helps developers think more deeply about the code being tested, which helps them avoid careless mistakes.
  • Documentation - Unit tests can be seen as the design specification of the tested components. For this reason, they also serve as documentation, especially when combined with good naming practices.
  • Easier to refactor - This is especially important when dealing with legacy codebases as we can make changes with confidence that the system will still work.
  • Debugging - If a unit test fails, we only need to worry about the latest code changes.
  • Reducing costs - As a consequence of bugs being found early in the development cycle, the cost of finding and fixing problems is greatly reduced. Especially compared to fixing the problem when the code is in a later stage of the development cycle, or even when it’s released.
  • Saving time - For all these reasons, unit testing can actually save time for all those in the development cycle, and keep everyone’s sanity on complex projects.

Now that we know what unit testing is and why it’s essential, let's look at how can we apply it on a .NET Framework project. We are going to use xUnit.net as the testing tool, Moq as the mocking framework and Autofixture to generate objects for us.

Setup

Let’s say we have a music store where we save information about the album, artist, available stock and format (CD, Vinyl, digital). A store employee can create, delete and modify existing entries in the store. Let’s say we want to test the following method that retrieves the album ID:

In this simple case, we retrieve an entry from the repository and have 2 possible outcomes: either the entry is null or is not null, in which case the entry is returned.

Organisation & Naming

The unit tests we write serve as documentation for the system, so naming and organizing the test projects should follow best practice. Test projects should follow the same folder organization as the tested project and are usually named ProjectName.Tests.

This particular method is contained in the InventoryService class, so let’s create an InventoryServiceTests class in our newly created test project. This will contain all the tests for the InventoryService class.

When it comes to naming tests, there are multiple approaches, all with their own strengths. I personally use MethodName_StateUnderTest_ExpectedBehavior as it’s pretty descriptive, and makes it easy to find the method being tested.

Testing

Now we’re ready to create our first test. Remember that in our case, the method being tested can either return the entity with the given album ID, or throw an exception, if the album ID isn’t valid. So let’s test for both scenarios:

There's a lot going on here. Let's take it step by step

  1. In the class constructor, we’re creating a mock instance of the inventory repository, called in the method we are testing. This is where the importance of a good architecture comes into play: this dependency is supplied to the InventoryService class via dependency injection and as such, we can easily mock the repository layer, thanks to mocking frameworks like Moq.
  2. When using xUnit, test methods use the Fact attribute. There are also Theories which are parameterized tests for when we want to test the same method with multiple inputs. You can read about this in more detail on the official documentation.
  3. We use the Arrange-Act-Assert pattern to clearly separate our steps:
  • Arrange - A mock entry is created with the help of AutoFixture, which is then used to set up the repository method, called in the class being tested. We pass this to the service class.
  • Act - The method being tested is called with the same parameter used in the repository setup phase.
  • Assert - The returned object is checked against what we expect to be the result. In this case, we expect the result to be the same mock entry that we’ve created.

After running the test, it successfully passes. Let’s move on to the next scenario:

Modifications to the class illustrate some important points:

  • Always declare constants instead of passing hardcoded values as parameters.
  • Since we would be repeating the initialization of the fixture, we can move it to the constructor. It’s usual for a large portion of related methods to share the same initialization. xUnit helps us, as every test is run in isolation, so the constructor is called before every test run.
  • The repository setup was made more generic by accepting any integer.
  • Assertions can be done when the method is asynchronous and throws an exception. In our case we throw an exception, which we handle in a different part of the application, so we want to make sure the exception is the correct type.

Code Coverage

After running our newly created tests we can see that our code coverage is 100%. This isn’t always possible to achieve in the real world due to time constraints. For this reason, you should always give priority to critical paths when implementing unit tests. It’s also important to give unit test to fixed bugs, to prevent regressions.

Unit testing is a learning curve and may take some time to get used to. But combining different tools such as xUnit, Moq and AutoFixture can make creating and maintaining unit tests a breeze.

Happy testing!

Our Most Recent Blog Posts

Discover our latest thoughts, tendencies, and breakthroughs in the realm of software development and data.

Swipe to View More

Get In Touch

Have a project in mind? No need to be shy, drop us a note and tell us how we can help realise your vision.

Please fill out this field.
Please fill out this field.
Please fill out this field.
Please fill out this field.

Thank you.

We've received your message and we'll get back to you as soon as possible.
Sorry, something went wrong while sending the form.
Please try again.