ZetCode

C# MSTest

最后修改于 2023 年 7 月 5 日

C# MSTest 教程展示了如何使用 MSTest 框架在 C# 中进行单元测试。

单元测试是一种软件测试,用于测试软件的各个单元(组件)。 单元测试的目的是验证软件的每个单元是否按设计运行。 单元是任何软件中最小的可测试部分。

MSTest 是 Microsoft 提供的一个单元测试库。 它适用于所有 .NET 语言。 还有其他单元测试库,包括 XUnit 和 NUnit。

我们可以将测试放在同一个项目目录中,也可以放在不同的目录中。 我们从一个更简单的选项开始,将测试放在同一个项目目录中。 最后,我们将测试放在解决方案中的不同目录中。

$ dotnet add package Microsoft.NET.Test.Sdk
$ dotnet add package MSTest.TestAdapter
$ dotnet add package MSTest.TestFramework

为了使用 MSTest,我们需要添加这三个库。

C# MSTest 简单示例

我们从一个简单的示例开始。

Arith.cs
namespace Messages.Services;

public class Messages
{
    public static Func<string> msg1 = () => "Hello there!";
    public static Func<string> msg2 = () => "Good Morning!";
}

我们测试简单的消息函数。

我们将测试放入 tests 目录中。 MSTest 会自动发现我们的测试。

tests/MessageTest.cs
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);
    }
}

该类用 [TestClass] 属性进行注释,测试方法用 [TestMethod] 属性进行注释。 我们使用断言来确保正确的输出。

$ 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 参数化测试

[DataTestMethod] 属性指示一个参数化方法。 参数使用 [DataRow] 属性添加。

Arith.cs
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;
}

我们将要测试简单的算术函数。

tests/ArithTest.cs
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);
    }
}

在此示例中,我们使用三组值来测试每个方法。

[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);
}

我们正在测试 Add 方法。 该方法使用 [DataRow] 属性给出的三组值进行测试。 计算值和 expected 值与 Assert.AreEqual 断言进行比较。

C# MSTest 跳过测试

可以使用 [Ignore] 属性跳过测试方法。

tests/ArithTest.cs
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);
    }
}

我们有四个测试方法。 其中两个使用 [Ignore] 属性跳过。

$ 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

使用 [DynamicData] 属性,我们可以将测试数据外部化到一个方法或属性中。

tests/ArithTest.cs
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 }
        };
    }
}

在此示例中,我们在单独的方法中具有测试数据。

将测试放在单独的目录中

在以下示例中,我们将展示如何将测试放在单独的目录中。

$ mkdir Separate
$ cd Separate

我们创建一个新目录。

$ dotnet new sln

我们创建一个新的空解决方案。

$ mkdir PalindromeService PalindromeService.Tests

创建了两个目录。

$ cd PalindromeService
$ dotnet new classlib

我们创建一个新的库。

PalindromeService\PalindromeService.cs
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;
    }
}

PalindromeService 包含 IsPalindrome 方法,该方法确定一个单词是否是回文。

$ cd .. 
$ dotnet sln add PalindromeService\PalindromeService.csproj

我们将 PalindromeService 添加到解决方案。

$ cd PalindromeService.Tests
$ dotnet new mstest
$ dotnet add reference ..\PalindromeService\PalindromeService.csproj

我们转到 PalindromeService.Tests 目录,并添加 unit 库,并添加对 PalindromeService 的引用。

PalindromeService.Tests\PalindromeService.Tests.csproj
<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>

这是项目文件的外观。

PalindromeService.Tests\Tests.cs
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);
    }
}

我们使用三个单词测试 IsPalindrome 方法。

[TestInitialize]
public void SetUp()
{
    _palindromeService = new PalindromeService();
}

[TestInitialize] 属性用于提供一组通用的函数,这些函数在每个测试方法调用之前执行。 在我们的例子中,我们创建了 PalindromeService

$ cd ..
$ dotnet sln add PalindromeService.Tests\PalindromeService.Tests.csproj

我们将测试项目添加到解决方案。

$ dotnet test

最后,我们可以运行测试。

来源

使用 MSTest 和 .NET 进行 C# 单元测试

在本文中,我们使用 MSTest 库在 C# 中完成了单元测试。

作者

我叫 Jan Bodnar,我是一位充满激情的程序员,拥有丰富的编程经验。 自 2007 年以来,我一直在撰写编程文章。 迄今为止,我已经撰写了 1,400 多篇文章和 8 本电子书。 我拥有超过十年的编程教学经验。

列出所有 C# 教程