我曾经写过很多的糟糕的单元测试程序。很多。但我坚持着写,现在我已经喜欢上了些单元测试。我编写单元测试的速度越来越快,当开发完程序,我现在有更多的信心相信它们能按照设计的预期来运行。我不希望我的程序里有bug,很多次,单元测试在很多弱智的小bug上挽救了我。如果我能这样并带来好处,我相信所有的人都应该写单元测试!

  作为一个自由职业者,我经常有机会能看到各种不同的公司内部是如何做开发工作的,我经常吃惊于如此多的公司仍然没有使用测试驱动开发(TDD)。当我问“为什么”,回答通常是归咎于下面的一个或多个常见的错误做法,这些错误是我在实施驱动测试开发中经常遇到的。这样的错误很容易犯,我也是受害者。我曾合作过的很多公司因为这些错误做法而放弃了测试驱动开发,他们会持有这样一种观点:驱动测试开发“增加了不必要的代码维护量”,或“把时间浪费在写测试上是不值得的”。

  人们会很合理的推断出这样的结论:

  写了单元测试但没有起到任何作用,那还不如不写。

  但根据我的经验,我可以很有信心的说:

  单元测试能让我的开发更有效率,让我的代码更有保障。

  带着这样的认识,下面让我们看看一些我遇到过/犯过的常见的在测试驱动开发中的错误做法,以及我从中学到的教训。

  1、不使用模拟框架

  我在驱动测试开发上学到第一件事情是应该在独立的环境中进行测试。这意味着我们需要对测试中所需要的外部依赖条件进行模拟,伪造,或者进行短路,让测试的过程不依赖外部条件。

  假设我们要测试下面这个类中的GetByID方法:

    public class ProductService : IProductService 
    { 
        private readonly IProductRepository _productRepository; 
        
        public ProductService(IProductRepository productRepository) 
        { 
            this._productRepository = productRepository; 
        } 
        
        public Product GetByID(string id) 
        { 
            Product product =  _productRepository.GetByID(id); 
        
            if (product == null) 
            { 
                throw new ProductNotFoundException(); 
            } 
        
            return product; 
        } 
    }

  为了让测试能够进行,我们需要写一个IProductRepository的临时模拟代码,这样ProductService.GetByID能在独立的环境中运行。模拟出的IProductRepository临时接口应该是下面这样:

    [TestMethod] 
    public void GetProductWithValidIDReturnsProduct() 
    { 
        // Arrange
        IProductRepository productRepository = new StubProductRepository(); 
        ProductService productService = new ProductService(productRepository); 
        
        // Act
        Product product = productService.GetByID("spr-product"); 
        
        // Assert
        Assert.IsNotNull(product); 
    } 
        
    public class StubProductRepository : IProductRepository 
    { 
        public Product GetByID(string id) 
        { 
            return new Product() 
            { 
                ID = "spr-product", 
                Name = "Nice Product"
            }; 
        } 
        
        public IEnumerable<Product> GetProducts() 
        { 
            throw new NotImplementedException(); 
        } 
    }