Skip to main content

Unit Testing (Examples in C# with MSTest)

Introduction

Unit tests are programs that automatically verify that each part of your code works as expected.

This guide presents the fundamental concepts of unit testing, illustrated with MSTest, a testing framework for C#. However, the principles remain applicable to all languages (see end of page).

Learning objectives:

  • Understand why and how to write unit tests
  • Master the AAA (Arrange, Act, Assert) pattern
  • Configure an MSTest test project
  • Write robust and maintainable tests

Benefits of unit testing:

  • Quality: Code coverage is the percentage of code tested and ensures that the code works as expected and that future changes do not break existing features. Having a high coverage rate is a sign of code quality and robustness.
  • Quick bug detection: Unit tests allow you to quickly detect errors in the code, reducing debugging time.
  • Living documentation: Unit tests serve as living documentation for the code, showing how each part is supposed to work.
  • Easier code modification: Unit tests allow you to modify code with confidence, as they can ensure that changes haven't broken anything.

Prerequisites & Installation

Prior knowledge

  • Basic knowledge of C# (or another programming language)
  • Object-oriented programming concepts

Required tools

ToolVersionDescription
.NET SDK6.0+C# development framework
IDE-Visual Studio, VS Code or JetBrains Rider

Creating and configuring an MSTest test project

Prerequisites: have an existing C# project

If you don't have a project yet, create one:

mkdir MyProjectSolution # Create a folder for the project if needed
cd MyProjectSolution # Go into the project folder

dotnet new sln -n MyProjectSolution # Create a .sln solution file

dotnet new console -o src/MyProject # Create a console project in src/MyProject

dotnet sln add src/MyProject/MyProject.csproj # Add the project to the solution

Resulting architecture:

MyProjectSolution/
├── MyProjectSolution.sln
└── src/
└── MyProject/
├── MyProject.csproj
└── Program.cs

Configure the test project

In your solution folder:

dotnet new mstest -o tests/MyProject.Tests
dotnet sln add tests/MyProject.Tests/MyProject.Tests.csproj
dotnet add tests/MyProject.Tests reference src/MyProject/MyProject.csproj

This creates a test project named MyProject.Tests in the tests folder, then links the test project to the solution and the main project.

Final structure:

MyProjectSolution/
├── MyProjectSolution.sln
├── src/
│ └── MyProject/
│ ├── MyProject.csproj
│ ├── MyClass.cs
│ └── Program.cs
└── tests/
└── MyProject.Tests/
├── MyProject.Tests.csproj
├── MyClassTests.cs
└── MSTestSettings.cs
tip

Organize test files following the structure of the main project (e.g., a file MyClassTests.cs for MyClass.cs).

For more details: Microsoft docs - Create an MSTest test project

Structure of a unit test (AAA pattern)

All unit tests follow the AAA pattern:

  • Arrange: Prepare the data and objects needed to test the method
  • Act: Call the method to test
  • Assert: Verify the result obtained

Realistic example with MSTest

Suppose a class that contains methods to calculate VAT and the total price (including VAT) for a given amount:

namespace MyNamespace
{
public static class Calculator
{
public static decimal CalculateVAT(decimal amount, decimal rate)
{
if (rate < 0)
{
throw new ArgumentException("The rate cannot be negative.");
}
return amount * rate;
}

public static decimal CalculateTotalPrice(decimal amount, decimal rate)
{
return amount + CalculateVAT(amount, rate);
}
}
}

Associated unit tests:

using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace MyNamespace.Tests
{
[TestClass]
public class CalculatorTests
{
[TestMethod]
public void CalculateVAT_WithValidAmountAndRate_ReturnsExpectedResult()
{
// Arrange
decimal amount = 100;
decimal rate = 0.2m;

// Act
decimal result = Calculator.CalculateVAT(amount, rate);

// Assert
Assert.AreEqual(20, result, "The VAT should be correct.");
}

[TestMethod]
public void CalculateTotalPrice_WithValidAmountAndRate_ReturnsCompletePrice()
{
// Arrange
decimal amount = 100;
decimal rate = 0.2m;

// Act
decimal price = Calculator.CalculateTotalPrice(amount, rate);

// Assert
Assert.AreEqual(120, price, "The total price should be the amount plus VAT.");
}
}
}

Writing unit tests with MSTest

The Assert class

Assert is used to verify expected results in your tests.

Most common methods:

  • Assert.AreEqual(expected, actual, message): Checks that two values are equal
  • Assert.AreNotEqual(expected, actual, message): Checks that two values are not equal
  • Assert.IsTrue(condition, message): Checks that a condition is true
  • Assert.IsFalse(condition, message): Checks that a condition is false
  • Assert.IsNull(object, message): Checks that an object is null
  • Assert.IsNotNull(object, message): Checks that an object is not null
tip

If a test fails, the (optional) message is displayed, which saves time.

Parameterized tests

To test a method with several sets of data, use [DataTestMethod] and [DataRow]:

[DataTestMethod]
[DataRow(100, 0.2, 20)]
[DataRow(50, 0.1, 5)]
[DataRow(200, 0.15, 30)]
public void CalculateVAT_WithVariousValues_ReturnsExpectedResult(
decimal amount, decimal rate, decimal expected)
{
Assert.AreEqual(expected, Calculator.CalculateVAT(amount, rate));
}

Exception handling

To test that a method throws an expected exception:

Option 1: With [ExpectedException]

[TestMethod]
[ExpectedException(typeof(ArgumentException))]
public void CalculateVAT_NegativeRate_ThrowsException()
{
Calculator.CalculateVAT(100, -0.2m);
}

Option 2: With Assert.ThrowsException (recommended)

[TestMethod]
public void CalculateVAT_NegativeRate_ThrowsException()
{
var exception = Assert.ThrowsException<ArgumentException>(
() => Calculator.CalculateVAT(100, -0.2m)
);
Assert.AreEqual("The rate cannot be negative.", exception.Message);
}

Running tests

How you run tests depends on your development environment:

  • Visual Studio: "Test" > "Test Explorer", Ctrl+E T, or right-click on the test project
  • Visual Studio Code: "Testing" tab or right-click on the test project
  • JetBrains Rider: "Tests" tab and "Test Coverage"
  • Terminal: dotnet test

Best practices

  • Segment: One test = one test method
  • Explicit naming: Indicate what is being tested and the expected result. Two naming conventions are common:
    • Method_Should...: Method_ShouldReturnExpectedResult
    • Given..._When..._Then...: Method_Condition_ExpectedResult
  • Isolation: Tests should not depend on each other
  • Limit side effects: Clean up resources if needed ([TestCleanup], [TestInitialize], [AssemblyInitialize], [ClassInitialize], etc.)

Setup/cleanup example:

private List<string> _list;

[TestInitialize]
public void Setup()
{
// Common arrange for all tests: initialization
_list = new List<string> { "A", "B" };
}

[TestCleanup]
public void Cleanup()
{
// Cleanup after each test
_list.Clear();
}

[TestMethod]
public void List_ShouldContainA()
{
// Act
bool containsA = _list.Contains("A");

// Assert
Assert.IsTrue(containsA);
}

Common mistakes

  • Forgetting [TestMethod] or [TestClass]: the test is not detected
  • Forgetting to build before testing (dotnet build)
  • Testing several behaviors in the same test
  • Dependency between tests (e.g., modifying a static variable)
  • Incorrect use of Assert (e.g., swapping expected/actual)
  • Not cleaning up resources (files, connections, etc.)
  • Not testing edge cases or exceptions
  • Leaving dead/unused code in tests
  • Not running tests regularly

Going further

Async tests

[TestMethod]
public async Task CalculAsync_ShouldReturnResult()
{
var result = await CalculatorAsync.CalculateAsync(10, 2);
Assert.AreEqual(20, result);
}

Mocks

To isolate dependencies:

  • Moq - Popular mocking framework
  • NSubstitute - Simple and elegant alternative

Configure MSTest

Parallelization, global timeout, etc. by editing the MSTestSettings.cs file. See the official MSTest documentation.

Unit testing in other languages

The principles of unit testing are universal. Here are some popular frameworks:

LanguageFrameworks
Pythonpytest, unittest
JavaJUnit
JavaScript/TypeScriptJest, Mocha
Gotesting
RustBuilt-in tests
PHPPHPUnit
RubyRSpec

Each language has its specifics, but the AAA logic and philosophy remain the same.

Resources

Other examples

using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace LivInParis.Tests
{
[TestClass]
public class NodeTests
{
[TestMethod]
public void Constructor_ShouldCreateNodeWithUniqueName()
{
// Arrange
var nodeName = "TestNode";

// Act
var node = new Node(nodeName);

// Assert
Assert.AreEqual(nodeName, node.Name);
Assert.AreNotEqual(-1, node.Id);
}
}
}
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace ExampleNamespace.Tests
{
[TestClass]
public class ExampleTests
{
[TestMethod]
public void Addition_ShouldReturnCorrectSum()
{
// Arrange
int a = 2;
int b = 3;

// Act
int result = Sum(a, b);

// Assert
Assert.AreEqual(5, result, "The sum should be 5");
}
}
}
namespace LivInParis.Tests
{
[TestClass]
public class NodeTests
{
[TestMethod]
public void Equals_ShouldReturnTrueForSameId()
{
// Arrange
var node1 = new Node("EqualsNode1");
var node2 = new Node("EqualsNode2");

// Act
bool areEqual = node1 == node1;
bool areNotEqual = node1 == node2;

// Assert
Assert.IsTrue(areEqual);
Assert.IsFalse(areNotEqual);
}
}
}
namespace Boggle.Tests;

[TestClass]
public class PlayerTests
{
[TestMethod]
[DataRow("J2", "en")]
[DataRow("J3", "fr")]
public void Name_ShouldReturnCorrectName(string name, string language)
{
// Arrange
Language.Initialize(language);
Player player = new Player(name);

// Act
string playerName = player.Name;

// Assert
Assert.AreEqual(name, playerName);
}
}