单元测试

提示

此内容摘自电子书《使用 .NET MAUI 的企业应用程序模式》,可在 .NET 文档上获取,也可作为免费可下载的 PDF 脱机阅读。

《使用 .NET MAUI 的企业应用程序模式》电子书包含缩略图。

多平台应用遇到类似于桌面和基于 Web 的应用程序的问题。 移动用户会因设备、网络连接、服务可用性以及各种其他因素而异。 因此,应测试多平台应用(因为它们将在现实世界中使用),以提高其质量、可靠性和性能。 应在应用上执行许多类型的测试,包括单元测试、集成测试和用户界面测试。 单元测试是最常见的形式,也是构建高质量应用程序必不可少的。

单元测试取应用的一个小单元,通常是一个方法,将其与代码的其余部分隔离开,并验证其行为是否符合预期。 其目标是检查每个功能单元是否按预期执行,以便错误不会在整个应用中传播。 在发生 bug 的位置检测 bug,比在次要故障点位置间接观察 bug 造成的影响更为有效。

作为软件开发工作流的一个组成部分时,单元测试对代码质量的影响最为显着。 单元测试可以充当应用程序的设计文档和功能规范。 一旦编写了方法,就应该编写单元测试来验证该方法响应标准、边界和不正确输入数据情况的行为,并检查代码做出的任何显式或隐式假设。 或者,如果使用测试驱动开发,则会在代码之前编写单元测试。 有关测试驱动开发以及如何实现它的详细信息,请参阅演练:使用测试资源管理器进行测试驱动开发。

注意

单元测试对回归非常有效。 也就是说,功能曾经正常工作,但被错误的更新所干扰。

单元测试通常使用准备-执行-断言模式:

步骤 说明
排列 初始化对象并设置传递给待测试方法的数据的值。
操作 使用所需参数调用待测试方法。
Assert 验证待测试方法的执行行为是否与预期相同。

此模式可确保单元测试可读、自描述和一致。

依赖关系注入和单元测试

采用松散耦合体系结构的动机之一是它有助于单元测试。 向依赖关系注入服务注册的类型之一是 IAppEnvironmentService 接口。 下面的代码示例显示了该类的概要:

public class OrderDetailViewModel : ViewModelBase
{
    private IAppEnvironmentService _appEnvironmentService;

    public OrderDetailViewModel(
        IAppEnvironmentService appEnvironmentService,
        IDialogService dialogService, INavigationService navigationService, ISettingsService settingsService)
        : base(dialogService, navigationService, settingsService)
    {
        _appEnvironmentService = appEnvironmentService;
    }
}

OrderDetailViewModel 类依赖于 IAppEnvironmentService 类型,依赖关系注入容器在实例化 OrderDetailViewModel 对象时会解析该类型。 但是,与其创建利用真实服务器、设备和配置对 OrderDetailViewModel 类进行单元测试的 IAppEnvironmentService 对象,不如将 IAppEnvironmentService 对象替换为模拟对象以进行测试。 模拟对象是具有与对象或接口相同签名的对象,但以特定的方式创建,以帮助进行单元测试。 它通常与依赖关系注入一起使用,以提供用于测试不同数据和工作流场景的接口的特定实现。

这种方法允许在运行时将 IAppEnvironmentService 对象传递到 OrderDetailViewModel 类中,并且出于可测试性的考虑,它允许在测试时将模拟类传递到 OrderDetailViewModel 类中。 这种方法的主要优点是它可以执行单元测试,而不需要诸如运行时平台功能、Web 服务或数据库等难以操作的资源。

测试 MVVM 应用程序

从 MVVM 应用程序测试模型和视图模型与测试任何其他类相同,并且使用相同的工具和技术;这包括单元测试和模拟等功能。 但是,模型和视图模型类的一些典型模式可以从特定的单元测试技术中受益。

提示

请在每个单元测试中测试一项内容。 随着测试复杂性的扩大,测试的验证变得更加困难。 通过将单元测试限制为单个关注点,可以确保测试更具可重复性、隔离性且执行时间更短。 有关更多最佳做法,请参阅 .NET 的单元测试最佳做法

不要试图在单元测试中练习单元行为的多个方面。 这样做会导致测试难以读取和更新。 在解释失败时,它还可能导致混淆。

eShop 多平台应用使用 MSTest 执行单元测试,它支持两种不同类型的单元测试:

测试类型 属性 说明
TestMethod TestMethod 定义要运行的实际测试方法。
DataSource DataSource 只对特定数据集成立的测试。

eShop 多平台应用包含的单元测试是 TestMethod,因此每个单元测试方法都使用 TestMethod 属性进行修饰。 除了 MSTest 之外,还有一些其他测试框架可用,包括 NUnitxUnit

测试异步功能

实现 MVVM 模式时,视图模型通常会对服务调用操作,通常是异步的。 测试调用这些操作的代码通常使用模拟作为实际服务的替代项。 以下代码示例演示了通过将模拟服务传递到视图模型来测试异步功能:

[TestMethod]
public async Task OrderPropertyIsNotNullAfterViewModelInitializationTest()
{
    // Arrange
    var orderService = new OrderMockService();
    var orderViewModel = new OrderDetailViewModel(orderService);

    // Act
    var order = await orderService.GetOrderAsync(1, GlobalSetting.Instance.AuthToken);
    await orderViewModel.InitializeAsync(order);

    // Assert
    Assert.IsNotNull(orderViewModel.Order);
}

此单元测试检查 OrderDetailViewModel 实例的 Order 属性在调用 InitializeAsync 方法之后是否有一个值。 导航到视图模型的对应视图时,会调用 InitializeAsync 方法。 有关导航的详细信息,请参阅导航

创建 OrderDetailViewModel 实例时,它期望将 IOrderService 实例指定为参数。 但是,OrderService 从 Web 服务中检索数据。 因此,将 OrderMockService 实例(OrderService 类的模拟版本)指定为 OrderDetailViewModel 构造函数的参数。 然后,当调用视图模型的 InitializeAsync 方法(使用 IOrderService 操作)时,将检索模拟数据而不是与 Web 服务通信。

测试 INotifyPropertyChanged 实现

实现 INotifyPropertyChanged 接口允许视图对源自视图模型和模型的更改做出反应。 这些更改不仅限于控件中显示的数据,它们还用于控制视图,例如导致启动动画或禁用控件的视图模型状态。

可以通过将事件处理程序附加到 PropertyChanged 事件并检查在为属性设置新值后是否引发事件来测试可由单元测试直接更新的属性。 下面的代码示例演示了此类测试:

[TestMethod]
public async Task SettingOrderPropertyShouldRaisePropertyChanged()
{
    var invoked = false;
    var orderService = new OrderMockService();
    var orderViewModel = new OrderDetailViewModel(orderService);

    orderViewModel.PropertyChanged += (sender, e) =>
    {
        if (e.PropertyName.Equals("Order"))
            invoked = true;
    };
    var order = await orderService.GetOrderAsync(1, GlobalSetting.Instance.AuthToken);
    await orderViewModel.InitializeAsync(order);

    Assert.IsTrue(invoked);
}

此单元测试调用 OrderViewModel 类的 InitializeAsync 方法,从而更新其 Order 属性。 此单元测试将通过,前提是为 Order 属性引发 PropertyChanged 事件。

测试基于消息的通信

可以通过订阅待测试代码发送的消息来对使用 MessagingCenter 类在松散耦合类之间进行通信的视图模型进行单元测试,如以下代码示例所示:

[TestMethod]
public void AddCatalogItemCommandSendsAddProductMessageTest()
{
    var messageReceived = false;
    var catalogService = new CatalogMockService();
    var catalogViewModel = new CatalogViewModel(catalogService);

    MessagingCenter.Subscribe<CatalogViewModel, CatalogItem>(
        this, MessageKeys.AddProduct, (sender, arg) =>
    {
        messageReceived = true;
    });
    catalogViewModel.AddCatalogItemCommand.Execute(null);

    Assert.IsTrue(messageReceived);
}

此单元测试检查 CatalogViewModel 是否发布 AddProduct 消息以响应正在执行的 AddCatalogItemCommand。 因为 MessagingCenter 类支持多播消息订阅,所以单元测试可以订阅 AddProduct 消息并执行回调委托以响应接收它。 此回调委托(指定为 lambda 表达式)设置一个布尔字段,Assert 语句使用该字段来验证测试的行为。

测试异常处理

也可以编写单元测试来检查是否针对无效操作或输入引发了特定异常,如以下代码示例所示:

[TestMethod]
public void InvalidEventNameShouldThrowArgumentExceptionText()
{
    var behavior = new MockEventToCommandBehavior
    {
        EventName = "OnItemTapped"
    };
    var listView = new ListView();

    Assert.Throws<ArgumentException>(() => listView.Behaviors.Add(behavior));
}

此单元测试将引发异常,因为 ListView 控件没有名为 OnItemTapped 的事件。 Assert.Throws<T> 方法是一个泛型方法,其中 T 是预期异常的类型。 传递给 Assert.Throws<T> 方法的参数是一个 lambda 表达式,它将引发异常。 因此,只要 lambda 表达式引发 ArgumentException,单元测试就会通过。

提示

请避免编写检查异常消息字符串的单元测试。 异常消息字符串可能会随着时间而改变,因此依赖于其存在的单元测试被认为是脆弱的。

测试验证

测试验证实现有两个方面:测试是否正确实现了任何验证规则,以及测试 ValidatableObject<T> 类是否按预期执行。

验证逻辑通常很容易测试,因为它通常是一个自包含的过程,其中输出取决于输入。 应对每个具有至少一个关联验证规则的属性调用 Validate 方法的结果进行测试,如以下代码示例所示:

[TestMethod]
public void CheckValidationPassesWhenBothPropertiesHaveDataTest()
{
    var mockViewModel = new MockViewModel();
    mockViewModel.Forename.Value = "John";
    mockViewModel.Surname.Value = "Smith";

    var isValid = mockViewModel.Validate();

    Assert.IsTrue(isValid);
}

此单元测试检查当 MockViewModel 实例中的两个 ValidatableObject<T> 属性都具有数据时验证是否成功。

除了检查验证是否成功,验证单元测试还应检查每个 ValidatableObject<T> 实例的 ValueIsValidErrors 属性的值,以验证类是否按预期执行。 下面的代码示例演示了执行此操作的单元测试:

[TestMethod]
public void CheckValidationFailsWhenOnlyForenameHasDataTest()
{
    var mockViewModel = new MockViewModel();
    mockViewModel.Forename.Value = "John";

    bool isValid = mockViewModel.Validate();

    Assert.IsFalse(isValid);
    Assert.IsNotNull(mockViewModel.Forename.Value);
    Assert.IsNull(mockViewModel.Surname.Value);
    Assert.IsTrue(mockViewModel.Forename.IsValid);
    Assert.IsFalse(mockViewModel.Surname.IsValid);
    Assert.AreEqual(mockViewModel.Forename.Errors.Count(), 0);
    Assert.AreNotEqual(mockViewModel.Surname.Errors.Count(), 0);
}

此单元测试检查当 MockViewModelSurname 属性没有任何数据时验证是否失败,并且每个 ValidatableObject<T> 实例的 ValueIsValidErrors 属性是否正确设置。

总结

单元测试取应用的一个小单元,通常是一个方法,将其与代码的其余部分隔离开,并验证其行为是否符合预期。 其目标是检查每个功能单元是否按预期执行,以便错误不会在整个应用中传播。

可以通过用模拟依赖对象行为的 mock 对象替换依赖对象来隔离待测试对象的行为。 这样就可以执行单元测试,而不需要诸如运行时平台功能、Web 服务或数据库等难以操作的资源

从 MVVM 应用程序测试模型和视图模型与测试任何其他类相同,并且可以使用相同的工具和技术。