我写了很多糟糕的单元测试。很多。但我坚持了下来,现在我已经爱上了一些单元测试。我写单元测试的速度越来越快,当我完成程序开发时,我现在更有信心它们会按设计工作。我不希望我的程序出现错误,很多时候,单元测试使我免于遇到最严重的小错误。如果我能做到这一点并带来好处,我相信每个人都应该写单元测试!作为一名自由职业者,我经常有机会看到各个公司内部是如何进行开发的,而且我常常惊讶于有多少公司仍然不使用测试驱动开发(TDD)。当我问“为什么”时,答案通常是归咎于我在实施测试驱动开发时经常遇到的以下一个或多个常见错误。这是一个容易犯的错误,我也是受害者。我工作过的许多公司都因为这些错误而放弃了测试驱动开发,他们认为测试驱动开发“增加了不必要的代码维护”或“浪费时间编写测试”。不值得。”人们会合理地推断,如果你写了单元测试,但他们什么都不做,你还不如不写。但根据我的经验,我可以自信地说:单元测试可以使我的开发更高效,我的代码也更安全。考虑到这一点,让我们看看我遇到/犯过的一些最常见的TDD错误,以及我从中吸取的教训。1.不要使用模拟框架我在驾考开发中学到的第一件事就是测试应该在隔离的环境中进行,这意味着我们需要模拟、伪造或短路测试中需要的外部依赖,这样测试过程就不会取决于外部条件。假设我们要测试下面这种中的GetByID方法:publicclassProductService:IProductService{privatereadonlyIProductRepository_productRepository;publicProductService(IProductRepositoryproductRepository){this._productRepository=productRepository;}publicProductGetByID(stringid){Productproduct=_productRepository.GetByID(id);if(product==null){thrownewProductNotFoundException();}返回产品;为了让测试工作,我们需要编写一个IProductRepository的临时模拟,以便ProductService.GetByID可以在隔离的环境中运行。模拟的IProductRepository临时接口应该是这样的:ProductServiceproductService=newProductService(productRepository);//ActProductproduct=product"sService.GetByID(//AssertAssert.IsNotNull(product);}publicclassStubProductRepository:IProductRepository{publicProductGetByID(stringid){returnnewProduct(){ID="spr-product",Name="NiceProduct"};}publicIEnumerableGetProducts(){throwNotImplemented(Ex);}}现在让我们使用无效的产品ID测试此方法的错误。[TestMethod]publicvoidGetProductWithInValidIDThrowsException(){//安排IProductRepositoryproductRepository=newStubNullProductRepository();ProductServiceproductService=newProductService(productRepository);//Act&AssertAssert.Throws("Get.idval>ByID)-productpublicclassStubNullProductRepository:IProductRepository{publicProductGetByID(stringid){returnnull;}publicIEnumerableGetProducts(){throwNotImplementedException();}}在这个例子中,我们为每个测试创建一个单独的Repository。但是我们也可以在Repository上添加额外的逻辑,例如:-product",Name="NiceProduct"};}returnnull;}publicIEnumerableGetProducts(){thrownewNotImplementedException();}}在第一个方法中,我们写了两个不同的IProductRepositorymock方法,在第二个方法中,我们的逻辑变得有点复杂。如果我们在这些逻辑上出错,那么我们的测试就得不到正确的结果,这就给我们的调试增加了额外的负担。我们需要找出是业务代码错了还是测试代码错了。你可能还会质疑这些mock代码中这个无用的GetProducts()方法,它有什么作用?因为这个方法在IProductRepository接口中,所以我们不得不添加这个方法来让程序编译通过——虽然在我们的测试中这个方法根本不是我们考虑的。使用这样的测试方式,我们不得不编写大量的临时模拟类,这无疑会让我们在维护时更加头疼。这个时候使用一个模拟框架,比如JustMock,会为我们省去很多工作。让我们重温一下之前的测试例子,这次我们将使用一个mock框架:Mock.Arrange(()=>productRepository.GetByID("spr-product")).Returns(newProduct());ProductServiceproductService=newProductService(productRepository);//行为Productproduct=productService.GetByID("spr-product");//断言Assert.IsNotNull(product);}[TestMethod]publicvoidGetProductWithInValidIDThrowsException(){//安排IProductRepositoryproductRepository=Mock.Create();ProductServiceproductService=newProductService(productRepository);//Act&AssertAssert.Throws副产品>(("-id"));注意到我们编写的代码量减少了吗?在此示例中,代码量减少了49%。更准确地说,使用模拟框架时为28行代码,不使用时为57行。我们还看到整个测试方法变得更具可读性!#p#2。过于松散的测试代码组织模拟框架让我们在模拟测试中生成某个依赖类非常简单,但有时容易实现,容易造成危害。为了说明这一点,观察下面两个单元测试,看看哪个更容易理解。这两个测试程序是测试一个相同的功能:Test#1TestMethod]publicvoidInitializeWithValidProductIDReturnsView(){//ArrangeIProductViewproductView=Mock.Create();Mock.Arrange(()=>productView.ProductID).Returns("spr-product");IProductServiceproductService=Mock.Create();Mock.Arrange(()=>productService.GetByID("spr-product")).Returns(newProduct()).OccursOnce();INavigationServicenavigationService=Mock.Create();Mock.Arrange(()=>navigationService.GoTo("/not-found"));IBasketServicebasketService=Mock.Create();Mock.Arrange(()=>basketService.ProductExists("spr-product")).Returns(true);varproductPresenter=newProductPresenter(productView,navigationService,productService,basketService);//行为productPresenter.Initialize();//断言Assert.IsNotNull(productView.Product);Assert.IsTrue(productView.IsInBasket);}测试#2[TestMethod]publicvoidInitializeWithValidProductIDReturnsView(){//安排varview=Mock.Create();Mock.Arrange(()=>view.ProductID).Returns("spr-product");varmock=newMockProductPresenter(视图);//行为mock.Presenter.Initialize();//断言Assert.IsNotNull(mock.Presenter.View.Product);Assert.IsTrue(mock.Presenter.View.IsInBasket);我相信测试#2更容易理解,不是吗?Test#1可读性不强的原因是测试创建代码太多。在测试#2中,我将构建测试的复杂逻辑提取到ProductPresenter类中,以提高测试代码的可读性。强大的。为了更清楚地理解这个概念,让我们看一下测试中引用的方法:publicvoidInitialize(){stringproductID=View.ProductID;Productproduct=_productService.GetByID(productID);if(product!=null){View.Product=product;View.IsInBasket=_basketService.ProductExists(productID);}else{NavigationService.GoTo("/not-found");}}该方法依赖View、ProductService、BasketService、NavigationService等类,必须模拟或临时构建。当有太多这样的依赖项时,需要编写准备好的代码的副作用就会显现出来,如上例所示。请注意,这是一个非常保守的例子。我看多的是一个类里面模拟了一二十个依赖。下面是我在测试中提取出来的模拟ProductPresenter的MockProductPresenter类:>();varnavigationService=Mock.Create();varbasketService=Mock.Create();//设置私有方法Mock.Arrange(()=>productService.GetByID("spr-product")).Returns(newProduct());Mock.Arrange(()=>basketService.ProductExists("spr-product")).Returns(true);Mock.Arrange(()=>navigationService.GoTo("/not-found")).OccursOnce();Presenter=newProductPresenter(view,navigationService,productService,basketService);因为View.ProductID的属性值决定了这个方法的逻辑方向,所以我们传递一个模拟的View实例。这种方式保证了需要模拟的依赖在产品ID变化时自动判断。我们也可以使用这个方法来处理测试过程的细节,就像我们在第二个单元测试的Initialize方法中处理product==null的情况一样:创建();Mock.Arrange(()=>view.ProductID).Returns("无效产品");varmock=newMockProductPresenter(视图);//行为mock.Presenter.Initialize();//断言Mock.Assert(mock.Presenter.NavigationService);这隐藏了ProductPresenter实现的一些细节,测试方法的可读性是最重要的。#p#3.一次测试太多项目看下面的单元测试,请不要使用“和”一词描述它:[TestMethod]publicvoidProductPriceTests(){//Arrangevarproduct=newProduct(){BasePrice=10米};//ActdecimalbasePrice=product.CalculatePrice(CalculationRules.None);decimaldiscountPrice=product.CalculatePrice(CalculationRules.Discounted);decimalstandardPrice=product.CalculatePrice(CalculationRules.Standard);//断言Assert.AreEqual(10m,basePrice);Assert.AreEqual(11m,discountPrice);Assert.AreEqual(12m,standardPrice);}我只能这样描述这个方法:“测试计算底价、折扣价、标准价是否都能返回正确的值。”这是判断您是否一次测试了太多东西的简单方法。上面的测试有三种情况会导致它失败。如果测试失败,我们需要找到什么/出了什么问题。理想情况下,每个方法都应该有自己的测试,例如:[TestMethod]publicvoidCalculateDiscountedPriceReturnsAmountOf11(){//Arrangevarproduct=newProduct(){BasePrice=10m};//ActdecimaldiscountPrice=product.CalculatePrice(CalculationRules.Discounted);//断言Assert.AreEqual(11m,discountPrice);}[TestMethod]publicvoidCalculateStandardPriceReturnsAmountOf12(){//安排varproduct=newProduct(){BasePrice=10m};//ActdecimalstandardPrice=product.CalculatePrice(CalculationRules.Standard);//断言Assert.AreEqual(12m,standardPrice);}[TestMethod]publicvoidNoDiscountRuleReturnsBasePrice(){//安排varproduct=newProduct(){BasePrice=10m};//ActdecimalbasePrice=product.CalculatePrice(CalculationRules.None);//断言Assert.AreEqual(10m,basePrice);注意非常具有描述性的测试名称。如果一个项目中有500个测试,其中一个失败了,您可以仅通过名称来判断是哪个测试对它负责。这样我们可能会有更多的方法,但是换来的好处是清晰。我在《代码大全(第2版)》看到了这个经验法则:为方法中的每一个IF、And、Or、Case、For、While等条件写一个独立的测试方法。驱动测试开发纯粹主义者可能会说每个测试中应该只有一个断言。这个原则我觉得有时候可以灵活处理,比如在测试一个对象的属性值时像下面这样:};退回产品;我认为没有必要为每个要断言的属性编写单独的测试方法。下面是我如何编写测试方法:[TestMethod]publicvoidProductMapperMapsToExpectedProperties(){//Arrangevarmapper=newProductMapper();varproductDto=newProductDto(){ID="sp-001",Price=10m,ProductName="SuperProduct"};//行为Productproduct=mapper.Map(productDto);//断言Assert.AreEqual(10m,product.BasePrice);Assert.AreEqual("sp-001",product.ID);Assert.AreEqual("超级产品",product.Name);}#p#4。先写程序再写测试。我坚持认为驾考发展的意义远高于考试本身。正确实施测试驱动开发可以大大提高开发效率,这是一种良性循环。我看到很多开发人员在开发某个功能后编写测试方法,将其视为提交代码之前需要完成的行政命令。事实上,重写测试代码只是驱动测试开发的一个方面。如果不按照先写测试再写被测程序的红、绿、重构方法原则,写测试很可能成为一种体力劳动。如果你想养成单元测试的习惯,你可以阅读一些关于TDD的资料,比如TheStringCalculatorCodeKata。5.请检查以下方法进行详细测试:publicProductGetByID(stringid){return_productRepository.GetByID(id);这个方法真的需要测试吗?不,我也不这么认为。驱动测试纯粹主义者可能坚持认为所有代码都应该被测试覆盖,并且有自动工具可以扫描并报告程序的某些部分未被测试覆盖,但是,我们必须小心不要落入这个陷阱。创建您自己的工作量陷阱。与我交谈过的许多反对测试驱动开发的人都将此作为不编写任何测试代码的主要原因。我给他们的回复是:只测试你需要测试的代码。我的观点是constructors,geters,setters等不需要专门测试。加深一下我前面提到的经验主义的记忆:为方法中的每一个IF、And、Or、Case、For、While等条件写一个独立的测试方法。如果一个方法没有上面提到的任何条件语句,它真的需要测试吗?测试愉快!获取文中代码您可以在此处找到文中示例的代码。英文原文:Top5TDDMistakes翻译链接:http://www.aqee.net/top-5-tdd-mistakes/