C#单元测试Nunit常用注解及代码示例 – 三郎君的日常

面试 · 2023年6月12日 0

C#单元测试Nunit常用注解及代码示例

C#单元测试Nunit常用注解及代码示例

NUnit提供了多个注解(Attributes)用于标记测试方法和测试类,以便进行各种配置和扩展。以下是一些常用的NUnit注解及其作用和示例代码,整理出来方便学习和使用。

1.[TestFixture]:标记一个测试类

它指示该类包含测试方法,并可以执行相关的设置和清理操作。

[TestFixture]
public class MathTests
{
    // 测试方法和其他注解
}

2.[Test]:标记一个测试方法

它表示一个单独的测试用例。

[Test]
public void Add_TwoPositiveNumbers_ReturnsCorrectSum()
{
    // 测试方法的代码和断言
}

3.[SetUp]:测试执行之前

标记一个方法,在每个测试方法之前执行准备操作。它可以用于设置共享的测试环境。

[SetUp]
public void Setup()
{
    // 在每个测试方法运行之前执行的设置操作
}

4.[TearDown]:测试执行之后

标记一个方法,在每个测试方法之后执行清理操作。它可以用于清理测试过程中产生的资源或状态。

[TearDown]
public void TearDown()
{
    // 在每个测试方法运行之后执行的清理操作
}

5.[TestCase]:测试用例

标记一个测试方法,指定参数化的测试用例。它允许传递不同的参数进行多次测试。

[Test]
[TestCase(5, 2, 3)]
[TestCase(10, 5, 5)]
public void Subtract_TwoNumbers_ReturnsCorrectDifference(int a, int b, int expectedDifference)
{
    // 测试方法的代码和断言
}

6.[Category]:标记过滤

标记一个测试方法或测试类

指定测试的分类标签。它可以用于过滤测试的运行。

[Test]
[Category("Math")]
public void Add_TwoPositiveNumbers_ReturnsCorrectSum()
{
    // 测试方法的代码和断言
}

7.[Ignore]:标记忽略

标记一个测试方法或测试类,

表示该测试被忽略,不会被执行。它可以用于临时禁用某些测试。

[Test]
[Ignore("Temporarily disabled")]
public void Add_TwoPositiveNumbers_ReturnsCorrectSum()
{
    // 测试方法的代码和断言
}

8.[MaxTime]:标记最大时间

标记一个测试方法,设置最大执行时间。如果测试方法的执行时间超过指定时间,则测试失败。

[Test]
[MaxTime(1000)]
public void Multiply_TwoNumbers_ReturnsCorrectProduct()
{
    // 测试方法的代码和断言
}

9.[Retry]:标记重试次数

标记一个测试方法,设置重试次数。如果测试失败,则会自动重试指定次数。

[Test]
[Retry(3)]
public void Divide_TwoNumbers_ReturnsCorrectQuotient()
{
    // 测试方法的代码和断言
}

10. [TestCaseSource]:指定测试数据源

标记一个测试方法,指定使用提供的数据源进行参数化测试。可以使用不同的数据源来动态生成测试用例。

[Test]
[TestCaseSource(nameof(GetTestData))]
public void Multiply_TwoNumbers_ReturnsCorrectProduct(int a, int b, int expectedProduct)
{
    // 测试方法的代码和断言
}

private static IEnumerable<TestCaseData> GetTestData()
{
    yield return new TestCaseData(2, 3, 6);
    yield return new TestCaseData(5, 5, 25);
}

11. [Timeout]:标记超时

标记一个测试方法,设置执行超时时间。如果测试方法的执行时间超过指定时间,则测试失败。

[Test]
[Timeout(1000)]
public void LongRunningTest()
{
    // 长时间运行的测试方法的代码和断言
}

12.[Parallelizable]:标记并行

标记一个测试类,指示该类的测试方法可以并行运行。可以提高测试的执行速度。

[TestFixture]
[Parallelizable]
public class MathTests
{
    // 测试方法和其他注解
}

13.[Sequential]:标记顺序

标记一个测试类,指示该类的测试方法按顺序执行。默认情况下,NUnit会并行执行测试方法,使用该注解可以强制按顺序执行。

[TestFixture]
[Sequential]
public class MathTests
{
    // 测试方法和其他注解
}

14.[OneTimeSetUp]:标记一次性初始

标记一个方法,在整个测试过程开始之前执行一次的准备操作。适用于设置整个测试过程中的共享环境。

[OneTimeSetUp]
public void OneTimeSetup()
{
    // 在整个测试过程开始之前执行的设置操作
}

15.[OneTimeTearDown]:标记一次性清理

标记一个方法,在整个测试过程结束之后执行一次的清理操作。适用于清理整个测试过程中产生的资源或状态。

[OneTimeTearDown]
public void OneTimeTearDown()
{
    // 在整个测试过程结束之后执行的清理操作
}

16.[TestOf]:标记测试对象

标记一个测试方法,指定要测试的方法或类的名称。用于更清晰地指示测试方法的目标。

[Test]
[TestOf(typeof(MathUtils))]
public void Add_TwoPositiveNumbers_ReturnsCorrectSum()
{
    // 测试方法的代码和断言
}

17.[Range]:指定参数范围

标记一个参数,指定参数的取值范围。可以用于生成连续的测试用例。

[Test]
public void Divide_TwoNumbers_ReturnsCorrectQuotient(
    [Range(1, 10)] int a,
    [Range(1, 10)] int b)
{
    // 测试方法的代码和断言
}

18.[Random]:指定参数随机

标记一个参数,指定参数的随机值。可以用于生成随机的测试用例。

[Test]
public void Multiply_TwoRandomNumbers_ReturnsCorrectProduct(
    [Random(1, 10, 5)] int a,
    [Random(1, 10, 5)] int b)
{
    // 测试方法的代码和断言
}

19.[Values]:指定参数

标记一个参数,指定参数的多个取值。可以用于生成离散的测试用例。

[Test]
public void Subtract_TwoNumbers_ReturnsCorrectDifference(
    [Values(10, 5, 2)] int a,
    [Values(2, 1, 1)] int b)
{
    // 测试方法的代码和断言
}

注意 Values此标记中的值交叉映射到了 Subtract_TwoNumbers_ReturnsCorrectDifference方法的参数a和b身上,一共进行的9次离散测试。不是三次测试哦,具体解释见下面。

20.[Values]和[TestCase]的区别

[Values][TestCase]是NUnit中用于参数化测试的两个注解,它们有一些区别和适用场景:

  1. [Values][Values]注解用于指定参数的多个取值,生成离散的测试用例。每个参数值都会作为独立的测试用例进行执行。 [Test]
    public void Subtract_TwoNumbers_ReturnsCorrectDifference(
    [Values(10, 5, 2)] int a,
    [Values(2, 1, 1)] int b)
    {
    // 测试方法的代码和断言
    }

上面的示例会生成9个测试用例,分别是:

  • a=10, b=2
  • a=10, b=1
  • a=10, b=1
  • a=5, b=2
  • a=5, b=1
  • a=5, b=1
  • a=2, b=2
  • a=2, b=1
  • a=2, b=1

[Values]适合于具有离散参数取值的测试场景,可以方便地生成多个测试用例,覆盖各种不同的参数组合。

  1. [TestCase][TestCase]注解用于指定参数化的测试用例。通过传递不同的参数进行多次测试,每个参数组合都会作为一个测试用例进行执行。 [Test]
    [TestCase(5, 2, 3)]
    [TestCase(10, 5, 5)]
    public void Subtract_TwoNumbers_ReturnsCorrectDifference(int a, int b, int expectedDifference)
    {
    // 测试方法的代码和断言
    }

上面的示例会生成两个测试用例,分别是:

  • a=5, b=2, expectedDifference=3
  • a=10, b=5, expectedDifference=5

[TestCase]适合于每个参数组合都有不同的期望结果的测试场景,可以通过指定不同的参数组合和期望结果来进行多次测试。

总结来说,[Values]用于生成离散的参数取值的测试用例,而[TestCase]用于指定具体的参数组合和期望结果的测试用例。根据具体的测试需求,选择适合的注解来编写参数化测试用例。

21.[TestCaseSource] :提供测试用例数据源

[TestCaseSource] 是 NUnit 中用于参数化测试的特性之一。它允许从静态字段、属性或方法中提供测试用例数据源。

使用 [TestCaseSource] 可以灵活地定义和传递测试用例数据,从而在不修改测试方法代码的情况下执行多组测试。可以从一个数据源中动态提供测试用例。

(1)使用静态字段作为数据源

private static readonly object[] TestCases =
{
    new object[] { 2, 3, 5 },    // 正常情况下的加法
    new object[] { -2, 3, 1 },   // 负数相加
    new object[] { 0, 0, 0 },    // 零相加
    // 其他测试用例...
};

[Test]
[TestCaseSource(nameof(TestCases))]
public void AddTest(int a, int b, int expected)
{
    int actual = Add(a, b);
    Assert.AreEqual(expected, actual);
}

(2)使用静态属性作为数据源

private static IEnumerable TestCases => new[]
{
    new TestCaseData(2, 3, 5),    // 正常情况下的加法
    new TestCaseData(-2, 3, 1),   // 负数相加
    new TestCaseData(0, 0, 0),    // 零相加
    // 其他测试用例...
};

[Test]
[TestCaseSource(nameof(TestCases))]
public void AddTest(int a, int b, int expected)
{
    int actual = Add(a, b);
    Assert.AreEqual(expected, actual);
}

(3)使用静态方法作为数据源

private static IEnumerable TestCases()
{
    yield return new TestCaseData(2, 3, 5);    // 正常情况下的加法
    yield return new TestCaseData(-2, 3, 1);   // 负数相加
    yield return new TestCaseData(0, 0, 0);    // 零相加
    // 其他测试用例...
}

[Test]
[TestCaseSource(nameof(TestCases))]
public void AddTest(int a, int b, int expected)
{
    int actual = Add(a, b);
    Assert.AreEqual(expected, actual);
}

注意事项:

  • 测试用例数据源可以是 object[]IEnumerableIEnumerable<object>IEnumerable<TestCaseData> 等类型。
  • 对于每个测试用例,需要使用 TestCaseData 类的实例进行封装,以提供输入参数和预期结果。
  • 数据源名称可以是数据源字段、属性或方法的名称,可以使用 nameof 运算符避免硬编码。

通过使用 [TestCaseSource] 特性,可以简化测试用例的编写,同时提供更灵活的参数化测试能力。这样可以轻松地执行多组输入输出的测试,覆盖不同的场景和边界情况。

22.示例一: MathUtilsTests

以下是一个稍微复杂一点的NUnit单元测试代码示例:

using NUnit.Framework;

[TestFixture]
public class MathUtilsTests
{
    private MathUtils mathUtils;

    [OneTimeSetUp]
    public void OneTimeSetUp()
    {
        // 在整个测试过程开始之前执行的设置操作
        mathUtils = new MathUtils();
    }

    [OneTimeTearDown]
    public void OneTimeTearDown()
    {
        // 在整个测试过程结束之后执行的清理操作
        mathUtils = null;
    }

    [SetUp]
    public void Setup()
    {
        // 在每个测试方法运行之前执行的设置操作
        // 可以在这里进行初始化操作或准备测试数据
    }

    [TearDown]
    public void TearDown()
    {
        // 在每个测试方法运行之后执行的清理操作
        // 可以在这里进行资源释放或状态重置
    }

    [Test]
    public void Add_TwoPositiveNumbers_ReturnsCorrectSum()
    {
        // Arrange
        int a = 5;
        int b = 3;
        int expectedSum = 8;

        // Act
        int sum = mathUtils.Add(a, b);

        // Assert
        Assert.AreEqual(expectedSum, sum);
    }

    [Test]
    public void Divide_TwoNumbers_ReturnsCorrectQuotient()
    {
        // Arrange
        int a = 10;
        int b = 2;
        int expectedQuotient = 5;

        // Act
        int quotient = mathUtils.Divide(a, b);

        // Assert
        Assert.AreEqual(expectedQuotient, quotient);
    }

    [Test]
    [TestCase(5, 2, 3)]
    [TestCase(10, 5, 5)]
    public void Subtract_TwoNumbers_ReturnsCorrectDifference(int a, int b, int expectedDifference)
    {
        // Act
        int difference = mathUtils.Subtract(a, b);

        // Assert
        Assert.AreEqual(expectedDifference, difference);
    }

    [Test]
    [MaxTime(1000)]
    public void Multiply_TwoNumbers_ReturnsCorrectProduct()
    {
        // Arrange
        int a = 4;
        int b = 5;
        int expectedProduct = 20;

        // Act
        int product = mathUtils.Multiply(a, b);

        // Assert
        Assert.AreEqual(expectedProduct, product);
    }

    [Test]
    [Retry(3)]
    public void Divide_ByZero_ThrowsDivideByZeroException()
    {
        // Arrange
        int a = 10;
        int b = 0;

        // Act & Assert
        Assert.Throws<DivideByZeroException>(() => mathUtils.Divide(a, b));
    }
}

上述示例展示了一个包含常见特性的复杂一点的NUnit单元测试代码:

  • 使用了[TestFixture]标记测试类,并在其中进行初始化和清理操作。
  • 使用了[SetUp][TearDown]分别在每个测试方法前后执行设置和清理操作。
  • 使用了[OneTimeSetUp][OneTimeTearDown]在整个测试过程开始和结束时执行设置和清理操作。
  • 使用了不同的测试方法,包括参数化测试([TestCase]),最大执行时间限制([MaxTime]),以及失败重试([Retry]`)等。
  • 使用了Assert断言来验证测试结果的正确性。

请根据实际需要适配这个示例,并在测试方法中编写具 体的测试逻辑和断言,以确保代码的正确性和覆盖率。

23.示例二:MainWindowTests

以下是一个更复杂和全面的WPF单元测试代码示例,涉及多个控件和功能:

using NUnit.Framework;
using Moq;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;

[TestFixture]
public class MainWindowTests
{
    private MainWindow mainWindow;
    private Mock<IDataService> dataServiceMock;

    [SetUp]
    public void Setup()
    {
        // 创建数据服务的模拟对象
        dataServiceMock = new Mock<IDataService>();

        // 创建主窗口,并将模拟对象注入
        mainWindow = new MainWindow(dataServiceMock.Object);
    }

    [Test]
    public void AddItemButton_Clicked_ItemIsAddedToListBox()
    {
        // Arrange
        string newItem = "New Item";
        Button addButton = mainWindow.FindName("AddItemButton") as Button;
        ListBox itemListBox = mainWindow.FindName("ItemListBox") as ListBox;

        // Act
        mainWindow.ItemTextBox.Text = newItem;
        addButton.RaiseEvent(new RoutedEventArgs(Button.ClickEvent));

        // Assert
        Assert.IsTrue(itemListBox.Items.Contains(newItem));
    }

    [Test]
    public void RemoveItemButton_Clicked_SelectedItemIsRemovedFromListBox()
    {
        // Arrange
        string itemToRemove = "Item 1";
        Button removeButton = mainWindow.FindName("RemoveItemButton") as Button;
        ListBox itemListBox = mainWindow.FindName("ItemListBox") as ListBox;

        // Add items to the ListBox
        List<string> items = new List<string> { itemToRemove, "Item 2", "Item 3" };
        foreach (string item in items)
        {
            itemListBox.Items.Add(item);
        }

        // Select an item to remove
        itemListBox.SelectedItem = itemToRemove;

        // Act
        removeButton.RaiseEvent(new RoutedEventArgs(Button.ClickEvent));

        // Assert
        Assert.IsFalse(itemListBox.Items.Contains(itemToRemove));
    }

    [Test]
    public void LoadDataButton_Clicked_DataServiceIsCalledAndItemsAreAddedToListBox()
    {
        // Arrange
        Button loadDataButton = mainWindow.FindName("LoadDataButton") as Button;
        ListBox itemListBox = mainWindow.FindName("ItemListBox") as ListBox;

        // Mock data to be returned by the data service
        List<string> data = new List<string> { "Item 1", "Item 2", "Item 3" };
        dataServiceMock.Setup(service => service.GetData()).Returns(data);

        // Act
        loadDataButton.RaiseEvent(new RoutedEventArgs(Button.ClickEvent));

        // Assert
        dataServiceMock.Verify(service => service.GetData(), Times.Once);
        Assert.AreEqual(data.Count, itemListBox.Items.Count);

        for (int i = 0; i < data.Count; i++)
        {
            Assert.AreEqual(data[i], itemListBox.Items[i]);
        }
    }
}

在这个示例中,我们针对一个包含按钮、文本框和列表框等多个控件的WPF主窗口(MainWindow)编写了单元测试。这个示例展示了以下内容:

  • 使用[TestFixture]标记测试类,使用[SetUp]在每个测试方法运行之前进行设置。
  • 使用Mock<T>创建模拟对象来代替实际的依赖项,以确保测试的独立性。
  • 使用FindName方法查找WPF控件,并进行类型转换以获取特定控件的实例。
  • 使用RaiseEvent方法模拟按钮的点击事件,以触发相关的事件处理程序。
  • 使用Setup方法设置模拟对象的行为和返回值,以模拟不同的场景和测试条件。
  • 使用Verify方法验证模拟对象的方法是否被正确调用。
  • 使用Assert断言来验证预期结果和实际结果是否一致。

具体而言,示例包括了以下测试方法:

  • AddItemButton_Clicked_ItemIsAddedToListBox:测试点击“AddItemButton”按钮后,新项是否被正确添加到列表框中。
  • RemoveItemButton_Clicked_SelectedItemIsRemovedFromListBox:测试点击“RemoveItemButton”按钮后,选定的项是否从列表框中被正确移除。
  • LoadDataButton_Clicked_DataServiceIsCalledAndItemsAreAddedToListBox:测试点击“LoadDataButton”按钮后,数据服务的GetData方法是否被正确调用,并且返回的数据是否正确地添加到列表框中。

这个示例展示了如何使用NUnit和Moq来进行更复杂和全面的WPF控件的单元测试。根据你的实际WPF应用程序的需求和场景,你可以适应和扩展这个示例,并编写适合你项目的测试用例。

24.示例三:ShoppingCartViewModelTests

以下是一个更贴近项目的WPF单元测试代码示例,模拟了一个购物车功能的测试:

using NUnit.Framework;
using Moq;
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows;
using System.Windows.Controls;

[TestFixture]
public class ShoppingCartViewModelTests
{
    private ShoppingCartViewModel shoppingCartViewModel;
    private Mock<ICartService> cartServiceMock;
    private Mock<IDialogService> dialogServiceMock;
    private ObservableCollection<Product> mockProducts;

    [SetUp]
    public void Setup()
    {
        // 创建购物车服务和对话框服务的模拟对象
        cartServiceMock = new Mock<ICartService>();
        dialogServiceMock = new Mock<IDialogService>();

        // 创建购物车视图模型,并将模拟对象注入
        shoppingCartViewModel = new ShoppingCartViewModel(cartServiceMock.Object, dialogServiceMock.Object);

        // 模拟一些产品数据
        mockProducts = new ObservableCollection<Product>
        {
            new Product { Id = 1, Name = "Product 1", Price = 10.0 },
            new Product { Id = 2, Name = "Product 2", Price = 20.0 },
            new Product { Id = 3, Name = "Product 3", Price = 30.0 }
        };
    }

    [Test]
    public void AddToCartCommand_Executed_ProductIsAddedToCart()
    {
        // Arrange
        Product selectedProduct = mockProducts.First();
        cartServiceMock.Setup(service => service.AddToCart(selectedProduct));

        // Act
        shoppingCartViewModel.SelectedProduct = selectedProduct;
        shoppingCartViewModel.AddToCartCommand.Execute(null);

        // Assert
        cartServiceMock.Verify(service => service.AddToCart(selectedProduct), Times.Once);
        Assert.IsTrue(shoppingCartViewModel.CartItems.Contains(selectedProduct));
    }

    [Test]
    public void RemoveFromCartCommand_Executed_ProductIsRemovedFromCart()
    {
        // Arrange
        Product selectedProduct = mockProducts.First();
        shoppingCartViewModel.CartItems.Add(selectedProduct);
        cartServiceMock.Setup(service => service.RemoveFromCart(selectedProduct));

        // Act
        shoppingCartViewModel.SelectedCartItem = selectedProduct;
        shoppingCartViewModel.RemoveFromCartCommand.Execute(null);

        // Assert
        cartServiceMock.Verify(service => service.RemoveFromCart(selectedProduct), Times.Once);
        Assert.IsFalse(shoppingCartViewModel.CartItems.Contains(selectedProduct));
    }

    [Test]
    public void CheckoutCommand_Executed_DialogIsShownAndCartIsCleared()
    {
        // Arrange
        shoppingCartViewModel.CartItems.Add(mockProducts.First());
        dialogServiceMock.Setup(service => service.ShowConfirmationDialog(It.IsAny<string>(), It.IsAny<string>())).Returns(true);
        cartServiceMock.Setup(service => service.Checkout());

        // Act
        shoppingCartViewModel.CheckoutCommand.Execute(null);

        // Assert
        dialogServiceMock.Verify(service => service.ShowConfirmationDialog("Checkout", "Are you sure you want to checkout?"), Times.Once);
        cartServiceMock.Verify(service => service.Checkout(), Times.Once);
        Assert.IsEmpty(shoppingCartViewModel.CartItems);
    }
}

在这个示例中,我们针对一个购物车视图模型(ShoppingCartViewModel)编写了单元测试。这个示例展示了以下内容:

  • 使用[TestFixture]标记测试类,使用[SetUp]在每个测试方法运行之前进行设置。
  • 使用Mock<T>创建模拟对象来代替实际的依赖项,以确保测试的独立性。
  • 使用Setup方法设置模拟对象的行为和返回值,以模拟不同的场景和测试条件。
  • 使用Verify方法验证模拟对象的方法是否被正确调用。
  • 使用Assert断言来验证预期结果和实际结果是否一致。

具体而言,示例包括了以下测试方法:

  • AddToCartCommand_Executed_ProductIsAddedToCart:测试执行“AddToCartCommand”命令后,选定的产品是否被正确添加到购物车。
  • RemoveFromCartCommand_Executed_ProductIsRemovedFromCart:测试执行“RemoveFromCartCommand”命令后,选定的产品是否被正确从购物车中移除。
  • CheckoutCommand_Executed_DialogIsShownAndCartIsCleared:测试执行“CheckoutCommand”命令后,确认对话框是否被正确显示,并且购物车是否被正确清空。

这个示例展示了如何使用NUnit和Moq来进行WPF应用程序中购物车功能的单元测试。你可以根据你的实际项目需求和场景,适应和扩展这个示例,并编写适合你项目的测试用例。

25.示例四:MedicalImageProcessorTests

以下是一个关于医学图像处理的单元测试代码示例:

using NUnit.Framework;
using Moq;
using System.Drawing;

[TestFixture]
public class MedicalImageProcessorTests
{
    private MedicalImageProcessor imageProcessor;
    private Mock<IMedicalImageLoader> imageLoaderMock;
    private Mock<IMedicalImageAnalyzer> imageAnalyzerMock;
    private Mock<IMedicalImageVisualizer> imageVisualizerMock;

    [SetUp]
    public void Setup()
    {
        // 创建医学图像加载器、图像分析器和图像可视化器的模拟对象
        imageLoaderMock = new Mock<IMedicalImageLoader>();
        imageAnalyzerMock = new Mock<IMedicalImageAnalyzer>();
        imageVisualizerMock = new Mock<IMedicalImageVisualizer>();

        // 创建医学图像处理器,并将模拟对象注入
        imageProcessor = new MedicalImageProcessor(imageLoaderMock.Object, imageAnalyzerMock.Object, imageVisualizerMock.Object);
    }

    [Test]
    public void LoadAndAnalyzeMedicalImage_ImageIsLoadedAndAnalysisResultsAreReturned()
    {
        // Arrange
        string imagePath = "path/to/medical/image.dcm";
        MedicalImage medicalImage = new MedicalImage(imagePath);
        MedicalImageAnalysisResult analysisResult = new MedicalImageAnalysisResult();

        imageLoaderMock.Setup(loader => loader.LoadImage(imagePath)).Returns(medicalImage);
        imageAnalyzerMock.Setup(analyzer => analyzer.AnalyzeImage(medicalImage)).Returns(analysisResult);

        // Act
        var result = imageProcessor.LoadAndAnalyzeMedicalImage(imagePath);

        // Assert
        imageLoaderMock.Verify(loader => loader.LoadImage(imagePath), Times.Once);
        imageAnalyzerMock.Verify(analyzer => analyzer.AnalyzeImage(medicalImage), Times.Once);
        Assert.AreEqual(analysisResult, result);
    }

    [Test]
    public void VisualizeMedicalImage_ImageIsLoadedAndVisualized()
    {
        // Arrange
        string imagePath = "path/to/medical/image.dcm";
        MedicalImage medicalImage = new MedicalImage(imagePath);
        Image visualizedImage = new Bitmap(800, 600);

        imageLoaderMock.Setup(loader => loader.LoadImage(imagePath)).Returns(medicalImage);
        imageVisualizerMock.Setup(visualizer => visualizer.VisualizeImage(medicalImage)).Returns(visualizedImage);

        // Act
        imageProcessor.VisualizeMedicalImage(imagePath);

        // Assert
        imageLoaderMock.Verify(loader => loader.LoadImage(imagePath), Times.Once);
        imageVisualizerMock.Verify(visualizer => visualizer.VisualizeImage(medicalImage), Times.Once);
    }
}

在这个示例中,我们针对一个医学图像处理器(MedicalImageProcessor)编写了单元测试。这个示例展示了以下内容:

  • 使用[TestFixture]标记测试类,使用[SetUp]在每个测试方法运行之前进行设置。
  • 使用Mock<T>创建模拟对象来代替实际的依赖项,以确保测试的独立性。
  • 使用Setup方法设置模拟对象的行为和返回值,以模拟不同的场景和测试条件。
  • 使用Verify方法验证模拟对象的方法是否被正确调用。
  • 使用Assert断言来验证预期结果和实际结果是否一致。

具体而言,示例包括了以下测试方法:

  • LoadAndAnalyzeMedicalImage_ImageIsLoadedAndAnalysisResultsAreReturned:测试加载医学图像并进行图像分析时,图像是否被正确加载,并且分析结果是否正确返回。
  • VisualizeMedicalImage_ImageIsLoadedAndVisualized:测试加载医学图像并进行图像可视化时,图像是否被正确加载,并且图像是否被正确可视化。

这个示例展示了如何使用NUnit和Moq来进行医学图像处理软件的单元测试。你可以根据你的实际项目需求和场景,适应和扩展这个示例,并编写适合你项目的测试用例。

以下是医学图像处理器(MedicalImageProcessor)的简单实现示例:

public class MedicalImageProcessor
{
    private readonly IMedicalImageLoader imageLoader;
    private readonly IMedicalImageAnalyzer imageAnalyzer;
    private readonly IMedicalImageVisualizer imageVisualizer;

    public MedicalImageProcessor(IMedicalImageLoader imageLoader, IMedicalImageAnalyzer imageAnalyzer, IMedicalImageVisualizer imageVisualizer)
    {
        this.imageLoader = imageLoader;
        this.imageAnalyzer = imageAnalyzer;
        this.imageVisualizer = imageVisualizer;
    }

    public MedicalImageAnalysisResult LoadAndAnalyzeMedicalImage(string imagePath)
    {
        MedicalImage medicalImage = imageLoader.LoadImage(imagePath);
        MedicalImageAnalysisResult analysisResult = imageAnalyzer.AnalyzeImage(medicalImage);
        return analysisResult;
    }

    public void VisualizeMedicalImage(string imagePath)
    {
        MedicalImage medicalImage = imageLoader.LoadImage(imagePath);
        imageVisualizer.VisualizeImage(medicalImage);
    }
}

在上述示例中,MedicalImageProcessor类是医学图像处理器的简单实现。它接收医学图像加载器(IMedicalImageLoader)、图像分析器(IMedicalImageAnalyzer)和图像可视化器(IMedicalImageVisualizer)作为构造函数的参数,并使用它们进行图像处理。

LoadAndAnalyzeMedicalImage方法接收图像路径作为参数,首先使用图像加载器加载医学图像,然后使用图像分析器对图像进行分析,并返回分析结果。

VisualizeMedicalImage方法接收图像路径作为参数,同样使用图像加载器加载医学图像,然后使用图像可视化器对图像进行可视化。

通过依赖注入的方式,MedicalImageProcessor类的实现与具体的图像加载、分析和可视化实现解耦,使得代码更加灵活、可测试和可扩展。

26.自定义测试标签【重要】

在 NUnit 中,你可以使用自定义标签(Custom Attributes)来为测试方法或测试类添加自定义元数据。这些自定义标签可以用于分类、过滤、标记或为测试提供其他相关信息。自定义标签的设计相对简单,需要集合业务情况进行适当处理。部分流行的自定义标签已经被Nunit.Framework接纳吸收,不需要再自定义了。比如作者标签,描述标签等等可以直接使用。

1.标记测试方法的优先级(Priority):

using System;

namespace MyNamespace
{
    [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
    public class PriorityAttribute : Attribute
    {
        public int Value { get; private set; }

        public PriorityAttribute(int value)
        {
            Value = value;
        }
    }
}

使用示例:

using NUnit.Framework;
using MyNamespace;

[TestFixture]
public class MyTestClass
{
    [Test]
    [Priority(1)] // 设置优先级为 1
    public void MyTestMethod()
    {
        // 测试逻辑
    }
}

2.标记测试方法的重要性(Importance):

using System;

namespace MyNamespace
{
    [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
    public class ImportanceAttribute : Attribute
    {
        public ImportanceLevel Level { get; private set; }

        public ImportanceAttribute(ImportanceLevel level)
        {
            Level = level;
        }
    }

    public enum ImportanceLevel
    {
        Low,
        Medium,
        High
    }
}

使用示例:

using NUnit.Framework;
using MyNamespace;

[TestFixture]
public class MyTestClass
{
    [Test]
    [Importance(ImportanceLevel.High)] // 设置重要性为 High
    public void MyTestMethod()
    {
        // 测试逻辑
    }
}

3.标记测试类的功能模块(Module):

using System;

namespace MyNamespace
{
    [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
    public class ModuleAttribute : Attribute
    {
        public string Name { get; private set; }

        public ModuleAttribute(string name)
        {
            Name = name;
        }
    }
}

使用示例:

using NUnit.Framework;
using MyNamespace;

[Module("User Management")] // 标记测试类属于 "User Management" 模块
public class UserManagementTests
{
    [Test]
    public void CreateUserTest()
    {
        // 测试逻辑
    }
}

4.标记测试方法的作者(Author):

using System;

namespace MyNamespace
{
    [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
    public class AuthorAttribute : Attribute
    {
        public string Name { get; private set; }

        public AuthorAttribute(string name)
        {
            Name = name;
        }
    }
}

使用示例:

using NUnit.Framework;
using MyNamespace;

[TestFixture]
public class MyTestClass
{
    [Test]
    [Author("John Doe")] // 设置作者为 "John Doe"
    public void MyTestMethod()
    {
        // 测试逻辑
    }
}

5.标记测试方法的描述(Description):

using System;

namespace MyNamespace
{
    [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
    public class DescriptionAttribute : Attribute
    {
        public string Text { get; private set; }

        public DescriptionAttribute(string text)
        {
            Text = text;
        }
    }
}

使用示例:

using NUnit.Framework;
using MyNamespace;

[TestFixture]
public class MyTestClass
{
    [Test]
    [Description("This test case verifies the login functionality.")] // 设置描述信息
    public void LoginTest()
    {
        // 测试逻辑
    }
}

6.标记测试方法的数据源(DataSource):

using System;

namespace MyNamespace
{
    [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
    public class DataSourceAttribute : Attribute
    {
        public string SourceName { get; private set; }

        public DataSourceAttribute(string sourceName)
        {
            SourceName = sourceName;
        }
    }
}

使用示例:

using NUnit.Framework;
using MyNamespace;

[TestFixture]
public class MyTestClass
{
    [Test]
    [DataSource("TestData.csv")] // 设置数据源为 "TestData.csv"
    public void DataDrivenTest()
    {
        // 测试逻辑
    }
}

这些示例展示了一些常用的 NUnit 自定义标签,你可以根据实际需求创建更多的自定义标签。通过自定义标签,你可以为测试方法或测试类添加额外的信息和元数据,以便更好地组织和管理测试。

7.示例:自定义标签[FileDataSource]实现读取数据源进行用例测试

要实现从文件中读取测试用例数据作为数据源,并跳过文件的第一行数据,你可以结合使用 NUnit 的 TestCaseSource 特性和自定义标签来实现。以下是具体数据源测试用例

(1)测试数据源文件

a b excepted
1,2,3
0,0,0
-1,-2,-3
-0,-1,-1
-010,-101,-111

以上测试用例仅作演示,不具备测试参考价值。实际测试请另行编写测试用例!

(2)自定义测试标签[FileDataSource(path:)]实现代码

以下是一个示例实现:首先,创建一个自定义的数据源属性,用于从文件中读取测试数据并跳过第一行

using System;
using System.Collections.Generic;
using System.IO;

namespace MyNamespace
{
    [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
    public class FileDataSourceAttribute : Attribute
    {
        public string FilePath { get; private set; }

        public FileDataSourceAttribute(string filePath)
        {
            FilePath = filePath;
        }

        public IEnumerable<TestCaseData> GetTestCases()
        {
            List<TestCaseData> testCases = new List<TestCaseData>();

            // 读取测试数据文件
            string[] lines = File.ReadAllLines(FilePath);

            // 跳过第一行数据(标题行)
            for (int i = 1; i < lines.Length; i++)
            {
                string line = lines[i];
                string[] values = line.Split(',');

                // 解析测试数据并添加到 TestCaseData 列表
                if (
                      values.Length == 3 
                      && int.TryParse(values[0], out int input1) 
                      && int.TryParse(values[1], out int input2) 
                      && int.TryParse(values[2], out int expectedOutput))
                {
                    TestCaseData testCase = new TestCaseData(input1, input2, expectedOutput);
                    testCases.Add(testCase);
                }
                else
                {
                    // 如果测试数据格式不正确,可以选择记录日志或抛出异常
                    throw new FormatException("Invalid test data format in the file.");
                }
            }
            return testCases;
        }
    }
}

在上述示例中,我们修改了之前的实现,添加了一个 for 循环来遍历文件的每一行数据,并跳过了第一行(索引为 0)。

然后,你可以在测试方法中使用 FileDataSourceAttributeTestCaseSource 特性,将数据源与测试方法关联起来,就像之前提供的示例中那样。

(3)测试类

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using FileDataSourceAttribute = TestTagBulid.FileDataSourceAttribute;
namespace TestTagBulid
{
    [TestFixture]
    [Author("ZhangJincheng")]
    public class MyTestClass
    {
        [Test]
        [TestCaseSource(typeof(FileDataSourceAttribute), nameof(FileDataSourceAttribute.GetTestCases))]
        [FileDataSource(@"C:/MISATDecompile/TestTagBulid/TestData.csv")]
        public void DataDrivenTest(int input1, int input2, int expectedOutput)
        {
            // 执行测试逻辑,并使用断言验证结果是否符合预期
            int actualOutput = SomeOperation(input1, input2);
            Assert.AreEqual(expectedOutput, actualOutput);
        }

        private int SomeOperation(int a, int b)
        {
            // 执行一些操作并返回结果
            return a + b;
        }
    }
}

在上述示例中,我们使用 [FileDataSource] 标签指定了数据源属性为 FileDataSourceAttribute,并使用 [TestCaseSource] 特性将数据源方法指定为 GetTestCases。通过这种方式,你可以从文件中读取测试用例数据作为数据源,并在读取数据时跳过第一行。如果不需要则修改for循环即可。

(4)测试结果