对企业应用进行单元测试

注意

本电子书于 2017 年春季出版,之后再未更新。 书中有许多内容仍然很有价值,但有些材料已经过时。

移动应用存在桌面应用程序和基于 Web 的应用程序中不会存在的独特问题。 移动用户因使用的设备、网络连接、服务可用性以及一系列其他因素而有所不同。 因此,应测试移动应用(因为它们将在现实世界中使用),以提高其质量、可靠性和性能。 应该对应用执行多种类型的测试,包括单元测试、集成测试和用户界面测试,其中单元测试是最常见的测试形式。

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

作为软件开发工作流的一个组成部分时,单元测试对代码质量的影响最大。 一旦编写了方法,就应该编写单元测试来验证该方法响应标准、边界和不正确输入数据情况的行为,并检查代码做出的任何显式或隐式假设。 或者,如果使用测试驱动开发,则会在代码之前编写单元测试。 在此方案中,单元测试既充当设计文档,又充当功能规范。

注意

单元测试对于防止回归(即,过去可以正常工作,但由于错误更新而受到干扰的功能)非常有效。

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

  • 单元测试方法的“准备”部分初始化对象并设置传递给待测试方法的数据的值
  • “行动”部分使用所需参数调用待测试方法
  • “断言”部分验证待测试方法的执行行为是否与预期相同

遵循此模式可确保单元测试可读且一致。

依赖项注入和单元测试

采用松散耦合体系结构的动机之一是它有助于单元测试。 向 Autofac 注册的类型之一是 OrderService 类。 下面的代码示例显示了该类的概要:

public class OrderDetailViewModel : ViewModelBase  
{  
    private IOrderService _ordersService;  

    public OrderDetailViewModel(IOrderService ordersService)  
    {  
        _ordersService = ordersService;  
    }  
    ...  
}

OrderDetailViewModel 类依赖于容器在实例化 OrderDetailViewModel 对象时解析的 IOrderService 类型。 但是,请不要创建 OrderService 对象来对 OrderDetailViewModel 类进行单元测试,而是将 OrderService 对象替换为模拟以进行测试。 图 10-1 演示了这种关系。

实现 IOrderService 接口的类

图 10-1:实现 IOrderService 接口的类

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

测试 MVVM 应用程序

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

提示

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

eShopOnContainers 移动应用执行单元测试,它支持两种不同类型的单元测试:

  • 事实是始终为 true 的测试,用于测试固定条件。
  • 理论是只对特定数据集为 true 的测试。

eShopOnContainers 多平台应用包含的单元测试是事实测试,因此每个单元测试方法都使用 [Fact] 属性进行修饰。

注意

xUnit 测试由测试运行程序执行。 若要执行测试运行程序,请运行所需平台的 eShopOnContainers.TestRunner 项目。

测试异步功能

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

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

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

    Assert.NotNull(orderViewModel.Order);  
}

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

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

测试 INotifyPropertyChanged 实现

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

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

[Fact]  
public async Task SettingOrderPropertyShouldRaisePropertyChanged()  
{  
    bool 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.True(invoked);  
}

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

测试基于消息的通信

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

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

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

    Assert.True(messageReceived);  
}

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

测试异常处理

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

[Fact]  
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 方法的结果进行测试,如以下代码示例所示:

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

    bool isValid = mockViewModel.Validate();  

    Assert.True(isValid);  
}

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

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

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

    bool isValid = mockViewModel.Validate();  

    Assert.False(isValid);  
    Assert.NotNull(mockViewModel.Forename.Value);  
    Assert.Null(mockViewModel.Surname.Value);  
    Assert.True(mockViewModel.Forename.IsValid);  
    Assert.False(mockViewModel.Surname.IsValid);  
    Assert.Empty(mockViewModel.Forename.Errors);  
    Assert.NotEmpty(mockViewModel.Surname.Errors);  
}

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

总结

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

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

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