Unit Testing Introduction

Vaibhav • September 10, 2025

As your C# programs grow in complexity, ensuring correctness becomes critical. Manual testing works for small scripts, but it quickly becomes error-prone and inefficient. That’s where unit testing comes in - a systematic way to verify that individual pieces of your code behave as expected. In this article, we’ll introduce the concept of unit testing, explain why it matters, and walk through writing your first tests using xUnit, a popular testing framework for .NET.

What is Unit Testing?

Unit testing is the practice of writing small, automated tests that check the behavior of a single “unit” of code - typically a method or function. Each test runs independently and verifies that the unit produces the correct output for a given input. If the code changes and breaks the expected behavior, the test fails, alerting you immediately.

A “unit” in unit testing usually refers to a method or function. The goal is to isolate and test it without relying on external systems like databases, files, or network calls.

Why Unit Testing Matters

Unit tests act as a safety net. They catch bugs early, make refactoring safer, and serve as living documentation for your code’s behavior. When written well, they reduce the time spent debugging and increase confidence in your codebase.

Write tests before or immediately after writing the code. This helps clarify the expected behavior and ensures your implementation meets it.

Setting Up xUnit in a C# Project

xUnit is a lightweight, extensible testing framework for .NET. To use it, you’ll typically create a separate test project in your solution. Here’s how to set it up:

// Create a new test project
dotnet new xunit -n MyApp.Tests
// Add reference to the main project
dotnet add MyApp.Tests reference ../MyApp/MyApp.csproj
// Run tests
dotnet test

This creates a test project with xUnit preconfigured. You can now start writing test methods inside test classes.

Writing Your First Unit Test

Let’s say you have a method that calculates the square of a number. Here’s how you’d write a test for it:

// Code under test
public class MathHelper
{
    public int Square(int x)
    {
        return x * x;
    }
}

// Test class
public class MathHelperTests
{
    [Fact]
    public void Square_ReturnsCorrectResult()
    {
        var helper = new MathHelper();
        int result = helper.Square(4);
        Assert.Equal(16, result);
    }
}

Explanation: The test method Square_ReturnsCorrectResult creates an instance of MathHelper, calls the Square method with input 4, and asserts that the result is 16. If the method returns anything else, the test fails.

Understanding Assertions

Assertions are the heart of unit tests. They check whether the actual result matches the expected result. xUnit provides several assertion methods:

Assert.Equal(expected, actual);       // Checks equality
Assert.True(condition);              // Checks if condition is true
Assert.False(condition);             // Checks if condition is false
Assert.Null(object);                 // Checks if object is null
Assert.NotNull(object);              // Checks if object is not null

Use the most specific assertion available - it makes failures easier to diagnose.

Test Naming and Structure

Good test names describe the behavior being tested. A common pattern is:

[Fact]
public void MethodName_Condition_ExpectedResult()
{
    // Arrange
    // Act
    // Assert
}

This structure - Arrange, Act, Assert - keeps tests readable and consistent. Arrange sets up the context, Act performs the operation, and Assert checks the result.

Testing Edge Cases

A robust test suite covers not just typical inputs but also edge cases. For example, testing zero, negative numbers, or very large inputs:

[Fact]
public void Square_Zero_ReturnsZero()
{
    var helper = new MathHelper();
    int result = helper.Square(0);
    Assert.Equal(0, result);
}

[Fact]
public void Square_Negative_ReturnsPositive()
{
    var helper = new MathHelper();
    int result = helper.Square(-3);
    Assert.Equal(9, result);
}

These tests ensure your method behaves correctly across a range of inputs.

Parameterized Tests with [Theory]

xUnit supports parameterized tests using [Theory] and [InlineData]. This lets you test multiple inputs with a single method:

[Theory]
[InlineData(2, 4)]
[InlineData(-2, 4)]
[InlineData(0, 0)]
public void Square_ValidInputs_ReturnsExpected(int input, int expected)
{
    var helper = new MathHelper();
    int result = helper.Square(input);
    Assert.Equal(expected, result);
}

This reduces duplication and makes your tests more maintainable.

Testing for Exceptions

Sometimes you want to verify that a method throws an exception for invalid input. xUnit provides Assert.Throws:

public class Divider
{
    public int Divide(int a, int b)
    {
        if (b == 0)
            throw new ArgumentException("Denominator cannot be zero.");
        return a / b;
    }
}

public class DividerTests
{
    [Fact]
    public void Divide_ByZero_ThrowsArgumentException()
    {
        var divider = new Divider();
        Assert.Throws(() => divider.Divide(10, 0));
    }
}

This test ensures that invalid input is handled correctly and that the method fails safely.

Test Independence and Isolation

Each test should be independent - it should not rely on the outcome or side effects of other tests. Avoid shared state, and reset any mutable data before each test.

xUnit creates a new instance of the test class for each test method, which helps maintain isolation.

Running and Interpreting Test Results

When you run dotnet test, xUnit executes all test methods and reports results. A green check means the test passed; a red cross means it failed. The output includes the name of the failing test and the reason.

Summary

Unit testing is a foundational skill for writing reliable, maintainable C# code. By testing individual methods in isolation, you catch bugs early, document expected behavior, and make future changes safer. In this article, you learned how to set up xUnit, write basic and parameterized tests, assert results, and handle exceptions. As you continue building applications, unit tests will become your first line of defense against regressions and unexpected behavior.

In the next article, we’ll explore Code Quality Tools - static analyzers, linters, and metrics that complement unit testing by catching issues before runtime.