自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

測(cè)試驅(qū)動(dòng)開發(fā)上的五大錯(cuò)誤

開發(fā) 測(cè)試
作為一個(gè)自由職業(yè)者,我經(jīng)常有機(jī)會(huì)能看到各種不同的公司內(nèi)部是如何做開發(fā)工作的,我經(jīng)常吃驚于如此多的公司仍然沒有使用測(cè)試驅(qū)動(dòng)開發(fā)(TDD)。當(dāng)我問“為什么”,回答通常是歸咎于下面的一個(gè)或多個(gè)常見的錯(cuò)誤做法,這些錯(cuò)誤是我在實(shí)施驅(qū)動(dòng)測(cè)試開發(fā)中經(jīng)常遇到的。這樣的錯(cuò)誤很容易犯,我也是受害者。

測(cè)試驅(qū)動(dòng)開發(fā)

我曾經(jīng)寫過很多的糟糕的單元測(cè)試程序。很多。但我堅(jiān)持著寫,現(xiàn)在我已經(jīng)喜歡上了些單元測(cè)試。我編寫單元測(cè)試的速度越來越快,當(dāng)開發(fā)完程序,我現(xiàn)在有更多的信心相信它們能按照設(shè)計(jì)的預(yù)期來運(yùn)行。我不希望我的程序里有bug,很多次,單元測(cè)試在很多***的小bug上挽救了我。如果我能這樣并帶來好處,我相信所有的人都應(yīng)該寫單元測(cè)試!

作為一個(gè)自由職業(yè)者,我經(jīng)常有機(jī)會(huì)能看到各種不同的公司內(nèi)部是如何做開發(fā)工作的,我經(jīng)常吃驚于如此多的公司仍然沒有使用測(cè)試驅(qū)動(dòng)開發(fā)(TDD)。當(dāng)我問“為什么”,回答通常是歸咎于下面的一個(gè)或多個(gè)常見的錯(cuò)誤做法,這些錯(cuò)誤是我在實(shí)施驅(qū)動(dòng)測(cè)試開發(fā)中經(jīng)常遇到的。這樣的錯(cuò)誤很容易犯,我也是受害者。我曾合作過的很多公司因?yàn)檫@些錯(cuò)誤做法而放棄了測(cè)試驅(qū)動(dòng)開發(fā),他們會(huì)持有這樣一種觀點(diǎn):驅(qū)動(dòng)測(cè)試開發(fā)“增加了不必要的代碼維護(hù)量”,或“把時(shí)間浪費(fèi)在寫測(cè)試上是不值得的”。

人們會(huì)很合理的推斷出這樣的結(jié)論:

寫了單元測(cè)試但沒有起到任何作用,那還不如不寫。

但根據(jù)我的經(jīng)驗(yàn),我可以很有信心的說:

單元測(cè)試能讓我的開發(fā)更有效率,讓我的代碼更有保障。

帶著這樣的認(rèn)識(shí),下面讓我們看看一些我遇到過/犯過的最常見的在測(cè)試驅(qū)動(dòng)開發(fā)中的錯(cuò)誤做法,以及我從中學(xué)到的教訓(xùn)。

1、不使用模擬框架

我在驅(qū)動(dòng)測(cè)試開發(fā)上學(xué)到***件事情就是應(yīng)該在獨(dú)立的環(huán)境中進(jìn)行測(cè)試。這意味著我們需要對(duì)測(cè)試中所需要的外部依賴條件進(jìn)行模擬,偽造,或者進(jìn)行短路,讓測(cè)試的過程不依賴外部條件。

假設(shè)我們要測(cè)試下面這個(gè)類中的GetByID方法:

  1. public class ProductService : IProductService  
  2. {  
  3.     private readonly IProductRepository _productRepository;  
  4.      
  5.     public ProductService(IProductRepository productRepository)  
  6.     {  
  7.         this._productRepository = productRepository;  
  8.     }  
  9.      
  10.     public Product GetByID(string id)  
  11.     {  
  12.         Product product =  _productRepository.GetByID(id);  
  13.      
  14.         if (product == null)  
  15.         {  
  16.             throw new ProductNotFoundException();  
  17.         }  
  18.      
  19.         return product;  
  20.     }  

為了讓測(cè)試能夠進(jìn)行,我們需要寫一個(gè)IProductRepository的臨時(shí)模擬代碼,這樣ProductService.GetByID就能在獨(dú)立的環(huán)境中運(yùn)行。模擬出的IProductRepository臨時(shí)接口應(yīng)該是下面這樣:

  1. [TestMethod]  
  2. public void GetProductWithValidIDReturnsProduct()  
  3. {  
  4.     // Arrange  
  5.     IProductRepository productRepository = new StubProductRepository();  
  6.     ProductService productService = new ProductService(productRepository);  
  7.      
  8.     // Act  
  9.     Product product = productService.GetByID("spr-product");  
  10.      
  11.     // Assert  
  12.     Assert.IsNotNull(product);  
  13. }  
  14.      
  15. public class StubProductRepository : IProductRepository  
  16. {  
  17.     public Product GetByID(string id)  
  18.     {  
  19.         return new Product()  
  20.         {  
  21.             ID = "spr-product",  
  22.             Name = "Nice Product" 
  23.         };  
  24.     }  
  25.      
  26.     public IEnumerable<Product> GetProducts()  
  27.     {  
  28.         throw new NotImplementedException();  
  29.     }  

現(xiàn)在讓我們用一個(gè)無效的產(chǎn)品ID來測(cè)試這個(gè)方法的報(bào)錯(cuò)效果。

  1. [TestMethod]  
  2. public void GetProductWithInValidIDThrowsException()  
  3. {  
  4.     // Arrange  
  5.     IProductRepository productRepository = new StubNullProductRepository();  
  6.     ProductService productService = new ProductService(productRepository);  
  7.      
  8.     // Act & Assert  
  9.     Assert.Throws<ProductNotFoundException>(() => productService.GetByID("invalid-id"));  
  10. }  
  11.      
  12. public class StubNullProductRepository : IProductRepository  
  13. {  
  14.     public Product GetByID(string id)  
  15.     {  
  16.         return null;  
  17.     }  
  18.      
  19.     public IEnumerable<Product> GetProducts()  
  20.     {  
  21.         throw new NotImplementedException();  
  22.     }  

在這個(gè)例子中,我們?yōu)槊總€(gè)測(cè)試都做了一個(gè)獨(dú)立的Repository。但我們也可在一個(gè)Repository上添加額外的邏輯,例如:

  1. public class StubProductRepository : IProductRepository   
  2. {   
  3.     public Product GetByID(string id)   
  4.     {   
  5.         if (id == "spr-product")   
  6.         {   
  7.             return new Product()   
  8.             {   
  9.                 ID = "spr-product",   
  10.                 Name = "Nice Product" 
  11.             };   
  12.         }   
  13.      
  14.         return null;   
  15.     }   
  16.      
  17.     public IEnumerable<Product> GetProducts()   
  18.     {   
  19.         throw new NotImplementedException();   
  20.     }   

在***種方法里,我們寫了兩個(gè)不同的IProductRepository模擬方法,而在第二種方法里,我們的邏輯變得有些復(fù)雜。如果我們?cè)谶@些邏輯中犯了錯(cuò),那我們的測(cè)試就沒法得到正確的結(jié)果,這又為我們的調(diào)試增加了額外的負(fù)擔(dān),我們需要找到是業(yè)務(wù)代碼出來錯(cuò)還是測(cè)試代碼不正確。

你也許還會(huì)質(zhì)疑這些模擬代碼中的這個(gè)沒有任何用處的 GetProducts()方法,它是干什么的?因?yàn)镮ProductRepository接口里有這個(gè)方法,我們不得不加入這個(gè)方法以讓程序能編譯通過——盡管在我們的測(cè)試中這個(gè)方法根本不是我們考慮到對(duì)象。

使用這樣的測(cè)試方法,我們不得不寫出大量的臨時(shí)模擬類,這無疑會(huì)讓我們?cè)诰S護(hù)時(shí)愈加頭痛。這種時(shí)候,使用一個(gè)模擬框架,比如JustMock,將會(huì)節(jié)省我們大量的工作。

讓我們重新看一下之前的這個(gè)測(cè)試?yán)?,這次我們將使用一個(gè)模擬框架:

  1. [TestMethod]  
  2. public void GetProductWithValidIDReturnsProduct()  
  3. {  
  4.     // Arrange  
  5.     IProductRepository productRepository = Mock.Create<IProductRepository>();  
  6.     Mock.Arrange(() => productRepository.GetByID("spr-product")).Returns(new Product());  
  7.     ProductService productService = new ProductService(productRepository);  
  8.      
  9.     // Act  
  10.     Product product = productService.GetByID("spr-product");  
  11.      
  12.     // Assert  
  13.     Assert.IsNotNull(product);  
  14. }  
  15.      
  16. [TestMethod]  
  17. public void GetProductWithInValidIDThrowsException()  
  18. {  
  19.     // Arrange  
  20.     IProductRepository productRepository = Mock.Create<IProductRepository>();  
  21.     ProductService productService = new ProductService(productRepository);  
  22.      
  23.     // Act & Assert  
  24.     Assert.Throws<ProductNotFoundException>(() => productService.GetByID("invalid-id"));  

有沒有注意到我們寫的代碼的減少量?在這個(gè)例子中代碼量減少49%,更準(zhǔn)確的說,使用模擬框架測(cè)試時(shí)代碼是28行,而沒有使用時(shí)是57行。我們還看到了整個(gè)測(cè)試方法變得可讀性更強(qiáng)了!

#p#

2、測(cè)試代碼組織的太松散

模擬框架讓我們?cè)谀M測(cè)試中的生成某個(gè)依賴類的工作變得非常簡(jiǎn)單,但有時(shí)候太輕易實(shí)現(xiàn)也容易產(chǎn)生壞處。為了說明這個(gè)觀點(diǎn),請(qǐng)觀察下面兩個(gè)單元測(cè)試,看看那一個(gè)容易理解。這兩個(gè)測(cè)試程序是測(cè)試一個(gè)相同的功能:

Test #1

  1. TestMethod]  
  2. public void InitializeWithValidProductIDReturnsView()  
  3. {  
  4.     // Arrange  
  5.     IProductView productView = Mock.Create<IProductView>();  
  6.     Mock.Arrange(() => productView.ProductID).Returns("spr-product");  
  7.      
  8.     IProductService productService = Mock.Create<IProductService>();  
  9.     Mock.Arrange(() => productService.GetByID("spr-product")).Returns(new Product()).OccursOnce();  
  10.      
  11.     INavigationService navigationService = Mock.Create<INavigationService>();  
  12.     Mock.Arrange(() => navigationService.GoTo("/not-found"));  
  13.      
  14.     IBasketService basketService = Mock.Create<IBasketService>();  
  15.     Mock.Arrange(() => basketService.ProductExists("spr-product")).Returns(true);  
  16.          
  17.     var productPresenter = new ProductPresenter(  
  18.                                             productView,  
  19.                                             navigationService,  
  20.                                             productService,   
  21.                                             basketService);  
  22.      
  23.     // Act  
  24.     productPresenter.Initialize();  
  25.      
  26.     // Assert  
  27.     Assert.IsNotNull(productView.Product);  
  28.     Assert.IsTrue(productView.IsInBasket);  

Test #2

  1. [TestMethod]  
  2. public void InitializeWithValidProductIDReturnsView()  
  3. {  
  4.     // Arrange     
  5.     var view = Mock.Create<IProductView>();  
  6.     Mock.Arrange(() => view.ProductID).Returns("spr-product");  
  7.      
  8.     var mock = new MockProductPresenter(view);  
  9.      
  10.     // Act  
  11.     mock.Presenter.Initialize();  
  12.      
  13.     // Assert  
  14.     Assert.IsNotNull(mock.Presenter.View.Product);  
  15.     Assert.IsTrue(mock.Presenter.View.IsInBasket);  

我相信Test #2是更容易理解的,不是嗎?而Test #1的可讀性不那么強(qiáng)的原因就是有太多的創(chuàng)建測(cè)試的代碼。在Test #2中,我把復(fù)雜的構(gòu)建測(cè)試的邏輯提取到了ProductPresenter類里,從而使測(cè)試代碼可讀性更強(qiáng)。

為了把這個(gè)概念說的更清楚,讓我們來看看測(cè)試中引用的方法:

  1. public void Initialize()  
  2. {  
  3.     string productID = View.ProductID;  
  4.     Product product = _productService.GetByID(productID);  
  5.      
  6.     if (product != null)  
  7.     {  
  8.         View.Product = product;  
  9.         View.IsInBasket = _basketService.ProductExists(productID);  
  10.     }  
  11.     else 
  12.     {  
  13.        NavigationService.GoTo("/not-found");  
  14.     }  

這個(gè)方法依賴于View, ProductService, BasketService and NavigationService等類,這些類都要模擬或臨時(shí)構(gòu)造出來。當(dāng)遇到這樣有太多的依賴關(guān)系時(shí),這種需要寫出準(zhǔn)備代碼的副作用就會(huì)顯現(xiàn)出來,正如上面的例子。

請(qǐng)注意,這還只是個(gè)很保守的例子。更多的我看到的是一個(gè)類里有模擬一、二十個(gè)依賴的情況。

下面就是我在測(cè)試中提取出來的模擬ProductPresenter的MockProductPresenter類:

  1. public class MockProductPresenter  
  2. {  
  3.     public IBasketService BasketService { get; set; }  
  4.     public IProductService ProductService { get; set; }  
  5.     public ProductPresenter Presenter { get; private set; }  
  6.      
  7.     public MockProductPresenter(IProductView view)  
  8.     {  
  9.         var productService = Mock.Create<IProductService>();  
  10.         var navigationService = Mock.Create<INavigationService>();  
  11.         var basketService = Mock.Create<IBasketService>();  
  12.      
  13.         // Setup for private methods  
  14.         Mock.Arrange(() => productService.GetByID("spr-product")).Returns(new Product());  
  15.         Mock.Arrange(() => basketService.ProductExists("spr-product")).Returns(true);  
  16.         Mock.Arrange(() => navigationService.GoTo("/not-found")).OccursOnce();  
  17.      
  18.         Presenter = new ProductPresenter(  
  19.                                    view,  
  20.                                         navigationService,  
  21.                                         productService,  
  22.                                         basketService);  
  23.     }  

因?yàn)閂iew.ProductID的屬性值決定著這個(gè)方法的邏輯走向,我們向MockProductPresenter類的構(gòu)造器里傳入了一個(gè)模擬的View實(shí)例。這種做法保證了當(dāng)產(chǎn)品ID改變時(shí)自動(dòng)判斷需要模擬的依賴。

我們也可以用這種方法處理測(cè)試過程中的細(xì)節(jié)動(dòng)作,就像我們?cè)诘诙€(gè)單元測(cè)試?yán)锏腎nitialize方法里處理product==null的情況:

  1. [TestMethod]  
  2. public void InitializeWithInvalidProductIDRedirectsToNotFound()  
  3. {  
  4.     // Arrange  
  5.     var view = Mock.Create<IProductView>();  
  6.     Mock.Arrange(() => view.ProductID).Returns("invalid-product");  
  7.      
  8.     var mock = new MockProductPresenter(view);  
  9.      
  10.     // Act  
  11.     mock.Presenter.Initialize();  
  12.      
  13.     // Assert  
  14.     Mock.Assert(mock.Presenter.NavigationService);  

這隱藏了一些ProductPresenter實(shí)現(xiàn)上的細(xì)節(jié)處理,測(cè)試方法的可讀性是***重要的。

#p#

3、一次測(cè)試太多的項(xiàng)目

看看下面的單元測(cè)試,請(qǐng)?jiān)诓皇褂?ldquo;和”這個(gè)詞的情況下描述它:

  1. [TestMethod]  
  2. public void ProductPriceTests()  
  3. {  
  4.     // Arrange  
  5.     var product = new Product()  
  6.     {  
  7.         BasePrice = 10m  
  8.     };  
  9.      
  10.     // Act  
  11.     decimal basePrice = product.CalculatePrice(CalculationRules.None);  
  12.     decimal discountPrice = product.CalculatePrice(CalculationRules.Discounted);  
  13.     decimal standardPrice = product.CalculatePrice(CalculationRules.Standard);  
  14.      
  15.     // Assert  
  16.     Assert.AreEqual(10m, basePrice);  
  17.     Assert.AreEqual(11m, discountPrice);  
  18.     Assert.AreEqual(12m, standardPrice);  

我只能這樣描述這個(gè)方法:

“測(cè)試中計(jì)算基價(jià),打折價(jià)和標(biāo)準(zhǔn)價(jià)是都能否返回正確的值。”

這是一個(gè)簡(jiǎn)單的方法來判斷你是否一次測(cè)試了過多的內(nèi)容。上面這個(gè)測(cè)試會(huì)有三種情況導(dǎo)致它失敗。如果測(cè)試失敗,我們需要去找到那個(gè)/哪些出了錯(cuò)。

理想情況下,每一個(gè)方法都應(yīng)該有它自己的測(cè)試,例如:

  1. [TestMethod]  
  2. public void CalculateDiscountedPriceReturnsAmountOf11()  
  3. {  
  4.     // Arrange  
  5.     var product = new Product()  
  6.     {  
  7.         BasePrice = 10m  
  8.     };  
  9.      
  10.     // Act  
  11.     decimal discountPrice = product.CalculatePrice(CalculationRules.Discounted);  
  12.      
  13.     // Assert  
  14.     Assert.AreEqual(11m, discountPrice);  
  15. }  
  16.      
  17. [TestMethod]  
  18. public void CalculateStandardPriceReturnsAmountOf12()  
  19. {  
  20.     // Arrange  
  21.     var product = new Product()  
  22.     {  
  23.         BasePrice = 10m  
  24.     };  
  25.      
  26.     // Act  
  27.     decimal standardPrice = product.CalculatePrice(CalculationRules.Standard);  
  28.      
  29.     // Assert  
  30.     Assert.AreEqual(12m, standardPrice);  
  31. }  
  32.      
  33. [TestMethod]  
  34. public void NoDiscountRuleReturnsBasePrice()  
  35. {  
  36.     // Arrange  
  37.     var product = new Product()  
  38.     {  
  39.         BasePrice = 10m  
  40.     };  
  41.      
  42.     // Act  
  43.     decimal basePrice = product.CalculatePrice(CalculationRules.None);  
  44.      
  45.     // Assert  
  46.     Assert.AreEqual(10m, basePrice);  

注意這些非常具有描述性的測(cè)試名稱。如果一個(gè)項(xiàng)目里有500個(gè)測(cè)試,其中一個(gè)失敗了,你能根據(jù)名稱就能知道哪個(gè)測(cè)試應(yīng)該為此承擔(dān)責(zé)任。

這樣我們可能會(huì)有更多的方法,但換來的好處是清晰。我在《代碼大全(第2版)》里看到了這句經(jīng)驗(yàn)之談:

為方法里的每個(gè)IF,And,Or,Case,F(xiàn)or,While等條件寫出獨(dú)立的測(cè)試方法。

驅(qū)動(dòng)測(cè)試開發(fā)純粹主義者可能會(huì)說每個(gè)測(cè)試?yán)镏粦?yīng)該有一個(gè)斷言。我想這個(gè)原則有時(shí)候可以靈活處理,就像下面測(cè)試一個(gè)對(duì)象的屬性值時(shí):

  1. public Product Map(ProductDto productDto)  
  2. {  
  3.     var product = new Product()  
  4.     {   
  5.         ID = productDto.ID,  
  6.         Name = productDto.ProductName,  
  7.         BasePrice = productDto.Price  
  8.     };  
  9.      
  10.     return product;  
  11. }  

我不認(rèn)為為每個(gè)屬性寫一個(gè)獨(dú)立的測(cè)試方法進(jìn)行斷言是有必要的。下面是我如何寫這個(gè)測(cè)試方法的:

  1. [TestMethod]  
  2. public void ProductMapperMapsToExpectedProperties()  
  3. {  
  4.     // Arrange  
  5.     var mapper = new ProductMapper();  
  6.     var productDto = new ProductDto()  
  7.     {  
  8.         ID = "sp-001",  
  9.         Price = 10m,  
  10.         ProductName = "Super Product" 
  11.     };  
  12.      
  13.     // Act  
  14.     Product product = mapper.Map(productDto);  
  15.      
  16.     // Assert  
  17.     Assert.AreEqual(10m, product.BasePrice);  
  18.     Assert.AreEqual("sp-001", product.ID);  
  19.     Assert.AreEqual("Super Product", product.Name);  

#p#

4、先寫程序后寫測(cè)試

我堅(jiān)持認(rèn)為,驅(qū)動(dòng)測(cè)試開發(fā)的意義遠(yuǎn)高于測(cè)試本身。正確的實(shí)施驅(qū)動(dòng)測(cè)試開發(fā)能巨大的提高開發(fā)效率,這是一種良性循環(huán)。我看到很多開發(fā)人員在開發(fā)完某個(gè)功能后才去寫測(cè)試方法,把這當(dāng)成一種在提交代碼前需要完成的行政命令來執(zhí)行。事實(shí)上,補(bǔ)寫測(cè)試代碼只是驅(qū)動(dòng)測(cè)試開發(fā)的一個(gè)內(nèi)容。

如果不是按照先寫測(cè)試后寫被測(cè)試程序的紅,綠,重構(gòu)方法原則,測(cè)試編寫很可能會(huì)變成一種體力勞動(dòng)。

如果想培養(yǎng)你的單元測(cè)試習(xí)慣,你可以看一些關(guān)于TDD的材料,比如The String Calculator Code Kata。

5、測(cè)試的過細(xì)

請(qǐng)檢查下面的這個(gè)方法:

  1. public Product GetByID(string id)  
  2. {  
  3.     return _productRepository.GetByID(id);  

這個(gè)方法真的需要測(cè)試嗎?不,我也認(rèn)為不需要。

驅(qū)動(dòng)測(cè)試純粹主義者可能會(huì)堅(jiān)持認(rèn)為所有的代碼都應(yīng)該被測(cè)試覆蓋,而且有這樣的自動(dòng)化工具能掃描并報(bào)告程序的某部分內(nèi)容沒有被測(cè)試覆蓋,然而,我們要當(dāng)心,不要落入這種給自己制造工作量的陷阱。

很多我交談過的反對(duì)驅(qū)動(dòng)測(cè)試開發(fā)的人都會(huì)引用這點(diǎn)來作為不寫任何測(cè)試代碼的主要理由。我對(duì)他們的回復(fù)是:只測(cè)試你需要測(cè)試的代碼。我的觀點(diǎn)是,構(gòu)造器,geter,setter等方法沒必要特意的測(cè)試。讓我們來加深記憶一下我前面提到的經(jīng)驗(yàn)論:

為方法里的每個(gè)IF,And,Or,Case,F(xiàn)or,While等條件寫出獨(dú)立的測(cè)試方法。

如果一個(gè)方法里沒有任何一個(gè)上面提到的條件語句,那它真的需要測(cè)試嗎?

祝測(cè)試愉快!

獲取文中的代碼

文中例子的代碼你可以從這里找到。

英文原文:Top 5 TDD Mistakes

譯文鏈接:http://www.aqee.net/top-5-tdd-mistakes/

責(zé)任編輯:林師授 來源: 外刊IT評(píng)論
相關(guān)推薦

2024-07-16 08:00:00

Kubernetes開發(fā)

2012-12-18 10:09:26

虛擬化應(yīng)用錯(cuò)誤

2012-08-02 09:05:59

移動(dòng)應(yīng)用設(shè)計(jì)錯(cuò)誤

2009-09-14 19:23:45

敏捷開發(fā)

2010-07-21 08:51:26

Perl錯(cuò)誤

2012-02-20 16:45:40

Android開發(fā)新手

2022-03-15 14:55:34

Kubernetes

2017-12-27 11:48:57

IT管理數(shù)據(jù)中心錯(cuò)誤

2019-03-27 08:27:32

物聯(lián)網(wǎng)IOT技術(shù)

2011-07-28 09:42:14

IT安全虛擬化數(shù)據(jù)泄漏

2023-03-10 09:00:49

Swift開發(fā)者工具

2023-09-12 09:47:38

云計(jì)算云管理

2013-08-06 14:20:51

Web

2019-06-04 10:40:07

2009-03-05 09:21:04

敏捷開發(fā)XP開源

2015-01-14 09:29:35

2024-01-03 07:52:10

趨勢(shì)云測(cè)試驅(qū)動(dòng)

2022-01-24 08:00:00

元宇宙數(shù)字環(huán)境技術(shù)

2011-07-27 10:37:28

IT安全安全錯(cuò)誤

2019-08-22 10:24:04

物聯(lián)網(wǎng)物聯(lián)網(wǎng)安全IoT
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)