移动应用程序具有独特的问题,桌面和基于Web的应用程序不必担心。移动用户将因其使用的设备,网络连接,服务可用性以及一系列其他因素而有所不同。因此,应该测试移动应用程序,因为它们将被用于现实世界,以提高其质量,可靠性和性能。应用程序应该执行许多类型的测试,包括单元测试,集成测试和用户界面测试,单元测试是最常见的测试形式。
单元测试需要应用程序的一小部分,通常是一种方法,将其与代码的其余部分隔离,并验证其是否符合预期。其目标是检查每个功能单元是否按预期执行,以使整个应用程序不会传播错误。检测出错的地方更有效地在二级故障点间接观察到错误的影响。
当它是软件开发工作流程的组成部分时,单元测试对代码质量的影响最大。一旦编写了一个方法,单元测试应该被写入,以响应标准,边界和不正确的输入数据情况来验证方法的行为,并且检查代码所做的任何显式或隐含的假设。或者,使用测试驱动开发,单元测试是在代码之前编写的。在这种情况下,单元测试既可以作为设计文档和功能规范。
注意:单元测试对于回归是非常有效的 - 也就是说,以前工作但已被错误更新打扰的功能。
单元测试通常使用排列动作断言模式:
· 单元测试方法的排列部分初始化对象并设置传递给被测方法的数据的值。
· act部分使用所需的参数调用被测方法。
· 断言部分验证被测方法的动作是否符合预期。
遵循此模式可确保单元测试可读和一致。
依赖注入和单元测试
采用松散耦合架构的动机之一是它有助于单元测试。 Autofac注册的类型之一是OrderService类。 以下代码示例显示了此类的大纲:
点击(此处)折叠或打开
- public class OrderDetailViewModel : ViewModelBase
- {
- private IOrderService _ordersService;
-
- public OrderDetailViewModel(IOrderService ordersService)
- {
- _ordersService = ordersService;
- }
- ...
- }
OrderDetailViewModel类具有对IOrderService类型的依赖关系,当容器实例化一个OrderDetailViewModel对象时,该容器将被解析。 但是,而不是创建一个OrderService对象来单元测试OrderDetailViewModel类,而不是为了测试而将OrderService对象替换为模拟。 图10-1说明了这种关系。
图10-1:实现IOrderService接口的类
此方法允许在运行时将OrderService对象传递到OrderDetailViewModel类中,为了可测试性,它允许在测试时将OrderMockService类传递到OrderDetailViewModel类中。 这种方法的主要优点是它可以执行单元测试,而不需要诸如Web服务或数据库之类的笨重资源。
测试MVVM应用程序
从MVVM应用程序中测试模型和查看模型与测试任何其他类相同,可以使用相同的工具和技术(如单元测试和模拟)。然而,有一些典型的模型和模型类的模式,可以从特定的单元测试技术中受益。
? 提示:每个单元测试测试一件事。不要试图对单位的行为进行单元测试。这样做会导致难以阅读和更新的测试。解释失败时也可能导致混乱。
eShopOnContainers手机应用程序使用xUnit执行单元测试,它支持两种不同类型的单元测试:
· 事实是总是真实的测试,它测试不变的条件。
· 理论是仅针对特定数据集的测试。
eShopOnContainers手机应用程序附带的单元测试是事实测试,因此每个单元测试方法都使用[Fact]属性进行装饰。
注意:xUnit测试由测试运行器执行。要执行测试运行器,请为所需的平台运行eShopOnContainers.TestRunner项目。
测试异步功能
在实现MVVM模式时,视图模型通常会以异步方式调用服务操作。 调用这些操作的代码的测试通常使用mock作为实际服务的替换。 以下代码示例演示了通过将模拟服务传递到视图模型中来测试异步功能:
点击(此处)折叠或打开
- [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属性被更新。 单元测试将通过,前提是PropertyChanged事件为Order属性生成。
测试基于消息的通信
使用MessagingCenter类在松散耦合类之间通信的查看模型可以通过订阅被测试代码发送的消息进行单元测试,如以下代码示例所示:
点击(此处)折叠或打开
- [Fact]
- public void AddCatalogItemCommandSendsAddProductMessageTest()
- {
- bool messageReceived = false;
- var catalogService = new CatalogMockService();
- var catalogViewModel = new CatalogViewModel(catalogService);
-
- Xamarin.Forms.MessagingCenter.SubscribeCatalogViewModel, CatalogItem>(
- this, MessageKeys.AddProduct, (sender, arg) =>
- {
- messageReceived = true;
- });
- catalogViewModel.AddCatalogItemCommand.Execute(null);
-
- Assert.True(messageReceived);
- }
此单元测试检查CatalogViewModel是否发布AddProduct消息以响应其AddCatalogItemCommand被执行。 因为MessagingCenter类支持多播消息订阅,所以单元测试可以订阅AddProduct消息并执行回调委托以响应接收它。 此回调委托,指定为lambda表达式,设置Assert语句使用的布尔字段,以验证测试的行为。
测试异常处理
也可以写出单元测试,检查是否针对无效操作或输入引发特定异常,如以下代码示例所示:
点击(此处)折叠或打开
- [Fact]
- public void InvalidEventNameShouldThrowArgumentExceptionText()
- {
- var behavior = new MockEventToCommandBehavior
- {
- EventName = "OnItemTapped"
- };
- var listView = new ListView();
-
- Assert.ThrowsArgumentException>(() => listView.Behaviors.Add(behavior));
- }
此单元测试将抛出异常,因为ListView控件没有名为OnItemTapped的事件。 Assert.Throws 方法是一种通用方法,其中T是预期异常的类型。 传递给Assert.Throws 方法的参数是一个将表示异常的lambda表达式。 因此,单元测试将通过,只要lambda表达式抛出一个ArgumentException。
? 提示:避免编写检查异常消息字符串的单元测试。 异常消息字符串可能随时间而变化,因此依赖于它们的存在的单元测试被认为是脆弱的。
测试验证
测试验证实现有两个方面:测试任何验证规则是否正确实现,并测试ValidatableObject 类按预期执行。
验证逻辑通常很容易测试,因为它通常是一个独立的过程,其中输出取决于输入。 对于具有至少一个关联验证规则的每个属性,调用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 属性都具有数据时,此单元测试会检查验证是否成功。
除了检查验证成功之外,验证单元测试还应检查每个ValidatableObject 实例的Value,IsValid和Errors属性的值,以验证该类是否按预期执行。 以下代码示例演示了执行此操作的单元测试:
点击(此处)折叠或打开
- [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);
- }
当MockViewModel的Surname属性没有任何数据,并且正确设置每个ValidatableObject 实例的Value,IsValid和Errors属性时,此单元测试将检查验证失败。
概要
单元测试需要应用程序的一小部分,通常是一种方法,将其与代码的其余部分隔离,并验证其是否符合预期。 其目标是检查每个功能单元是否按预期执行,以使整个应用程序不会传播错误。
可以通过用模拟依赖对象行为的模拟对象替换依赖对象来隔离被测对象的行为。 这样可以在不需要诸如Web服务或数据库等笨重资源的情况下执行单元测试。
从MVVM应用程序测试模型和查看模型与测试任何其他类相同,可以使用相同的工具和技术。