测试双打简介
在編寫單元測試時(shí),您會遇到許多協(xié)作者,并且他們都有非常特殊的行為,知道在正確的時(shí)間必須使用哪種測試兩倍可以使您的生活更輕松。
假
第一個(gè)是Dummy對象,它是最簡單的一個(gè),Dummy只是您為滿足構(gòu)造函數(shù)而傳遞的對象,它不會實(shí)現(xiàn)任何方法,也不會實(shí)現(xiàn)。
在測試課程時(shí),我們不想使用記錄器做任何事情,那么我們該怎么辦?
例如,有一個(gè)帶有記錄器的PaymentService :
public interface Logger { void append(String text); } public class PaymentService { private Logger logger; public PaymentService(Logger logger) { this .logger = logger; } public PaymentRequest createPaymentRequest(Sale sale, CreditCard creditCard) { logger.append( "Creating payment for sale " + sale.toString()); throw new UnsupportedOperationException(); } }在開始編寫測試之前,我們必須滿足Logger類的依賴性,但是實(shí)際的實(shí)現(xiàn)對單元測試不利,日志可能會保存到文本文件中或?qū)⑷罩景l(fā)送到其他地方,這破壞了隔離在測試中,我們也不想檢查日志中的任何內(nèi)容,它們與我們擁有的業(yè)務(wù)邏輯無關(guān),因此我們將為此實(shí)現(xiàn)一個(gè)Dummy。
public class LoggerDummy implements Logger { @Override public void append(String text) {} }就是它? 虛擬內(nèi)部沒有代碼。 對于這種情況,我們內(nèi)部不需要任何實(shí)現(xiàn),并且我們準(zhǔn)備編寫測試。
PaymentServiceShould { class PaymentServiceShould { @Test void create_payment_request() { LoggerDummy loggerDummy = new LoggerDummy(); Customer customer= new Customer( "name" , "address" ); Item item = new Item( "item" , 1000 ); List<Item> items= asList(item); Sale sale = new Sale(customer, items); CreditCard creditCard = new CreditCard(customer, "1" ); PaymentService paymentService = new PaymentService(loggerDummy); PaymentRequest actual = paymentService.createPaymentRequest(sale, creditCard); assertEquals( new PaymentRequest( 1000 , "1" ), actual); } }存根
存根稍微復(fù)雜一點(diǎn),它們?yōu)槲覀兊暮艚刑峁┕揞^應(yīng)答,它們?nèi)匀粵]有任何邏輯,但是它們不會拋出錯(cuò)誤,而是返回一個(gè)預(yù)定義的值。
在進(jìn)行測試時(shí),您希望測試具有確定性和可重復(fù)性,因此由于合作者的更改,測試不會在一段時(shí)間后停止工作。
現(xiàn)在, PaymentRequest必須包含信用卡操作員費(fèi)用,該費(fèi)用的費(fèi)率由信用卡操作員定義,該費(fèi)用由卡的前四位數(shù)字定義。要實(shí)現(xiàn)此目的,您必須創(chuàng)建一個(gè)存根并添加必要的內(nèi)容更改PaymentService 。 第一步是實(shí)現(xiàn)存根和生產(chǎn)代碼所需的接口,這是您預(yù)先進(jìn)行一些設(shè)計(jì)的部分,考慮存根中應(yīng)該包含哪些參數(shù)以及應(yīng)該返回什么,而不用考慮內(nèi)部實(shí)現(xiàn),但與該協(xié)作者的合同是:
public interface OperatorRate { int feeRate(String operator) }使用定義的接口,我們可以開始編寫存根:
public class OperatorRateStub implements OperatorRate { private int rate; public OperatorRateStub( int rate){ this .rate = rate; } @Override public int feeRate(String operator) { return rate; } }存根將始終返回在構(gòu)造函數(shù)中傳遞的值,我們對存根具有完全控制權(quán),并且它與生產(chǎn)代碼完全隔離。 現(xiàn)在,測試代碼已實(shí)現(xiàn)
@Test void create_payment_request() { LoggerDummy loggerDummy = new LoggerDummy(); Customer customer= new Customer( "name" , "address" ); Item item = new Item( "item" , 1000 ); List<Item> items= asList(item); Sale sale = new Sale(customer, items); CreditCard creditCard = new CreditCard(customer, "1" ); OperatorRate operatorRate = new OperatorRateStub( 10 ); PaymentService paymentService = new PaymentService(loggerDummy, operatorRate); PaymentRequest actual = paymentService.createPaymentRequest(sale, creditCard); assertEquals( new PaymentRequest( 1000 , "1" , 100 ), actual); }cks
嘲笑是您可以說出他們期望收到的東西的對象。 它們用于驗(yàn)證被測系統(tǒng)及其協(xié)作者之間的行為。
您設(shè)置期望值,調(diào)用SUT的方法,并驗(yàn)證是否在最后調(diào)用了該方法。
隨著我們正在維護(hù)的系統(tǒng)的發(fā)展,我們需要完成一個(gè)新的用戶故事,客戶希望每超過1000磅的PaymentRequest發(fā)送一封電子郵件給管理部門。 隔離發(fā)送電子郵件有兩個(gè)原因:
- 發(fā)送電子郵件是一種與外界交流的活動,我們不能在每次運(yùn)行測試時(shí)都發(fā)送電子郵件,這會降低測試速度,而且確實(shí)很煩人。
- PaymentService應(yīng)該不知道電子郵件發(fā)件人的實(shí)現(xiàn),將這兩件事混合會造成耦合,并使維護(hù)服務(wù)或更改我們發(fā)送電子郵件的方式更加困難,這就是電子郵件發(fā)件人自己獲得服務(wù)的原因。
我們需要遵循的步驟是:
- 創(chuàng)建一個(gè)界面
- 創(chuàng)建一個(gè)實(shí)現(xiàn)接口的模擬
- 寫我們的測試
界面:
public interface PaymentEmailSender { void send(PaymentRequest paymentRequest); }然后我們必須實(shí)現(xiàn)我們的模擬:
public class PaymentServiceMock implements PaymentEmailSender { private List<PaymentRequest> paymentRequestSent = new ArrayList<>(); private List<PaymentRequest> expectedPaymentRequest = new ArrayList<>(); @Override public void send(PaymentRequest paymentRequest) { paymentRequestSent.add(paymentRequest); } public void expect(PaymentRequest paymentRequest) { expectedPaymentRequest.add(paymentRequest); } public void verify() { assertEquals(paymentRequestSent, expectedPaymentRequest); } }這是一個(gè)非常簡單的模仿對象,但它會做的工作,我們實(shí)現(xiàn)接口我們剛剛創(chuàng)建的,我們所做的send方法商店P(guān)aymentRequest ,我們添加了兩種方法來設(shè)置模擬, expect和verify ,在verify方法使用jUnit assertEqual方法將期望值與SUT傳遞的值進(jìn)行比較。
我們針對新的用戶故事編寫測試:
@Test void send_email_to_the_administration_if_sale_is_over_1000() { EmailSenderMock emailSender = new EmailSenderMock(); LoggerDummy loggerDummy = new LoggerDummy(); OperatorRate operatorRate = new OperatorRateStub( 10 ); PaymentService paymentService = new PaymentService(loggerDummy, operatorRate, emailSender); PaymentRequest paymentRequest = new PaymentRequest( 1000 , "1" , 100 ); Customer customer= new Customer( "name" , "address" ); Item item = new Item( "item" , 1000 ); List<Item> items = asList(item); Sale sale = new Sale(customer, items); CreditCard creditCard = new CreditCard(customer, "1" ); paymentService.createPaymentRequest(sale, creditCard); emailSender.expect(paymentRequest); emailSender.verify(); }測試結(jié)果為:
org.opentest4j.AssertionFailedError: Expected :[] Actual :[PaymentRequest{total= 2500 , cardNumber= '1234123412341234' , gatewayFee= 250 }]然后,我們執(zhí)行生產(chǎn)代碼:
public class PaymentService { private Logger logger; private OperatorRate operatorRate; private final EmailSender emailSender; public PaymentService(Logger logger, OperatorRate operatorRate, EmailSender emailSender) { this .logger = logger; this .operatorRate = operatorRate; this .emailSender = emailSender; } public PaymentRequest createPaymentRequest(Sale sale, CreditCard creditCard) { logger.append( "Creating payment for sale: " + sale); int feeRate = operatorRate.feeRate(creditCard.cardNumber); int fee = (feeRate * sale.total()) / 100 ; PaymentRequest paymentRequest = new PaymentRequest(sale.total(), creditCard.cardNumber, fee); if (sale.total() >= 1000 ) { emailSender.send(paymentRequest); } return paymentRequest; } }測試通過,我們就完成了故事。
間諜
可以像間諜一樣,將某個(gè)間諜滲透到您的SUT中并記錄他的一舉一動,就像電影間諜一樣。 與模擬不同,間諜是沉默的,它取決于您根據(jù)他提供的數(shù)據(jù)進(jìn)行斷言。
當(dāng)您不確定自己的協(xié)作對象會調(diào)用什么時(shí),可以使用間諜,因此您可以記錄所有內(nèi)容并斷言間諜是否調(diào)用了所需數(shù)據(jù)。
對于此示例,我們可以使用為模擬創(chuàng)建的相同接口,并使用間諜實(shí)施新測試。
public class PaymentEmailSpy implements PaymentEmailSender { private List<PaymentRequest> paymentRequests = new ArrayList<>(); @Override public void send(PaymentRequest paymentRequest) { paymentRequests.add(paymentRequest); } public int timesCalled() { return paymentRequests.size(); } public boolean calledWith(PaymentRequest paymentRequest) { return paymentRequests.contains(paymentRequest); } }Spy的實(shí)現(xiàn)接近于模擬,但是與其給出我們期望的調(diào)用,我們只是記錄了類的行為,然后我們進(jìn)行了測試,然后可以聲明我們需要的東西。
PaymentServiceShould { class PaymentServiceShould { private OperatorRate operatorRate; private EmailSenderMock emailSender; private PaymentService paymentService; private LoggerDummy loggerDummy; public static final Customer BOB = new Customer( "Bob" , "address" ); public static final Item IPHONE = new Item( "iPhone X" , 1000 ); public static final CreditCard BOB_CREDIT_CARD = new CreditCard BOB_CREDIT_CARD = CreditCard(BOB, "1" ); @BeforeEach void setUp() { loggerDummy = new LoggerDummy(); operatorRate = new OperatorRateStub( 10 ); emailSender = new EmailSenderMock(); paymentService = new PaymentService(loggerDummy, operatorRate, emailSender); } @Test void not_send_email_for_sales_under_1000() { Item iphoneCharger = new Item( "iPhone Charger" , 50 ); Sale sale = new Sale(BOB, asList(iphoneCharger)); EmailSenderSpy emailSpy = new EmailSenderSpy(); PaymentService spiedPaymentService = new PaymentService(loggerDummy, operatorRate, emailSpy); spiedPaymentService.createPaymentRequest(sale, BOB_CREDIT_CARD); assertEquals( 0 , emailSpy.timesCalled()); } }假貨
我們使用間諜創(chuàng)建一個(gè)PaymentService ,進(jìn)行必要的調(diào)用,然后可以根據(jù)間諜提供的數(shù)據(jù)進(jìn)行斷言。
偽造與我們擁有的所有其他示例不同,偽造具有簡化的業(yè)務(wù)邏輯,而不是固定的響應(yīng)或僅記錄呼叫。
Fake的一個(gè)示例是InMemory存儲庫,我們可以在其中存儲,檢索甚至進(jìn)行一些查詢,但是它沒有背后的真實(shí)數(shù)據(jù)庫,實(shí)際上所有內(nèi)容都可以存儲在列表中,或者您可以偽造諸如API之類的外部服務(wù)。
在這種情況下,我們可以創(chuàng)建一個(gè)偽造品來模擬連接到支付網(wǎng)關(guān)的API,并用來測試我們對OperatorRate生產(chǎn)實(shí)現(xiàn)。
在這種情況下,我們的生產(chǎn)實(shí)現(xiàn)將通過信用卡運(yùn)營商將Json發(fā)送到網(wǎng)關(guān),并以比率返回Json,然后將進(jìn)行正確的解析并返回Json中的值。
因此,我們開始為實(shí)現(xiàn)OperatorRate CreditCardRate類編寫測試
public class CreditCardRateShould { @Test void return_rate_for_credit_card_payment() { PaymentGateway fakeCreditCardGateway = new FakeCreditCardGateway(); CreditCardRate creditCardRate = new CreditCardRate(fakeCreditCardGateway); String operator = "1234123412341234" ; int result = creditCardRate.feeRate(operator); assertEquals( 10 , result); } }被測試的類與外部服務(wù)對話,該服務(wù)被FakeCreditCardGateway偽造。
偽網(wǎng)關(guān)正在解析Json并應(yīng)用一些非常簡單的邏輯并返回另一個(gè)Json。
public class FakeCreditCardGateway implements PaymentGateway { @Override public String rateFor(String cardOperator) { String operator = parseJson(cardOperator); int rate = 15 ; if (operator.startsWith( "1234" )) { rate = 10 ; } if (operator.startsWith( "1235" )) { rate = 8 ; } return jsonFor(rate); } private String jsonFor( int rate) { return new JsonObject() .add( "rate" , rate) .toString(); } private String parseJson(String cardOperator) { JsonObject payload = Json.parse(cardOperator).asObject(); return payload.getString( "operator" , "" ); } }最后是CreditCardRate類的生產(chǎn)代碼
public class CreditCardRate implements OperatorRate { private PaymentGateway paymentGateway; public CreditCardRate(PaymentGateway paymentGateway) { this .paymentGateway = paymentGateway; } @Override public int feeRate(String operator) { String payload = jsonFor(operator); String rateJson = paymentGateway.rateFor(payload); return parse(rateJson); } private int parse(String rateJson) { return Json.parse(rateJson).asObject() .getInt( "rate" , 0 ); } private String jsonFor(String operator) { return new JsonObject() .add( "operator" , operator) .toString(); } }使用此偽造品,我們可以測試要發(fā)送到網(wǎng)關(guān)的Json是否正確,具有某種邏輯,以便偽造品網(wǎng)關(guān)可以回答不同的速率,最后可以測試我們是否正確解析了響應(yīng)Json。
這是一個(gè)非常臨時(shí)的實(shí)現(xiàn),無需處理HTTP請求,但是我們可以對如何將其轉(zhuǎn)換為現(xiàn)實(shí)世界有所了解。 如果您想編寫集成測試以進(jìn)行真正的HTTP調(diào)用,則可以看看WireMock和嘲笑jay-server之類的東西 。
Mockito和鴨子綜合癥
不僅Mockito,而且大多數(shù)嘲笑框架都具有這種鴨子綜合癥,在鴨子綜合癥中他們可以做很多事情,鴨子可以游泳,飛行和行走。 這些框架的作品具有虛擬,模擬,間諜和存根。
那么我們?nèi)绾沃涝谑褂每蚣苓M(jìn)行模擬時(shí)正在使用什么呢? 為了解決這個(gè)問題,我們將使用手動測試雙打編寫的測試,并將其重構(gòu)為使用Mockito。
PaymentServiceShould { class PaymentServiceShould { private OperatorRate operatorRate; private EmailSenderMock emailSender; private PaymentService paymentService; private LoggerDummy loggerDummy; public static final Customer BOB = new Customer( "Bob" , "address" ); public static final Item IPHONE = new Item( "iPhone X" , 1000 ); public static final CreditCard BOB_CREDIT_CARD = new CreditCard BOB_CREDIT_CARD = CreditCard(BOB, "1" ); @BeforeEach void setUp() { loggerDummy = new LoggerDummy(); operatorRate = new OperatorRateStub( 10 ); emailSender = new EmailSenderMock(); paymentService = new PaymentService(loggerDummy, operatorRate, emailSender); } @Test void create_payment_request() { Sale sale = new Sale(BOB, asList(IPHONE)); PaymentRequest actual = paymentService.createPaymentRequest(sale, BOB_CREDIT_CARD); assertEquals( new PaymentRequest( 1000 , "1" , 100 ), actual); } @Test void send_email_to_the_administration_if_sale_is_over_1000() { Sale sale = new Sale(BOB, asList(IPHONE)); paymentService.createPaymentRequest(sale, BOB_CREDIT_CARD); emailSender.expect( new PaymentRequest( 1000 , "1" , 100 )); emailSender.verify(); } @Test void not_send_email_for_sales_under_1000() { Item iphoneCharger = new Item( "iPhone Charger" , 50 ); Sale sale = new Sale(BOB, asList(iphoneCharger)); EmailSenderSpy emailSpy = new EmailSenderSpy(); PaymentService spiedPaymentService = new PaymentService(loggerDummy, operatorRate, emailSpy); spiedPaymentService.createPaymentRequest(sale, BOB_CREDIT_CARD); assertEquals( 0 , emailSpy.timesCalled()); } @Test void send_email_to_hmrs_for_sales_over_10_thousand() { Item reallyExpensiveThing = new Item( "iPhone Charger" , 50000 ); Sale sale = new Sale(BOB, asList(reallyExpensiveThing)); EmailSenderSpy emailSpy = new EmailSenderSpy(); PaymentService spiedPaymentService = new PaymentService(loggerDummy, operatorRate, emailSpy); spiedPaymentService.createPaymentRequest(sale, BOB_CREDIT_CARD); assertEquals( 2 , emailSpy.timesCalled()); } }假
創(chuàng)建Mockito模擬時(shí),該對象是Dummy,它沒有任何行為,因此我們可以開始重構(gòu)測試并更改LoggerDummy以使用Mockito對象。
PaymentServiceShould { class PaymentServiceShould { private OperatorRate operatorRate; private EmailSenderMock emailSender; private PaymentService paymentService; - private LoggerDummy loggerDummy; + private Logger logger; public static final Customer BOB = new Customer( "Bob" , "address" ); public static final Item IPHONE = new Item( "iPhone X" , 1000 ); public static final CreditCard BOB_CREDIT_CARD = new CreditCard BOB_CREDIT_CARD = CreditCard(BOB, "1" ); @BeforeEach void setUp() { LoggerDummy(); - loggerDummy = new LoggerDummy(); + logger = mock(Logger. class ); operatorRate = new OperatorRateStub( 10 ); emailSender = new EmailSenderMock(); PaymentService(loggerDummy, operatorRate, emailSender); - paymentService = new PaymentService(loggerDummy, operatorRate, emailSender); + paymentService = new PaymentService(logger, operatorRate, emailSender); } @Test @@ - 48 , 7 + 49 , 7 @@ class PaymentServiceShould { Item iphoneCharger = new Item( "iPhone Charger" , 50 ); Sale sale = new Sale(BOB, asList(iphoneCharger)); EmailSenderSpy emailSpy = new EmailSenderSpy(); - PaymentService spiedPaymentService = new PaymentService(loggerDummy, operatorRate, emailSpy); + PaymentService spiedPaymentService = new PaymentService(logger, operatorRate, emailSpy); spiedPaymentService.createPaymentRequest(sale, BOB_CREDIT_CARD); @@ - 60 , 7 + 61 , 7 @@ class PaymentServiceShould { Item reallyExpensiveThing = new Item( "iPhone Charger" , 50000 ); Sale sale = new Sale(BOB, asList(reallyExpensiveThing)); EmailSenderSpy emailSpy = new EmailSenderSpy(); - PaymentService spiedPaymentService = new PaymentService(loggerDummy, operatorRate, emailSpy); + PaymentService spiedPaymentService = new PaymentService(logger, operatorRate, emailSpy); spiedPaymentService.createPaymentRequest(sale, BOB_CREDIT_CARD);所有測試都通過了,我們不必使用我們擁有的LoggerDummy實(shí)現(xiàn)。
存根
現(xiàn)在我們必須開始對模擬進(jìn)行某些操作,并按照手動測試雙打的相同順序,必須將Mockito對象轉(zhuǎn)換為存根,因?yàn)镸ockito具有g(shù)iven()方法,可以在其中設(shè)置值退回。
對于基元,Mockito返回0,Objects返回null,對于List,Map或Set這樣的集合返回空集合。
given()以下列方式工作:
given(<method to be called>).willReturn(returnValue);并且我們在測試中更改了實(shí)現(xiàn)。
import static java.util.Arrays.asList; import static org.junit.jupiter.api.Assertions.assertEquals; + import static org.mockito.ArgumentMatchers.anyString; + import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; @@ - 20 , 9 + 22 , 10 @@ class PaymentServiceShould { @BeforeEach void setUp() { logger = mock(Logger. class ); - operatorRate = new OperatorRateStub( 10 ); + operatorRate = mock(OperatorRate. class ); emailSender = new EmailSenderMock(); paymentService = new PaymentService(logger, operatorRate, emailSender); + given(operatorRate.feeRate(BOB_CREDIT_CARD.cardNumber)).willReturn( 10 ); }現(xiàn)在,該模擬的行為就像存根,測試正在通過。
嘲弄和間諜
在我們創(chuàng)建的上一個(gè)測試中,我們?nèi)栽谑褂脛?chuàng)建的PaymentEmailMock ,現(xiàn)在我們可以在Mockito中更改它。
@@ - 8 , 11 + 8 , 12 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; + import static org.mockito.Mockito.verify; PaymentServiceShould { class PaymentServiceShould { private OperatorRate operatorRate; - private EmailSenderMock emailSender; + private EmailSender emailSender; private PaymentService paymentService; private Logger logger; public static final Customer BOB = new Customer( "Bob" , "address" ); @@ - 23 , 7 + 24 , 7 @@ class PaymentServiceShould { void setUp() { logger = mock(Logger. class ); operatorRate = mock(OperatorRate. class ); - emailSender = new EmailSenderMock(); + emailSender = mock(EmailSender. class ); paymentService = new PaymentService(logger, operatorRate, emailSender); given(operatorRate.feeRate(BOB_CREDIT_CARD.cardNumber)).willReturn( 10 ); } @@ - 43 , 8 + 44 , 8 @@ class PaymentServiceShould { paymentService.createPaymentRequest(sale, BOB_CREDIT_CARD); - emailSender.expect( new PaymentRequest( 1000 , "1" , 100 )); - emailSender.verify(); + PaymentRequest paymentRequest = new PaymentRequest( 1000 , "1" , 100 ); + verify(emailSender).send(paymentRequest); }所有測試都通過了,很棒,但是Mockito的存根和我們創(chuàng)建的存根之間是有區(qū)別的。 這次我們不必指定期望的內(nèi)容,我們直接進(jìn)入驗(yàn)證步驟。 那就是Mockito再次扮演多個(gè)角色,由Mockito創(chuàng)建的模擬程序?qū)⑾耖g諜一樣記錄所有收到的呼叫。
我們?nèi)匀挥惺褂瞄g諜的測試,我們可以將測試更改為僅使用模仿。
PaymentServiceShould { class PaymentServiceShould { void not_send_email_for_sales_under_1000() { Item iphoneCharger = new Item( "iPhone Charger" , 50 ); Sale sale = new Sale(BOB, asList(iphoneCharger)); - EmailSenderSpy emailSpy = new EmailSenderSpy(); - PaymentService spiedPaymentService = new PaymentService(logger, operatorRate, emailSpy); - spiedPaymentService.createPaymentRequest(sale, BOB_CREDIT_CARD); + paymentService.createPaymentRequest(sale, BOB_CREDIT_CARD); , emailSpy.timesCalled()); - assertEquals( 0 , emailSpy.timesCalled()); + verify(emailSender, never()).send(any(PaymentRequest. class )); } @Test void send_email_to_hmrs_for_sales_over_10_thousand() { Item reallyExpensiveThing = new Item( "iPhone Charger" , 50000 ); Sale sale = new Sale(BOB, asList(reallyExpensiveThing)); - EmailSenderSpy emailSpy = new EmailSenderSpy(); - PaymentService spiedPaymentService = new PaymentService(logger, operatorRate, emailSpy); - spiedPaymentService.createPaymentRequest(sale, BOB_CREDIT_CARD); + paymentService.createPaymentRequest(sale, BOB_CREDIT_CARD); , emailSpy.timesCalled()); - assertEquals( 2 , emailSpy.timesCalled()); + PaymentRequest paymentRequest = new PaymentRequest( 50000 , "1" , 5000 ); + verify(emailSender, times( 2 )).send(paymentRequest); } }verify具有多個(gè)修飾符,例如:
- atLeast(int)
- atLeastOnce()
- atMost(int)
- times(int)
同樣,我們有具有多個(gè)功能的模擬對象,這次有一個(gè)模擬和一個(gè)間諜。
假貨呢?
偽造品是內(nèi)部具有邏輯的對象,我們無法使用Mockito來實(shí)現(xiàn),但這不是問題,在大多數(shù)情況下,您不需要偽造品,通常偽造品會增長,并且您將結(jié)束測試以查看您的偽造品是否正常正確地。
正如鮑伯叔叔所說的那樣,他的帖子是“小嘲笑”:
是的,嗯。 我不常寫假貨。 確實(shí),我三十多年沒有寫過一篇。
良好做法和氣味。
CQS,存根和模擬
如果您不熟悉CQS,請繼續(xù)閱讀以下內(nèi)容:
OO技巧:命令查詢分離的藝術(shù)
bliki:CommandQuerySeparation
決定在哪里使用存根和模擬的一個(gè)好的經(jīng)驗(yàn)法則是遵循“命令查詢分離”原則,您可以在其中:
指令
- 他們沒有返回值
- 用于在您的類中對數(shù)據(jù)進(jìn)行突變。
- 使用Mockito進(jìn)行模擬時(shí),請使用verify() 。
查詢
- 是從類中查詢數(shù)據(jù)
- 不要產(chǎn)生任何副作用
- 只返回?cái)?shù)據(jù)。
- 在使用Mockito進(jìn)行模擬時(shí)使用named given()
您擁有的僅模擬/存根類
關(guān)于模擬,我們必須了解的一件事是,不僅涉及測試,而且還涉及設(shè)計(jì)我們的SUT及其協(xié)作者的工作方式,要找到不使用第三方庫的應(yīng)用程序?qū)⒎浅@щy,但是這并不意味著您必須嘲笑它??們,實(shí)際上您絕對不應(yīng)該那樣做。 模擬第三方庫的主要內(nèi)容是您需要對其進(jìn)行更改,更改簽名會破壞所有模擬您的測試。
解決方案? 使用模擬工具圍繞該庫編寫一個(gè)瘦包裝器,您可以設(shè)計(jì)一個(gè)僅接收和返回必要信息的瘦包裝器,但是我們?nèi)绾螠y試包裝器呢?
在這種情況下,可以根據(jù)您所具有的依賴性來測試包裝器,如果您有數(shù)據(jù)庫層的包裝器,則可以在另一個(gè)源集中進(jìn)行集成測試,因此您可以運(yùn)行單元測試而不必?fù)?dān)心集成測試的速度變慢你失望。
不要嘲笑數(shù)據(jù)結(jié)構(gòu)。
當(dāng)您擁有自己的數(shù)據(jù)結(jié)構(gòu)時(shí),不必模擬它,您可以簡單地用所需的數(shù)據(jù)實(shí)例化,以防難以實(shí)例化數(shù)據(jù)結(jié)構(gòu)或需要多個(gè)對象時(shí)可以使用Builder模式。
您可以在此處了解Builder模式。
使您的測試變得簡約
使用模擬對象進(jìn)行測試時(shí),請務(wù)必不要使測試過于脆弱,這一點(diǎn)很重要,重要的是,您可以重構(gòu)代碼庫而不會造成測試的煩惱,如果發(fā)生這種情況,您可能需要對模擬進(jìn)行檢查,這可能是一些超額規(guī)定的事情,如果在多個(gè)測試中都發(fā)生這種情況,則最終會減慢開發(fā)速度。 解決方案是重新檢查代碼,看看是否需要更改規(guī)范或代碼。
想象一下,在開始的示例中,不是使用Dummy作為記錄器,而是使用了模擬。 然后,模擬將驗(yàn)證記錄器通過的所有消息,并進(jìn)行任何更改都會破壞測試。 沒有人愿意僅僅因?yàn)樗麄冃迯?fù)了日志中的錯(cuò)字而導(dǎo)致測試失敗。
不要使用模擬/存根來測試邊界/隔離的對象
沒有協(xié)作者的對象不必使用模擬對象進(jìn)行測試,像這樣的對象只需要在返回或存儲的值中聲明即可。 聽起來似乎很明顯,但是加強(qiáng)它是很好的。
對于像JSON解析器這樣的依賴項(xiàng),您可以測試包裝器是否具有真正的依賴項(xiàng)。 您可以在Fake的示例中看到這一點(diǎn),而不是模擬Json庫,而是使用真實(shí)的庫,可以使用類似包裝器的方式進(jìn)行轉(zhuǎn)換,然后我們必須使用真實(shí)的Json測試包裝器庫并查看創(chuàng)建的json是否正確,在這種情況下,我們永遠(yuǎn)不會嘲笑該依賴項(xiàng)。
不要添加行為
模擬是測試雙打,您不應(yīng)該在測試雙打中增加復(fù)雜性,您的偽造包含一些邏輯,但是除此之外,測試雙打都不應(yīng)該包含邏輯,這是您放錯(cuò)了責(zé)任的癥狀。
這個(gè)問題的一個(gè)例子是一個(gè)返回另一個(gè)模擬的模擬,如果您有一個(gè)類似服務(wù)的東西可以返回另一個(gè)服務(wù),那么您可能想再看一下應(yīng)用程序的設(shè)計(jì)。
僅嘲笑/與你的近鄰打樁
一個(gè)可能具有多個(gè)依賴關(guān)系的復(fù)雜對象可能很難測試,從中我們可以看到的一個(gè)癥狀是測試的設(shè)置很復(fù)雜,并且測試也很難閱讀。 單元測試應(yīng)該專注于同時(shí)測試一件事,并且應(yīng)該只為鄰居設(shè)定期望值(認(rèn)為是Demeter法則)。 您可能必須引入角色來橋接對象及其周圍環(huán)境。
太多的模擬/存根
您的SUT可能有多個(gè)合作者,并且您的測試開始變得更加復(fù)雜且難以閱讀,就像在我們看到的其他情況下一樣,SUT可能承擔(dān)了太多的責(zé)任,以至于您不得不破壞對象成為更專注的小公司。
因此,如果您的服務(wù)在構(gòu)造函數(shù)中具有多個(gè)類,例如:
public ReadCommand(UserRepository userRepository, MessageRepository messageRepository, MessageFormatter messageFormatter, Console console, String username) { this .userRepository = userRepository; this .messageRepository = messageRepository; this .messageFormatter = messageFormatter; this .console = console; this .username = username; }您可以將其重構(gòu)為:
public ReadCommand(UserRepository userRepository, MessageRepository messageRepository, MessagePrinter messagePrinter, String username) { this .userRepository = userRepository; this .messageRepository = messageRepository; this .messagePrinter = messagePrinter; this .username = username; }現(xiàn)在, MessagePrinter具有MessageFormatter和Console一起工作,因此,當(dāng)您測試ReadCommand類時(shí),只需要驗(yàn)證是否調(diào)用了打印方法即可。
翻譯自: https://www.javacodegeeks.com/2019/04/introduction-to-test-doubles.html
總結(jié)
- 上一篇: 昂科威安卓手机映射怎么用(昂科威安卓)
- 下一篇: DDOS攻击平台(ddos攻击拍卖平台)