Introduction
Imagine you are building a LEGO structure. As you add more pieces you want to make sure each new piece fits perfectly without compromising the entire structure. In software development unit testing is like checking each LEGO piece before adding it to ensure the whole structure remains strong.
Unit testing is a fundamental practice that help us to ensure that each part of our code works correctly. In this article I will try to explain what unit testing is, why it is important and how we can perform unit testing in .NET using simple tools and techniques.
What is Unit testing?
Unit testing involves testing individual units of code like functions or methods to verify that they perform as expected. Think of a unit test as a small, automated checklist that confirms whether a specific part of your program works correctly.
For example, if you have a method that calculates the division of two numbers, a unit test would check if the method returns the correct result when given specific inputs.
Why is Unit Testing Important?
Lets shift our analogy and please imagine we are building a car. We have to test individual components like the engine, brakes, lights and so on to ensure they function properly on their own. Unit testing serves a similar purpose in software development.
There are several reasons why this is important, but the most significant are:
- Catching bugs early, Much like identifying a faulty car component early on, unit testing helps us detect errors in our code before they escalate into more significant issues
- Easier maintenance, As we keep adding or changing features in our application unit tests ensure that new changes don't break existing functionality
- Confidence in code, Unit tests give us confidence that our code works as intended, making it easier to refactor and extend our application
Getting Started with Unit Testing in .NET
Enough with the theory and lets take a look in a practical example, after all it's well known that we developers prefer code over the theory that comes with it 😁
For my tests I will use the xUnit framework.
- Setting up a Unit Test Project
In .NET, setting up unit testing is straightforward. When we create a new project in our favorite IDE, we can add a unit test project alongside it.
- Create a new project, In our IDE we select to Create a new project and the we have to choose the desirable framework "xUnit" or "NUnit", at the moment these are the most popular testing frameworks in .NET.
- Add a reference, Ensure that unit test project references the project containing the code you want to test.
- Writing our first Unit Test Let's say we have a simple method in our code that divides two numbers
public class Calculator
{
public int Div(int numerator, int denominator)
{
return numerator / denominator;
}
}
To test this method we will write a unit test like the following:
public class CalculatorTests
{
[Fact]
public void Div_ReturnsCorrectResult()
{
// Arrange
Calculator calculator = new Calculator();
int numerator = 10;
int denominator = 2;
// Act
int result = calculator.Div(numerator, denominator);
// Assert
Assert.Equal(5, result);
}
}
Lets break it down into pieces so we can better understand it:
First of all, as you can easily see, I have deliberately divided the test into three parts: Arrange, Act, and Assert. The reason for this is that it is considered good practice in our tests to use the AAA pattern. This way, we keep our tests better organized, making them easier to read and understand.
- Arrange, in this section we prepare any objects or data needed for the test, like creating an instance of the calculator object or assign values to the
numerator
anddenominator
variables - Act, here we are calling the method that we want to test,
Div
in our case - Assert, we check if the result is what we expected, in our case if
10 / 2
should equal5
Testing Multiple Inputs
In the example above, we created our test and ran it with a single set of inputs. But how can we run the test again using a different set of inputs? .NET testing frameworks offer a way to accomplish this without having to rewrite the entire test.
Example with the use of xUnit
[Theory]
[InlineData(10, 2, 5)] // Test case: 10 / 2 = 5
[InlineData(20, 4, 5)] // Test case: 20 / 4 = 5
[InlineData(0, 1, 0)] // Test case: 0 / 1 = 0
[InlineData(-10, -2, 5)] // Test case: -10 / -2 = 5
public void Div_ReturnsCorrectResult(
int numerator,
int denominator,
int expected)
{
// Arrange
Calculator calculator = new Calculator();
// Act
int result = calculator.Div(numerator, denominator);
// Assert
Assert.Equal(expected, result);
}
Here we are using [Theory]
and [InlineData]
instead of the [Fact]
for parameterized tests. Each [InlineData]
attribute provides a different set of inputs to the test method
Similarly in NUnit
we can run tests with multiple inputs by using the [TestCase]
attribute
[TestCase(10, 2, 5)] // Test case: 10 / 2 = 5
[TestCase(20, 4, 5)] // Test case: 20 / 4 = 5
[TestCase(0, 1, 0)] // Test case: 0 / 1 = 0
[TestCase(-10, -2, 5)] // Test case: -10 / -2 = 5
public void Div_ReturnsCorrectResult(
int numerator,
int denominator,
int expected)
{
// Arrange
Calculator calculator = new Calculator();
// Act
int result = calculator.Div(numerator, denominator);
// Assert
Assert.AreEqual(expected, result);
}
Techniques for Effective Unit Testing
Test-Driven Development (TDD)
Think of designing a car where you first create a detailed blueprint before starting the assembly. In Test-Driven Development, we write the unit tests before developing the actual code. This approach ensures that our code is built according to the specifications set by the tests, leading to a more reliable and testable software.-
Mocking Dependencies
Sometimes, a method we want to test depends on external resources, like needing a specific part that’s not yet available when building our car. In these situations, we can usemocking
to create a simulated version of the required component. This allow us to test our car’s assembly process without relying on the actual, unavailable part.For example, if a method retrieves data from a database, we can "mock" the database to return a specific value without actually querying the real database.
Libraries like Moq or NSubstitute are popular choices for mocking in .NET.
-
Testing Both Happy and Sad Paths
When testing our code it is important to check not only the ideal scenarios (happy paths) but also how our code behaves under less that ideal conditions (sad paths).- Happy Path Testing: This is like making sure that all the pieces fit perfectly when you follow the instructions step by step. We always want to verify that our code works as expected when everything goes right. For example, if our method is supposed to divide two numbers we write a test to ensure it returns the correct result.
- Sad Path Testing: In our example, we create a test method that examines the
Div
method, assuming the inputs are always valid. However, what happens if the denominator is zero? Sad path testing ensures that our code does not crash in such scenarios and either manages the error gracefully or provides a meaningful error message.
Here is an example on how we might test it:
[Fact] public void Div_DivideByZero_ThrowsDivideByZeroException() { // Arrange Calculator calculator = new Calculator(); // Act & Assert Assert.Throws<DivideByZeroException>(() => calculator.Div(10, 0)); }
Testing both paths ensures that our code is robust and can handle real world scenarios where users may not always follow the “happy path.”
-
Writing Clean and Maintainable Tests
- Keep Tests Simple: Each test should focus on one thing. Think of it as testing one car component at a time.
- Name Tests Clearly: Use descriptive names so that anyone reading your tests can easily understand what they do. For example,
Div_ReturnsCorrectResult
clearly indicates that the test checks whether the Div method returns the correct result. - Avoid Duplicating Code: If multiple tests require the same setup, consider using helper methods or a test setup method to avoid duplicating code.
Conclusion
Unit testing in .NET is like building a LEGO structure piece by piece, ensuring each part fits perfectly before moving on to the next. By catching bugs early, simplifying maintenance, and boosting confidence in our code, unit testing becomes an invaluable tool in our development process.
Remember to test both the happy and sad paths. While happy path testing confirms that our application works under ideal conditions, sad path testing checks its resilience and ability to handle unexpected scenarios. Together, they ensure that our application is robust, secure, and ready for real-world use.
Start small, use the tools available in .NET like xUnit
or NUnit
, and incorporate best practices like TDD, mocking, and comprehensive path testing. With these techniques, you'll be well on your way to building robust, reliable applications that stand the test of time—just like a well-built LEGO masterpiece 😃
If you enjoyed my article and are interested feel free to share it in. If you want to read more about Unit testing you can visit this link.
Happy coding! 🚀💻
Top comments (5)
I find Specflow C# a double edge sword provides the unit test capabilities along with documentation of the tests.
Thank you for reading my article! I appreciate your insights on SpecFlow C#. I haven’t had the chance to work with it myself, but I will take a closer look at it. Thanks for bringing it to my attention!
If you haven't tried Stryker.NET yet, you really should. It is great for finding mistakes with your unit tests.
Hello! Thank you so much for reading my article! To be honest, I haven't looked into Stryker.NET yet, but I will definitely check it out. Thank you very much!