我相信Test #2是更容易理解的,不是吗?而Test #1的可读性不那么强的原因是有太多的创建测试的代码。在Test #2中,我把复杂的构建测试的逻辑提取到了ProductPresenter类里,从而使测试代码可读性更强。

  为了把这个概念说的更清楚,让我们来看看测试中引用的方法:

    public void Initialize() 
    { 
        string productID = View.ProductID; 
        Product product = _productService.GetByID(productID); 
        
        if (product != null) 
        { 
            View.Product = product; 
            View.IsInBasket = _basketService.ProductExists(productID); 
        } 
        else
        { 
           NavigationService.GoTo("/not-found"); 
        } 
    }

  这个方法依赖于View, ProductService, BasketService and NavigationService等类,这些类都要模拟或临时构造出来。当遇到这样有太多的依赖关系时,这种需要写出准备代码的副作用会显现出来,正如上面的例子。

  请注意,这还只是个很保守的例子。更多的我看到的是一个类里有模拟一、二十个依赖的情况。

  下面是我在测试中提取出来的模拟ProductPresenter的MockProductPresenter类:

    public class MockProductPresenter 
    { 
        public IBasketService BasketService { get; set; } 
        public IProductService ProductService { get; set; } 
        public ProductPresenter Presenter { get; private set; } 
        
        public MockProductPresenter(IProductView view) 
        { 
            var productService = Mock.Create<IProductService>(); 
            var navigationService = Mock.Create<INavigationService>(); 
            var basketService = Mock.Create<IBasketService>(); 
        
            // Setup for private methods 
            Mock.Arrange(() => productService.GetByID("spr-product")).Returns(new Product()); 
            Mock.Arrange(() => basketService.ProductExists("spr-product")).Returns(true); 
            Mock.Arrange(() => navigationService.GoTo("/not-found")).OccursOnce(); 
        
            Presenter = new ProductPresenter( 
                                       view, 
                                            navigationService, 
                                            productService, 
                                            basketService); 
        } 
    }

  因为View.ProductID的属性值决定着这个方法的逻辑走向,我们向MockProductPresenter类的构造器里传入了一个模拟的View实例。这种做法保证了当产品ID改变时自动判断需要模拟的依赖。

  我们也可以用这种方法处理测试过程中的细节动作,像我们在第二个单元测试里的Initialize方法里处理product==null的情况:

    [TestMethod] 
    public void InitializeWithInvalidProductIDRedirectsToNotFound() 
    { 
        // Arrange 
        var view = Mock.Create<IProductView>(); 
        Mock.Arrange(() => view.ProductID).Returns("invalid-product"); 
        
        var mock = new MockProductPresenter(view); 
        
        // Act 
        mock.Presenter.Initialize(); 
        
        // Assert 
        Mock.Assert(mock.Presenter.NavigationService); 
    }

  这隐藏了一些ProductPresenter实现上的细节处理,测试方法的可读性是第一重要的。