Skip to main content

.NET Unit Testing

Introduction

Unit testing is a fundamental practice in modern software development that helps ensure your code functions as expected. In the .NET ecosystem, unit testing involves writing code that verifies the behavior of individual units or components of your application in isolation.

In this guide, we'll explore how to implement effective unit tests in .NET applications. Whether you're building web applications with ASP.NET Core, desktop apps with WPF, or libraries for general use, the concepts and techniques covered here will help you create more reliable and maintainable code.

Why Unit Testing Matters

Before diving into the technical details, let's understand why unit testing is crucial:

  • Bug detection: Find issues early in the development process
  • Code quality: Encourages better, more modular code design
  • Documentation: Tests serve as living documentation of how code should work
  • Confidence: Build confidence in making changes without breaking existing functionality
  • Faster development: Ultimately saves time by reducing debugging and manual testing

Unit Testing Frameworks in .NET

The .NET ecosystem offers several testing frameworks to help you write and run unit tests:

1. MSTest (Microsoft Test)

This is Microsoft's built-in testing framework, integrated directly with Visual Studio.

2. NUnit

One of the most popular open-source testing frameworks, offering a rich set of assertions and test attributes.

3. xUnit.net

A newer, community-focused testing tool for .NET, designed with extensibility in mind.

For this guide, we'll primarily focus on MSTest and xUnit, but most concepts apply across all frameworks.

Getting Started with MSTest

MSTest is the easiest framework to start with for beginners as it's integrated into Visual Studio.

Setting Up Your First Test Project

  1. In Visual Studio, right-click on your solution and select Add > New Project
  2. Search for "test" and select MSTest Test Project
  3. Name your project (typically your main project name with ".Tests" appended)

Writing Your First Test

Let's create a simple calculator class and test its addition function:

First, in your main project, create a calculator class:

csharp
namespace MyApp
{
public class Calculator
{
public int Add(int a, int b)
{
return a + b;
}

public int Subtract(int a, int b)
{
return a - b;
}
}
}

Now, in your test project, write a test:

csharp
using Microsoft.VisualStudio.TestTools.UnitTesting;
using MyApp;

namespace MyApp.Tests
{
[TestClass]
public class CalculatorTests
{
[TestMethod]
public void Add_TwoNumbers_ReturnsCorrectSum()
{
// Arrange
Calculator calculator = new Calculator();

// Act
int result = calculator.Add(5, 3);

// Assert
Assert.AreEqual(8, result);
}
}
}

This test follows the AAA pattern:

  • Arrange: Set up the test conditions
  • Act: Execute the code being tested
  • Assert: Verify the results match expectations

Running Tests

To run your tests:

  1. Open Test Explorer (Test > Test Explorer)
  2. Click "Run All" or right-click on specific tests to run them

Unit Testing with xUnit

xUnit is a more modern framework with some different conventions.

Setting Up xUnit

  1. Add a new .NET Class Library project to your solution
  2. Install the xUnit NuGet packages:
    • xunit
    • xunit.runner.visualstudio

Writing Tests in xUnit

The same calculator test in xUnit would look like:

csharp
using Xunit;
using MyApp;

namespace MyApp.Tests
{
public class CalculatorTests
{
[Fact]
public void Add_TwoNumbers_ReturnsCorrectSum()
{
// Arrange
Calculator calculator = new Calculator();

// Act
int result = calculator.Add(5, 3);

// Assert
Assert.Equal(8, result);
}
}
}

Notice the differences:

  • [TestClass] is not needed
  • [TestMethod] becomes [Fact]
  • Assert.AreEqual becomes Assert.Equal

Parameterized Tests in xUnit

xUnit makes it easy to run the same test with different inputs:

csharp
[Theory]
[InlineData(5, 3, 8)]
[InlineData(0, 0, 0)]
[InlineData(-5, 5, 0)]
[InlineData(int.MaxValue, 1, int.MinValue)] // Overflow case
public void Add_MultipleScenarios_ReturnsExpectedResults(int a, int b, int expected)
{
// Arrange
Calculator calculator = new Calculator();

// Act
int result = calculator.Add(a, b);

// Assert
Assert.Equal(expected, result);
}

This approach allows you to test multiple scenarios without duplicating test code.

Best Practices for Unit Testing

1. Test One Thing per Test

Each test should verify a single behavior or outcome:

csharp
// Good: Tests one specific behavior
[TestMethod]
public void Add_NegativeNumbers_ReturnsCorrectSum()
{
Calculator calculator = new Calculator();
Assert.AreEqual(-8, calculator.Add(-5, -3));
}

// Bad: Testing multiple behaviors in one test
[TestMethod]
public void Calculator_Operations_Work()
{
Calculator calculator = new Calculator();
Assert.AreEqual(8, calculator.Add(5, 3));
Assert.AreEqual(2, calculator.Subtract(5, 3));
// More assertions...
}

2. Use Descriptive Test Names

Name your tests clearly to indicate:

  • What's being tested
  • Under what circumstances
  • What the expected outcome is
csharp
// Good name
public void Subtract_WhenSecondNumberIsLarger_ReturnsNegativeNumber()

// Poor name
public void SubtractTest()

3. Arrange-Act-Assert Pattern

Structure tests with clear sections:

csharp
[TestMethod]
public void Divide_NonZeroDenominator_ReturnsCorrectResult()
{
// Arrange
Calculator calculator = new Calculator();

// Act
double result = calculator.Divide(10, 2);

// Assert
Assert.AreEqual(5, result);
}

4. Test Edge Cases

Don't just test the happy path; consider boundaries and special cases:

csharp
[TestMethod]
[ExpectedException(typeof(DivideByZeroException))]
public void Divide_ByZero_ThrowsException()
{
Calculator calculator = new Calculator();
calculator.Divide(10, 0);
}

Mocking Dependencies

Real-world code often has dependencies. For true unit tests, we should isolate the code under test using mocks.

Using Moq Framework

First, install the Moq NuGet package.

Let's test a service that depends on a repository:

csharp
public interface IProductRepository
{
Product GetById(int id);
}

public class ProductService
{
private readonly IProductRepository _repository;

public ProductService(IProductRepository repository)
{
_repository = repository;
}

public string GetProductName(int id)
{
var product = _repository.GetById(id);
return product?.Name ?? "Not found";
}
}

Here's how to test it with Moq:

csharp
[TestMethod]
public void GetProductName_ExistingProduct_ReturnsName()
{
// Arrange
var mockRepo = new Mock<IProductRepository>();
mockRepo.Setup(repo => repo.GetById(1))
.Returns(new Product { Id = 1, Name = "Test Product" });

var service = new ProductService(mockRepo.Object);

// Act
var result = service.GetProductName(1);

// Assert
Assert.AreEqual("Test Product", result);
}

[TestMethod]
public void GetProductName_NonExistingProduct_ReturnsNotFound()
{
// Arrange
var mockRepo = new Mock<IProductRepository>();
mockRepo.Setup(repo => repo.GetById(It.IsAny<int>()))
.Returns((Product)null);

var service = new ProductService(mockRepo.Object);

// Act
var result = service.GetProductName(999);

// Assert
Assert.AreEqual("Not found", result);
}

Real-World Example: Testing an ASP.NET Core Controller

Let's create and test a simple API controller:

csharp
public class TodoItem
{
public int Id { get; set; }
public string Title { get; set; }
public bool IsComplete { get; set; }
}

public interface ITodoRepository
{
IEnumerable<TodoItem> GetAll();
TodoItem GetById(int id);
void Add(TodoItem item);
}

[ApiController]
[Route("api/[controller]")]
public class TodoController : ControllerBase
{
private readonly ITodoRepository _repository;

public TodoController(ITodoRepository repository)
{
_repository = repository;
}

[HttpGet]
public ActionResult<IEnumerable<TodoItem>> GetAll()
{
return Ok(_repository.GetAll());
}

[HttpGet("{id}")]
public ActionResult<TodoItem> GetById(int id)
{
var item = _repository.GetById(id);

if (item == null)
{
return NotFound();
}

return item;
}
}

Now let's test this controller:

csharp
[TestClass]
public class TodoControllerTests
{
[TestMethod]
public void GetAll_ReturnsOkResult_WithAllItems()
{
// Arrange
var mockRepo = new Mock<ITodoRepository>();
var testItems = new List<TodoItem>
{
new TodoItem { Id = 1, Title = "Item 1", IsComplete = false },
new TodoItem { Id = 2, Title = "Item 2", IsComplete = true }
};

mockRepo.Setup(repo => repo.GetAll())
.Returns(testItems);

var controller = new TodoController(mockRepo.Object);

// Act
var result = controller.GetAll();

// Assert
var okResult = result.Result as OkObjectResult;
Assert.IsNotNull(okResult);

var returnedItems = okResult.Value as IEnumerable<TodoItem>;
Assert.IsNotNull(returnedItems);
Assert.AreEqual(2, returnedItems.Count());
}

[TestMethod]
public void GetById_ExistingId_ReturnsOkResult()
{
// Arrange
var testItem = new TodoItem { Id = 1, Title = "Test Item", IsComplete = false };

var mockRepo = new Mock<ITodoRepository>();
mockRepo.Setup(repo => repo.GetById(1))
.Returns(testItem);

var controller = new TodoController(mockRepo.Object);

// Act
var result = controller.GetById(1);

// Assert
var actionResult = result.Result;
Assert.IsNull(actionResult); // ActionResult will be null because the item is returned directly

var returnedItem = result.Value;
Assert.IsNotNull(returnedItem);
Assert.AreEqual(1, returnedItem.Id);
Assert.AreEqual("Test Item", returnedItem.Title);
}

[TestMethod]
public void GetById_NonExistingId_ReturnsNotFound()
{
// Arrange
var mockRepo = new Mock<ITodoRepository>();
mockRepo.Setup(repo => repo.GetById(999))
.Returns((TodoItem)null);

var controller = new TodoController(mockRepo.Object);

// Act
var result = controller.GetById(999);

// Assert
var actionResult = result.Result;
Assert.IsInstanceOfType(actionResult, typeof(NotFoundResult));
}
}

Test-Driven Development (TDD)

Unit testing is closely related to Test-Driven Development, a methodology that follows this cycle:

  1. Red: Write a failing test for the functionality you want to implement
  2. Green: Write the simplest code that makes the test pass
  3. Refactor: Clean up the code while keeping the tests passing

Let's demonstrate TDD by building a string utility class:

Step 1: Write a failing test

csharp
[TestMethod]
public void ReverseString_NonEmptyString_ReturnsReversedString()
{
// Arrange
StringUtils utils = new StringUtils();

// Act
string result = utils.ReverseString("hello");

// Assert
Assert.AreEqual("olleh", result);
}

Step 2: Write minimal implementation to pass

csharp
public class StringUtils
{
public string ReverseString(string input)
{
char[] chars = input.ToCharArray();
Array.Reverse(chars);
return new string(chars);
}
}

Step 3: Add more tests and refine

csharp
[TestMethod]
public void ReverseString_EmptyString_ReturnsEmptyString()
{
StringUtils utils = new StringUtils();
Assert.AreEqual("", utils.ReverseString(""));
}

[TestMethod]
public void ReverseString_NullInput_ThrowsArgumentNullException()
{
StringUtils utils = new StringUtils();
Assert.ThrowsException<ArgumentNullException>(() => utils.ReverseString(null));
}

Step 4: Improve the implementation

csharp
public string ReverseString(string input)
{
if (input == null)
throw new ArgumentNullException(nameof(input));

if (input.Length <= 1)
return input;

char[] chars = input.ToCharArray();
Array.Reverse(chars);
return new string(chars);
}

Code Coverage and Quality Metrics

Code coverage tools help you understand how much of your code is tested:

  1. Enable code coverage in Visual Studio:

    • Test > Analyze Code Coverage > All Tests
  2. View the results to identify untested code sections

  3. Aim for high coverage, but remember that 100% coverage doesn't guarantee perfect tests.

Summary

Unit testing is an essential skill for .NET developers that leads to higher quality, more maintainable code. In this guide, we've covered:

  • The fundamentals of unit testing in .NET
  • Setting up and using MSTest and xUnit frameworks
  • Writing effective unit tests with clear structure and assertions
  • Mocking dependencies to isolate code under test
  • Real-world examples with ASP.NET Core
  • Test-Driven Development approach
  • Measuring code coverage

By incorporating these practices into your development workflow, you'll build more reliable applications and catch issues earlier in the development process.

Additional Resources and Exercises

Resources:

Exercises:

  1. Create a string utility class with methods for common operations (reverse, capitalize, truncate) and write tests for each method.
  2. Build a simple calculator with advanced operations (square root, power, etc.) using TDD.
  3. Create a web API controller for a bookstore with CRUD operations and write comprehensive tests.
  4. Refactor an existing untested code by adding appropriate tests.
  5. Try to achieve at least 80% code coverage on a small project.

By consistently practicing these concepts, you'll become proficient in unit testing and improve the overall quality of your .NET applications.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)