对企业应用进行单元测试Unit Testing Enterprise Apps

移动应用具有独特的问题,因为桌面和基于 web 的应用程序无需担心。Mobile apps have unique problems that desktop and web-based applications don't have to worry about. 移动用户使用的设备、网络连接、服务可用性和其他因素范围会有所不同。Mobile users will differ by the devices that they use, by network connectivity, by the availability of services, and a range of other factors. 因此,应测试移动应用,因为它们将在现实世界中用于提高其质量、可靠性和性能。Therefore, mobile apps should be tested as they will be used in the real world to improve their quality, reliability, and performance. 应该对应用执行多种类型的测试,包括单元测试、集成测试和用户界面测试,并将单元测试作为最常见的测试形式。There are many types of testing that should be performed on an app, including unit testing, integration testing, and user interface testing, with unit testing being the most common form of testing.

单元测试采用一小部分应用,通常是一种方法,将其与代码的其余部分隔离,并验证它是否按预期方式工作。A unit test takes a small unit of the app, typically a method, isolates it from the remainder of the code, and verifies that it behaves as expected. 其目标是检查每个功能单元是否按预期执行,以便不会在整个应用中传播错误。Its goal is to check that each unit of functionality performs as expected, so that errors don't propagate throughout the app. 检测发生 bug 的 bug 更高效,可在辅助故障点间接观察 bug 的影响。Detecting a bug where it occurs is more efficient that observing the effect of a bug indirectly at a secondary point of failure.

当它是软件开发工作流的组成部分时,单元测试对代码质量具有最大的影响。Unit testing has the greatest effect on code quality when it's an integral part of the software development workflow. 一旦编写了方法,就应该编写单元测试来验证方法的行为,以响应输入数据的标准、边界和不正确的情况,并检查代码所做的任何显式或隐式假设。As soon as a method has been written, unit tests should be written that verify the behavior of the method in response to standard, boundary, and incorrect cases of input data, and that check any explicit or implicit assumptions made by the code. 或者,在测试驱动开发中,单元测试在代码之前编写。Alternatively, with test driven development, unit tests are written before the code. 在这种情况下,单元测试将充当设计文档和功能规范。In this scenario, unit tests act as both design documentation and functional specifications.

备注

单元测试对于回归是非常有效的,也就是说,使用的功能,但在发生错误的更新时受到干扰。Unit tests are very effective against regression – that is, functionality that used to work but has been disturbed by a faulty update.

单元测试通常使用 "排列方式-法-断言" 模式:Unit tests typically use the arrange-act-assert pattern:

  • 单元测试方法的 "排列" 部分会初始化对象并设置传递给待测试方法的数据的值。The arrange section of the unit test method initializes objects and sets the value of the data that is passed to the method under test.
  • Act节调用所测试的方法,并提供所需的参数。The act section invokes the method under test with the required arguments.
  • 断言部分验证待测试方法的操作是否按预期方式运行。The assert section verifies that the action of the method under test behaves as expected.

遵循此模式可确保单元测试的可读性和一致性。Following this pattern ensures that unit tests are readable and consistent.

依赖关系注入和单元测试Dependency Injection and Unit Testing

采用松耦合体系结构的动机之一是它有利于单元测试。One of the motivations for adopting a loosely-coupled architecture is that it facilitates unit testing. 向 Autofac 注册的类型之一是OrderService类。One of the types registered with Autofac is the OrderService class. 下面的代码示例演示了此类的大纲:The following code example shows an outline of this class:

public class OrderDetailViewModel : ViewModelBase  
{  
    private IOrderService _ordersService;  

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

类依赖于容器在实例化OrderDetailViewModel对象时解析的类型。IOrderService OrderDetailViewModelThe OrderDetailViewModel class has a dependency on the IOrderService type which the container resolves when it instantiates a OrderDetailViewModel object. 但是,不是创建OrderService对象对OrderDetailViewModel类进行单元测试,而是将OrderService对象替换为模拟,以用于测试目的。However, rather than create an OrderService object to unit test the OrderDetailViewModel class, instead, replace the OrderService object with a mock for the purpose of the tests. 图10-1 说明了这种关系。Figure 10-1 illustrates this relationship.

图10-1: 实现 IOrderService 接口的类Figure 10-1: Classes that implement the IOrderService interface

此方法允许OrderService在运行时将对象传入OrderDetailViewModel类,并且在可测试性的兴趣情况下,它允许OrderMockService将类传入OrderDetailViewModel类中的测试时间。This approach allows the OrderService object to be passed into the OrderDetailViewModel class at runtime, and in the interests of testability, it allows the OrderMockService class to be passed into the OrderDetailViewModel class at test time. 此方法的主要优点是,它可以实现单元测试,而无需使用难于处理的资源(如 web 服务或数据库)。The main advantage of this approach is that it enables unit tests to be executed without requiring unwieldy resources such as web services, or databases.

测试 MVVM 应用程序Testing MVVM Applications

从 MVVM 应用程序测试模型和查看模型与测试任何其他类相同,并且可以使用相同的工具和技术(如单元测试和模拟)。Testing models and view models from MVVM applications is identical to testing any other classes, and the same tools and techniques – such as unit testing and mocking, can be used. 但是,模型和视图模型类具有一些典型模式,特定的单元测试技术可能对这些模式有益。However, there are some patterns that are typical to model and view model classes, that can benefit from specific unit testing techniques.

提示

对每个单元测试进行测试。Test one thing with each unit test. 不要让单元测试执行单元的行为的多个方面。Don't be tempted to make a unit test exercise more than one aspect of the unit's behavior. 这样做会导致难以读取和更新的测试。Doing so leads to tests that are difficult to read and update. 它还可能在解释失败时导致混淆。It can also lead to confusion when interpreting a failure.

EShopOnContainers 移动应用使用xUnit来执行单元测试,支持两种不同类型的单元测试:The eShopOnContainers mobile app uses xUnit to perform unit testing, which supports two different types of unit tests:

  • 事实是始终为 true 的测试,用于测试固定条件。Facts are tests that are always true, which test invariant conditions.
  • 理论是仅适用于一组特定数据的测试。Theories are tests that are only true for a particular set of data.

EShopOnContainers 移动应用随附的单元测试是事实测试,因此每个单元测试方法都是用[Fact]特性修饰的。The unit tests included with the eShopOnContainers mobile app are fact tests, and so each unit test method is decorated with the [Fact] attribute.

备注

xUnit 测试由测试运行程序执行。xUnit tests are executed by a test runner. 若要执行测试运行程序,请运行所需平台的 eShopOnContainers. TestRunner 项目。To execute the test runner, run the eShopOnContainers.TestRunner project for the required platform.

测试异步功能Testing Asynchronous Functionality

实现 MVVM 模式时,视图模型通常会以异步方式调用服务上的操作。When implementing the MVVM pattern, view models usually invoke operations on services, often asynchronously. 调用这些操作的代码测试通常使用模拟作为实际服务的替换项。Tests for code that invokes these operations typically use mocks as replacements for the actual services. 下面的代码示例演示了如何通过将模拟服务传递到视图模型来测试异步功能:The following code example demonstrates testing asynchronous functionality by passing a mock service into a view model:

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

此单元测试检查Order在调用InitializeAsync方法后OrderDetailViewModel实例的属性是否具有值。This unit test checks that the Order property of the OrderDetailViewModel instance will have a value after the InitializeAsync method has been invoked. 当导航到视图模型的相应视图时,将调用方法。InitializeAsyncThe InitializeAsync method is invoked when the view model's corresponding view is navigated to. 有关导航的详细信息,请参阅导航For more information about navigation, see Navigation.

创建实例时,它OrderService需要将实例指定为参数。 OrderDetailViewModelWhen the OrderDetailViewModel instance is created, it expects an OrderService instance to be specified as an argument. 但是, OrderService从 web 服务检索数据。However, the OrderService retrieves data from a web service. 因此, OrderMockService实例( OrderService该类的模拟版本)被指定OrderDetailViewModel为构造函数的参数。Therefore, an OrderMockService instance, which is a mock version of the OrderService class, is specified as the argument to the OrderDetailViewModel constructor. 然后,调用视图模型的InitializeAsync方法(该方法调用IOrderService操作)时,将检索模拟数据,而不是与 web 服务进行通信。Then, when the view model's InitializeAsync method is invoked, which invokes IOrderService operations, mock data is retrieved rather than communicating with a web service.

测试 INotifyPropertyChanged 实现Testing INotifyPropertyChanged Implementations

通过实现INotifyPropertyChanged接口,视图可以对源自视图模型和模型的更改做出反应。Implementing the INotifyPropertyChanged interface allows views to react to changes that originate from view models and models. 这些更改并不限于控件中显示的数据–它们还用于控制视图,例如,查看模型状态导致动画启动或禁用控件。These changes are not limited to data shown in controls – they are also used to control the view, such as view model states that cause animations to be started or controls to be disabled.

可以通过将事件处理程序附加到PropertyChanged事件,并检查为属性设置新值后是否引发事件,来测试可通过单元测试直接更新的属性。Properties that can be updated directly by the unit test can be tested by attaching an event handler to the PropertyChanged event and checking whether the event is raised after setting a new value for the property. 下面的代码示例演示了此类测试:The following code example shows such a test:

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

此单元测试将调用InitializeAsync OrderViewModel类的方法,这将导致其Order属性被更新。This unit test invokes the InitializeAsync method of the OrderViewModel class, which causes its Order property to be updated. 如果对PropertyChanged Order属性引发了事件,则单元测试将通过。The unit test will pass, provided that the PropertyChanged event is raised for the Order property.

测试基于消息的通信Testing Message-based Communication

如下面的代码示例MessagingCenter中所示,可以通过订阅正在进行的代码所发送的消息,查看使用类在松耦合类之间进行通信的视图模型。View models that use the MessagingCenter class to communicate between loosely-coupled classes can be unit tested by subscribing to the message being sent by the code under test, as demonstrated in the following code example:

[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正在执行。This unit test checks that the CatalogViewModel publishes the AddProduct message in response to its AddCatalogItemCommand being executed. 由于类支持多路广播消息订阅,因此单元测试可以订阅该AddProduct消息并执行回调委托以响应接收它。 MessagingCenterBecause the MessagingCenter class supports multicast message subscriptions, the unit test can subscribe to the AddProduct message and execute a callback delegate in response to receiving it. 指定为 lambda 表达式的此回调委托将设置一个boolean字段,该字段由该Assert语句用来验证测试的行为。This callback delegate, specified as a lambda expression, sets a boolean field that's used by the Assert statement to verify the behavior of the test.

测试异常处理Testing Exception Handling

还可以编写单元测试来检查是否针对无效操作或输入引发了特定异常,如下面的代码示例所示:Unit tests can also be written that check that specific exceptions are thrown for invalid actions or inputs, as demonstrated in the following code example:

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

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

此单元测试将引发异常,因为该ListView控件没有名为OnItemTapped的事件。This unit test will throw an exception, because the ListView control does not have an event named OnItemTapped. 方法是泛型方法,其中T是预期异常的类型。 Assert.Throws<T>The Assert.Throws<T> method is a generic method where T is the type of the expected exception. 传递给Assert.Throws<T>方法的参数是将引发异常的 lambda 表达式。The argument passed to the Assert.Throws<T> method is a lambda expression that will throw the exception. 因此,单元测试将会传递,前提是 lambda 表达式引发ArgumentExceptionTherefore, the unit test will pass provided that the lambda expression throws an ArgumentException.

提示

避免编写检查异常消息字符串的单元测试。Avoid writing unit tests that examine exception message strings. 异常消息字符串可能会随时间而变化,因此依赖于其存在的单元测试被视为脆弱。Exception message strings might change over time, and so unit tests that rely on their presence are regarded as brittle.

测试验证Testing Validation

测试验证实现有两个方面:测试是否正确实现了任何验证规则,并测试ValidatableObject<T>类是否按预期执行。There are two aspects to testing the validation implementation: testing that any validation rules are correctly implemented, and testing that the ValidatableObject<T> class performs as expected.

验证逻辑通常是很简单的测试,因为它通常是一个自包含进程,其中的输出取决于输入。Validation logic is usually simple to test, because it is typically a self-contained process where the output depends on the input. 对于至少具有一个关联的验证规则的每Validate个属性,应在其上调用方法的结果,如以下代码示例所示:There should be tests on the results of invoking the Validate method on each property that has at least one associated validation rule, as demonstrated in the following code example:

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

    bool isValid = mockViewModel.Validate();  

    Assert.True(isValid);  
}

如果ValidatableObject<T> 实例MockViewModel中的两个属性都具有数据,则此单元测试将检查验证是否成功。This unit test checks that validation succeeds when the two ValidatableObject<T> properties in the MockViewModel instance both have data.

除了检查验证是否成功外,验证单元测试还应检查Value每个ValidatableObject<T>实例的、 IsValidErrors属性的值,以验证类是否按预期执行。As well as checking that validation succeeds, validation unit tests should also check the values of the Value, IsValid, and Errors property of each ValidatableObject<T> instance, to verify that the class performs as expected. 下面的代码示例演示了执行此操作的单元测试:The following code example demonstrates a unit test that does this:

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

此单元测试检查Surname在的MockViewModel属性没有任何数据,且每个ValidatableObject<T>实例的ValueIsValidErrors属性设置正确时,验证是否失败。This unit test checks that validation fails when the Surname property of the MockViewModel doesn't have any data, and the Value, IsValid, and Errors property of each ValidatableObject<T> instance are correctly set.

总结Summary

单元测试采用一小部分应用,通常是一种方法,将其与代码的其余部分隔离,并验证它是否按预期方式工作。A unit test takes a small unit of the app, typically a method, isolates it from the remainder of the code, and verifies that it behaves as expected. 其目标是检查每个功能单元是否按预期执行,以便不会在整个应用中传播错误。Its goal is to check that each unit of functionality performs as expected, so that errors don't propagate throughout the app.

可以通过将依赖对象替换为模拟依赖对象的行为的 mock 对象,来隔离受测对象的行为。The behavior of an object under test can be isolated by replacing dependent objects with mock objects that simulate the behavior of the dependent objects. 这样,就可以执行单元测试,而无需的资源(如 web 服务或数据库)。This enables unit tests to be executed without requiring unwieldy resources such as web services, or databases.

测试 MVVM 应用程序的模型和视图模型与测试任何其他类的方式相同,可以使用相同的工具和方法。Testing models and view models from MVVM applications is identical to testing any other classes, and the same tools and techniques can be used.