ZetCode

C# MSTest

last modified October 16, 2026

In this article we show how to do unit testing in C# with the MSTest framework.

Unit testing is a software testing method where individual units (components) of software are tested. The purpose of unit testing is to validate that each unit of the software performs as designed. A unit is the smallest testable part of any software, typically a method or a class.

MSTest is a unit-testing framework from Microsoft. It is available for all .NET languages. Other popular unit-testing frameworks are xUnit and NUnit. The examples were tested with MSTest 4.0 and .NET 10.

The tests are placed in a separate test project, which references the project being tested. Both projects belong to one solution.

Creating the projects

We create a solution with a class library and a test project.

$ mkdir Services
$ cd Services
$ dotnet new sln
$ dotnet new classlib -o Services
$ dotnet new mstest -o Services.Tests
$ dotnet sln add Services/Services.csproj Services.Tests/Services.Tests.csproj
$ dotnet add Services.Tests reference Services/Services.csproj

The dotnet new sln command creates a solution file; since .NET 10, it uses the XML-based .slnx format. The mstest template creates a test project. We add both projects to the solution and reference the library from the test project. We delete the generated Class1.cs and Test1.cs files.

Services.Tests/Services.Tests.csproj
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <LangVersion>latest</LangVersion>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="MSTest" Version="4.0.2" />
  </ItemGroup>

  <ItemGroup>
    <Using Include="Microsoft.VisualStudio.TestTools.UnitTesting" />
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\Services\Services.csproj" />
  </ItemGroup>

</Project>

This is the project file of the test project. The MSTest metapackage brings the test framework, the test adapter, and the test SDK. The Using item imports the Microsoft.VisualStudio.TestTools.UnitTesting namespace in all files of the project.

C# MSTest simple example

We start with a simple example. We test the following arithmetic functions.

Services/Basic.cs
namespace Services;

public static class Basic
{
    public static int Add(int a, int b) => a + b;
    public static int Sub(int a, int b) => a - b;
    public static int Mul(int a, int b) => a * b;
    public static int Div(int a, int b) => a / b;
}

The Basic class is part of the Services library.

Services.Tests/BasicTests.cs
namespace Services.Tests;

[TestClass]
public sealed class BasicTests
{
    [TestMethod]
    public void AddTwoNumbers()
    {
        Assert.AreEqual(5, Basic.Add(2, 3));
    }

    [TestMethod]
    public void SubTwoNumbers()
    {
        Assert.AreEqual(-1, Basic.Sub(2, 3));
    }
}

The class is annotated with the [TestClass] attribute, the test methods are annotated with the [TestMethod] attribute. MSTest discovers them automatically.

Assert.AreEqual(5, Basic.Add(2, 3));

We use assertions to ensure the correct output. The first argument of AreEqual is the expected value, the second is the actual value. Keeping this order makes the error messages correct.

$ dotnet test
...
A total of 1 test files matched the specified pattern.

Passed!  - Failed:     0, Passed:     2, Skipped:     0, Total:     2, Duration: 20 ms - Services.Tests.dll (net10.0)

The dotnet test command builds the solution and runs all tests.

C# MSTest parameterized tests

A test method can take parameters. The values are provided with the [DataRow] attribute; the method is executed once for each data row. In older versions of MSTest, such methods were marked with the [DataTestMethod] attribute; it is no longer needed.

Services.Tests/ParamTests.cs
namespace Services.Tests;

[TestClass]
public sealed class ParamTests
{
    [TestMethod]
    [DataRow(1, 2, 3)]
    [DataRow(2, 2, 4)]
    [DataRow(-1, 4, 3)]
    public void Add(int x, int y, int expected)
    {
        Assert.AreEqual(expected, Basic.Add(x, y));
    }

    [TestMethod]
    [DataRow(9, 3, 27)]
    [DataRow(-3, -3, 9)]
    [Ignore("Not ready yet")]
    public void Mul(int x, int y, int expected)
    {
        Assert.AreEqual(expected, Basic.Mul(x, y));
    }

    [TestMethod]
    [DynamicData(nameof(DivData))]
    public void Div(int x, int y, int expected)
    {
        Assert.AreEqual(expected, Basic.Div(x, y));
    }

    public static IEnumerable<object[]> DivData =>
    [
        [9, 3, 3],
        [3, 3, 1],
        [8, 3, 2]
    ];

    [TestMethod]
    public void DivByZero()
    {
        Assert.ThrowsExactly<DivideByZeroException>(() => Basic.Div(5, 0));
    }
}

In this example, we test the arithmetic methods with several sets of values.

[TestMethod]
[DataRow(1, 2, 3)]
[DataRow(2, 2, 4)]
[DataRow(-1, 4, 3)]
public void Add(int x, int y, int expected)

The Add method is tested with three sets of values given by the [DataRow] attributes.

[Ignore("Not ready yet")]

Test methods can be skipped with the [Ignore] attribute. The optional message explains why the test is skipped.

[DynamicData(nameof(DivData))]

With the [DynamicData] attribute, the test data is provided by a static property or method. This is useful when the data is computed or too large for attributes.

Assert.ThrowsExactly<DivideByZeroException>(() => Basic.Div(5, 0));

The Assert.ThrowsExactly method verifies that the code throws an exception of the given type. The Assert.Throws method also accepts derived exception types. The older [ExpectedException] attribute was removed in MSTest 4.

$ dotnet test
...
A total of 1 test files matched the specified pattern.
  Skipped Mul (9,3,27)
  Skipped Mul (-3,-3,9)

Passed!  - Failed:     0, Passed:    14, Skipped:     2, Total:    16, Duration: 52 ms - Services.Tests.dll (net10.0)

Each data row counts as a separate test. The two rows of the Mul test are skipped. The total also includes the palindrome tests from the next section.

C# MSTest test initialization

The [TestInitialize] attribute marks a method that runs before each test method of the class. It is used to prepare the objects that the tests need. The [TestCleanup] attribute marks a method that runs after each test.

Services/TextTools.cs
using System.Globalization;

namespace Services;

public class TextTools
{
    public bool IsPalindrome(string word)
    {
        var elements = new List<string>();
        var enumerator = StringInfo.GetTextElementEnumerator(word.ToLower());

        while (enumerator.MoveNext())
        {
            elements.Add(enumerator.GetTextElement());
        }

        return elements.SequenceEqual(Enumerable.Reverse(elements));
    }
}

The IsPalindrome method determines if a word is a palindrome. It works with text elements (grapheme clusters) rather than with char values, so that it also handles accented letters composed of several characters correctly.

Services.Tests/TextToolsTests.cs
namespace Services.Tests;

[TestClass]
public sealed class TextToolsTests
{
    private TextTools tools = null!;

    [TestInitialize]
    public void SetUp()
    {
        tools = new TextTools();
    }

    [TestMethod]
    [DataRow("racecar")]
    [DataRow("Level")]
    [DataRow("nun")]
    public void IsPalindrome(string word)
    {
        Assert.IsTrue(tools.IsPalindrome(word));
    }

    [TestMethod]
    [DataRow("falcon")]
    [DataRow("sky")]
    public void IsNotPalindrome(string word)
    {
        Assert.IsFalse(tools.IsPalindrome(word));
    }
}

We test the IsPalindrome method with palindromes and non-palindromes.

[TestInitialize]
public void SetUp()
{
    tools = new TextTools();
}

A new TextTools object is created before each test. Each test thus starts with a fresh object and the tests do not influence each other. The null! initializer tells the compiler that the field is assigned before it is used.

With the normal verbosity of the console logger, we can see the result of each test:

$ dotnet test --logger "console;verbosity=normal"
...
  Passed IsPalindrome ("nun") [6 ms]
  Passed IsNotPalindrome ("falcon") [6 ms]
  Passed IsPalindrome ("racecar") [6 ms]
  Passed IsPalindrome ("Level") [6 ms]
  Passed IsNotPalindrome ("sky") [5 ms]
...

The tests run in parallel; the generated MSTestSettings.cs file enables parallel execution at the method level. Therefore, the order of the results may vary.

C# MSTest failing test

If we change the last data row of the Div test to [8, 3, 3], the test fails. Integer division of 8 by 3 gives 2.

$ dotnet test
...
  Failed Div (8,3,3) [8 ms]
  Error Message:
   Assert.AreEqual failed. Expected:<3>. Actual:<2>. 'expected' expression: 'expected', 'actual' expression: 'Basic.Div(x, y)'.
  Stack Trace:
...
Failed!  - Failed:     1, Passed:    13, Skipped:     2, Total:    16, Duration: 56 ms - Services.Tests.dll (net10.0)

The error message shows the expected and the actual values and the expressions that produced them.

Source

Unit testing C# with MSTest and .NET

In this article we have done unit testing in C# with the MSTest framework.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all C# tutorials.