C# MSTest
last modified July 5, 2023
C# MSTest tutorial shows how to do unit testing in C# with MSTest framework.
Unit testing is a software testing where individual units (components) of a 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.
MSTest is a unit-testing library from Microsoft. It is available for all .NET languages. There are other unit-testing libraries including XUnit and NUnit.
We can either place tests in the same project directory or inside a different directory. We start with a simpler option and place tests in the same project directory. In the end, we place tests in a different directory within a solution.
$ dotnet add package Microsoft.NET.Test.Sdk $ dotnet add package MSTest.TestAdapter $ dotnet add package MSTest.TestFramework
In order to use MSTest, we need to add these three libraries.
C# MSTest simple example
We start with a simple example.
namespace Messages.Services; public class Messages { public static Func<string> msg1 = () => "Hello there!"; public static Func<string> msg2 = () => "Good Morning!"; }
We test simple message functions.
We put our tests into the tests directory. MSTest automatically discovers our tests.
namespace Messages.Tests; using Microsoft.VisualStudio.TestTools.UnitTesting; using Messages.Services; [TestClass] public class MessageTest { private const string Expected1 = "Hello there!"; private const string Expected2 = "Good Morning!"; [TestMethod] public void Message1() { var m1 = Messages.msg1(); Assert.AreEqual(Expected1, m1); } [TestMethod] public void Message2() { var m2 = Messages.msg2(); Assert.AreEqual(Expected2, m2); } }
The class is annotated with the [TestClass]
attribute, the test
methods are annotated with the [TestMethod]
attribute. We use
assertions to ensure the correct output.
$ dotnet test ... Starting test execution, please wait... A total of 1 test files matched the specified pattern. Passed! - Failed: 0, Passed: 2, Skipped: 0, Total: 2, ...
C# MSTest parameterized tests
The [DataTestMethod]
attribute indicates a parameterized method.
The parameters are added with the [DataRow]
attribute.
namespace Arithmetic.Services; public class Basic { public static Func<int, int, int> add = (a, b) => a + b; public static Func<int, int, int> mul = (a, b) => a * b; public static Func<int, int, int> sub = (a, b) => a - b; public static Func<int, int, int> div = (a, b) => a / b; }
We are going to test simple arithmetic functions.
namespace Messages.Tests; using Microsoft.VisualStudio.TestTools.UnitTesting; using Arithmetic.Services; [TestClass] public class ArithTest { [DataTestMethod] [DataRow(1, 2, 3)] [DataRow(2, 2, 4)] [DataRow(-1, 4, 3)] public void Add(int x, int y, int expected) { int r = Basic.add(x, y); Assert.AreEqual(r, expected); } [DataTestMethod] [DataRow(1, 2, -1)] [DataRow(2, 2, 0)] [DataRow(3, 2, 1)] public void Sub(int x, int y, int expected) { int r = Basic.sub(x, y); Assert.AreEqual(r, expected); } [DataTestMethod] [DataRow(9, 3, 27)] [DataRow(3, 3, 9)] [DataRow(-3, -3, 9)] public void Mul(int x, int y, int expected) { int r = Basic.mul(x, y); Assert.AreEqual(r, expected); } [DataTestMethod] [DataRow(9, 3, 3)] [DataRow(3, 3, 1)] [DataRow(8, 2, 4)] public void Div(int x, int y, int expected) { int r = Basic.div(x, y); Assert.AreEqual(r, expected); } }
In this example, we test each method with three sets of values.
[DataTestMethod] [DataRow(1, 2, 3)] [DataRow(2, 2, 4)] [DataRow(-1, 4, 3)] public void Add(int x, int y, int expected) { int r = Basic.add(x, y); Assert.AreEqual(r, expected); }
We are testing the Add
method. The method is tested with three
sets of values given by the [DataRow]
attribute. The calculated
and expected
values are compared with Assert.AreEqual
assertion.
C# MSTest skipping tests
Test methods can be skipped with [Ignore]
attribute.
namespace Arithmetic.Tests; using Microsoft.VisualStudio.TestTools.UnitTesting; using Arithmetic.Services; [TestClass] public class ArithTest { [DataRow(1, 2, 3)] [DataRow(2, 2, 4)] [DataRow(-1, 4, 3)] [DataTestMethod] public void Add(int x, int y, int z) { int r = Basic.add(x, y); Assert.AreEqual(r, z); } [DataTestMethod] [DataRow(1, 2, -1)] [DataRow(2, 2, 0)] [DataRow(3, 2, 1)] public void Sub(int x, int y, int z) { int r = Basic.sub(x, y); Assert.AreEqual(r, z); } [DataTestMethod] [DataRow(9, 3, 27)] [DataRow(3, 3, 9)] [DataRow(-3, -3, 9)] [Ignore] public void Mul(int x, int y, int z) { int r = Basic.mul(x, y); Assert.AreEqual(r, z); } [DataTestMethod] [DataRow(9, 3, 3)] [DataRow(3, 3, 1)] [DataRow(8, 2, 4)] [Ignore] public void Div(int x, int y, int z) { int r = Basic.div(x, y); Assert.AreEqual(r, z); } }
We have four test methods. Two of them are skipped using the
[Ignore]
attribute.
$ dotnet test ... Starting test execution, please wait... A total of 1 test files matched the specified pattern. Skipped Mul (9,3,27) Skipped Mul (3,3,9) Skipped Mul (-3,-3,9) Skipped Div (9,3,3) Skipped Div (3,3,1) Skipped Div (8,2,4) Passed! - Failed: 0, Passed: 6, Skipped: 6, Total: 12, Duration: 84 ms ...
C# MSTest DynamicData
With [DynamicData]
attribute, we can externalize the test data into
a method or a property.
namespace Messages.Tests; using Microsoft.VisualStudio.TestTools.UnitTesting; using Arithmetic.Services; [TestClass] public class Tests { [DataTestMethod] [DynamicData(nameof(AddData), DynamicDataSourceType.Method)] public void Add(int x, int y, int expected) { int r = Basic.add(x, y); Assert.AreEqual(r, expected); } [DataTestMethod] [DynamicData(nameof(SubData), DynamicDataSourceType.Method)] public void Sub(int x, int y, int expected) { int r = Basic.sub(x, y); Assert.AreEqual(r, expected); } [DataTestMethod] [DynamicData(nameof(MulData), DynamicDataSourceType.Method)] public void Mul(int x, int y, int expected) { int r = Basic.mul(x, y); Assert.AreEqual(r, expected); } [DataTestMethod] [DynamicData(nameof(DivData), DynamicDataSourceType.Method)] public void Div(int x, int y, int expected) { int r = Basic.div(x, y); Assert.AreEqual(r, expected); } private static IEnumerable<object[]> AddData() { return new[] { new object[] { 1, 2, 3 }, new object[] { 2, 2, 4 }, new object[] { -1, 4, 3 } }; } private static IEnumerable<object[]> SubData() { return new[] { new object[] { 1, 2, -1 }, new object[] { 2, 2, 0 }, new object[] { 3, 2, 1 } }; } private static IEnumerable<object[]> MulData() { return new[] { new object[] { 9, 3, 27 }, new object[] { 3, 3, 9 }, new object[] { -3, -3, 9 } }; } private static IEnumerable<object[]> DivData() { return new[] { new object[] { 9, 3, 3 }, new object[] { 3, 3, 1 }, new object[] { 8, 2, 4 } }; } }
In the example, we have test data in separate methods.
Placing tests in separate directory
In the following example, we show how to place tests in a separate directory.
$ mkdir Separate $ cd Separate
We create a new directory.
$ dotnet new sln
We create a new empty solution.
$ mkdir PalindromeService PalindromeService.Tests
Two directories are created.
$ cd PalindromeService $ dotnet new classlib
We create a new library.
namespace Palindrome.Services; using System.Globalization; public class PalindromeService { public bool IsPalindrome(string word) { IEnumerable<string> GraphemeClusters(string s) { var enumerator = StringInfo.GetTextElementEnumerator(s); while (enumerator.MoveNext()) { yield return (string)enumerator.Current; } } var reversed = string.Join("", GraphemeClusters(word).Reverse().ToArray()); return reversed == word; } }
The PalindromeService
contains the IsPalindrome
method, which determines if a words is a palindrome.
$ cd .. $ dotnet sln add PalindromeService\PalindromeService.csproj
We add the PalindromeService
to the solution.
$ cd PalindromeService.Tests $ dotnet new mstest $ dotnet add reference ..\PalindromeService\PalindromeService.csproj
We go to the PalindromeService.Tests
directory and add the
unit
libraries add the reference to the
PalindromeService
.
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>net6.0</TargetFramework> <Nullable>enable</Nullable> <IsPackable>false</IsPackable> </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" /> <PackageReference Include="MSTest.TestAdapter" Version="2.2.8" /> <PackageReference Include="MSTest.TestFramework" Version="2.2.8" /> </ItemGroup> <ItemGroup> <ProjectReference Include="..\PalindromeService\PalindromeService.csproj" /> </ItemGroup> </Project>
This is how the project file looks like.
namespace Palindrome.Services.Tests; using Microsoft.VisualStudio.TestTools.UnitTesting; [TestClass] public class Tests { private PalindromeService? _palindromeService; [TestInitialize] public void SetUp() { _palindromeService = new PalindromeService(); } [DataTestMethod] [DataRow("racecar")] [DataRow("level")] [DataRow("nun")] public void IsPalindrome(string word) { var r = _palindromeService!.IsPalindrome(word); Assert.AreEqual(r, true); } }
We test the IsPalindrome
method with three words.
[TestInitialize] public void SetUp() { _palindromeService = new PalindromeService(); }
The [TestInitialize]
attribute is used to provide a common set of
functions that are performed just before each test method is called. In our
case, we create the PalindromeService
.
$ cd .. $ dotnet sln add PalindromeService.Tests\PalindromeService.Tests.csproj
We add the test project to the solution.
$ dotnet test
Finally, we can run the tests.
Source
Unit testing C# with MSTest and .NET
In this article we have done unit testing in C# with MSTest library.
Author
List all C# tutorials.