在编写单元测试时,您会遇到许多协作者,并且他们都有非常特殊的行为,知道在正确的时间必须使用哪种测试两倍可以使您的生活更轻松。
假
第一个是Dummy对象,它是最简单的一个,Dummy只是您为满足构造函数而传递的对象,它不会实现任何方法,也不会实现。
在测试课程时,我们不想使用记录器做任何事情,那么我们该怎么办?
例如,有一个带有记录器的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类的依赖性,但是实际的实现对单元测试不利,日志可能会保存到文本文件中或将日志发送到其他地方,这破坏了隔离在测试中,我们也不想检查日志中的任何内容,它们与我们拥有的业务逻辑无关,因此我们将为此实现一个Dummy。
public class LoggerDummy implements Logger {
@Override
public void append(String text) {} }
就是它? 虚拟内部没有代码。 对于这种情况,我们内部不需要任何实现,并且我们准备编写测试。
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);
} }
存根
存根稍微复杂一点,它们为我们的呼叫提供罐头应答,它们仍然没有任何逻辑,但是它们不会抛出错误,而是返回一个预定义的值。
在进行测试时,您希望测试具有确定性和可重复性,因此由于合作者的更改,测试不会在一段时间后停止工作。
现在, PaymentRequest
必须包含信用卡操作员费用,该费用的费率由信用卡操作员定义,该费用由卡的前四位数字定义。要实现此目的,您必须创建一个存根并添加必要的内容更改PaymentService
。 第一步是实现存根和生产代码所需的接口,这是您预先进行一些设计的部分,考虑存根中应该包含哪些参数以及应该返回什么,而不用考虑内部实现,但与该协作者的合同是:
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;
} }
存根将始终返回在构造函数中传递的值,我们对存根具有完全控制权,并且它与生产代码完全隔离。 现在,测试代码已实现
@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
嘲笑是您可以说出他们期望收到的东西的对象。 它们用于验证被测系统及其协作者之间的行为。
您设置期望值,调用SUT的方法,并验证是否在最后调用了该方法。
随着我们正在维护的系统的发展,我们需要完成一个新的用户故事,客户希望每超过1000磅的PaymentRequest
发送一封电子邮件给管理部门。 隔离发送电子邮件有两个原因:
- 发送电子邮件是一种与外界交流的活动,我们不能在每次运行测试时都发送电子邮件,这会降低测试速度,而且确实很烦人。
-
PaymentService
应该不知道电子邮件发件人的实现,将这两件事混合会造成耦合,并使维护服务或更改我们发送电子邮件的方式更加困难,这就是电子邮件发件人自己获得服务的原因。
我们需要遵循的步骤是:
- 创建一个界面
- 创建一个实现接口的模拟
- 写我们的测试
界面:
public interface PaymentEmailSender {
void send(PaymentRequest paymentRequest); }
然后我们必须实现我们的模拟:
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);
} }
这是一个非常简单的模仿对象,但它会做的工作,我们实现接口我们刚刚创建的,我们所做的send
方法商店PaymentRequest
,我们添加了两种方法来设置模拟, expect
和verify
,在verify
方法使用jUnit assertEqual
方法将期望值与SUT传递的值进行比较。
我们针对新的用户故事编写测试:
@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(); }
测试结果为:
org.opentest4j.AssertionFailedError: Expected :[] Actual :[PaymentRequest{total= 2500 , cardNumber= '1234123412341234' , gatewayFee= 250 }]
然后,我们执行生产代码:
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;
}
}
测试通过,我们就完成了故事。
间谍
可以像间谍一样,将某个间谍渗透到您的SUT中并记录他的一举一动,就像电影间谍一样。 与模拟不同,间谍是沉默的,它取决于您根据他提供的数据进行断言。
当您不确定自己的协作对象会调用什么时,可以使用间谍,因此您可以记录所有内容并断言间谍是否调用了所需数据。
对于此示例,我们可以使用为模拟创建的相同接口,并使用间谍实施新测试。
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
的实现接近于模拟,但是与其给出我们期望的调用,我们只是记录了类的行为,然后我们进行了测试,然后可以声明我们需要的东西。
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());
} }
假货
我们使用间谍创建一个PaymentService
,进行必要的调用,然后可以根据间谍提供的数据进行断言。
伪造与我们拥有的所有其他示例不同,伪造具有简化的业务逻辑,而不是固定的响应或仅记录呼叫。
Fake的一个示例是InMemory存储库,我们可以在其中存储,检索甚至进行一些查询,但是它没有背后的真实数据库,实际上所有内容都可以存储在列表中,或者您可以伪造诸如API之类的外部服务。
在这种情况下,我们可以创建一个伪造品来模拟连接到支付网关的API,并用来测试我们对OperatorRate
生产实现。
在这种情况下,我们的生产实现将通过信用卡运营商将Json发送到网关,并以比率返回Json,然后将进行正确的解析并返回Json中的值。
因此,我们开始为实现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);
} }
被测试的类与外部服务对话,该服务被FakeCreditCardGateway
伪造。
伪网关正在解析Json并应用一些非常简单的逻辑并返回另一个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
类的生产代码
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();
} }
使用此伪造品,我们可以测试要发送到网关的Json是否正确,具有某种逻辑,以便伪造品网关可以回答不同的速率,最后可以测试我们是否正确解析了响应Json。
这是一个非常临时的实现,无需处理HTTP请求,但是我们可以对如何将其转换为现实世界有所了解。 如果您想编写集成测试以进行真正的HTTP调用,则可以看看WireMock和嘲笑jay-server之类的东西 。
Mockito和鸭子综合症
不仅Mockito,而且大多数嘲笑框架都具有这种鸭子综合症,在鸭子综合症中他们可以做很多事情,鸭子可以游泳,飞行和行走。 这些框架的作品具有虚拟,模拟,间谍和存根。
那么我们如何知道在使用框架进行模拟时正在使用什么呢? 为了解决这个问题,我们将使用手动测试双打编写的测试,并将其重构为使用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());
} }
假
创建Mockito模拟时,该对象是Dummy,它没有任何行为,因此我们可以开始重构测试并更改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
实现。
存根
现在我们必须开始对模拟进行某些操作,并按照手动测试双打的相同顺序,必须将Mockito对象转换为存根,因为Mockito具有given()
方法,可以在其中设置值退回。
对于基元,Mockito返回0,Objects返回null,对于List,Map或Set这样的集合返回空集合。
given()
以下列方式工作:
given(<method to be called>).willReturn(returnValue);
并且我们在测试中更改了实现。
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 );
}
现在,该模拟的行为就像存根,测试正在通过。
嘲弄和间谍
在我们创建的上一个测试中,我们仍在使用创建的PaymentEmailMock
,现在我们可以在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的存根和我们创建的存根之间是有区别的。 这次我们不必指定期望的内容,我们直接进入验证步骤。 那就是Mockito再次扮演多个角色,由Mockito创建的模拟程序将像间谍一样记录所有收到的呼叫。
我们仍然有使用间谍的测试,我们可以将测试更改为仅使用模仿。
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
具有多个修饰符,例如:
-
atLeast(int)
-
atLeastOnce()
-
atMost(int)
-
times(int)
同样,我们有具有多个功能的模拟对象,这次有一个模拟和一个间谍。
假货呢?
伪造品是内部具有逻辑的对象,我们无法使用Mockito来实现,但这不是问题,在大多数情况下,您不需要伪造品,通常伪造品会增长,并且您将结束测试以查看您的伪造品是否正常正确地。
正如鲍伯叔叔所说的那样,他的帖子是“小嘲笑”:
是的,嗯。 我不常写假货。 确实,我三十多年没有写过一篇。
良好做法和气味。
CQS,存根和模拟
如果您不熟悉CQS,请继续阅读以下内容:
决定在哪里使用存根和模拟的一个好的经验法则是遵循“命令查询分离”原则,您可以在其中:
指令
- 他们没有返回值
- 用于在您的类中对数据进行突变。
- 使用Mockito进行模拟时,请使用
verify()
。
查询
- 是从类中查询数据
- 不要产生任何副作用
- 只返回数据。
- 在使用Mockito进行模拟时使用named
given()
您拥有的仅模拟/存根类
关于模拟,我们必须了解的一件事是,不仅涉及测试,而且还涉及设计我们的SUT及其协作者的工作方式,要找到不使用第三方库的应用程序将非常困难,但是这并不意味着您必须嘲笑它们,实际上您绝对不应该那样做。 模拟第三方库的主要内容是您需要对其进行更改,更改签名会破坏所有模拟您的测试。
解决方案? 使用模拟工具围绕该库编写一个瘦包装器,您可以设计一个仅接收和返回必要信息的瘦包装器,但是我们如何测试包装器呢?
在这种情况下,可以根据您所具有的依赖性来测试包装器,如果您有数据库层的包装器,则可以在另一个源集中进行集成测试,因此您可以运行单元测试而不必担心集成测试的速度变慢你失望。
不要嘲笑数据结构。
当您拥有自己的数据结构时,不必模拟它,您可以简单地用所需的数据实例化,以防难以实例化数据结构或需要多个对象时可以使用Builder模式。
您可以在此处了解Builder模式。
使您的测试变得简约
使用模拟对象进行测试时,请务必不要使测试过于脆弱,这一点很重要,重要的是,您可以重构代码库而不会造成测试的烦恼,如果发生这种情况,您可能需要对模拟进行检查,这可能是一些超额规定的事情,如果在多个测试中都发生这种情况,则最终会减慢开发速度。 解决方案是重新检查代码,看看是否需要更改规范或代码。
想象一下,在开始的示例中,不是使用Dummy作为记录器,而是使用了模拟。 然后,模拟将验证记录器通过的所有消息,并进行任何更改都会破坏测试。 没有人愿意仅仅因为他们修复了日志中的错字而导致测试失败。
不要使用模拟/存根来测试边界/隔离的对象
没有协作者的对象不必使用模拟对象进行测试,像这样的对象只需要在返回或存储的值中声明即可。 听起来似乎很明显,但是加强它是很好的。
对于像JSON解析器这样的依赖项,您可以测试包装器是否具有真正的依赖项。 您可以在Fake的示例中看到这一点,而不是模拟Json库,而是使用真实的库,可以使用类似包装器的方式进行转换,然后我们必须使用真实的Json测试包装器库并查看创建的json是否正确,在这种情况下,我们永远不会嘲笑该依赖项。
不要添加行为
模拟是测试双打,您不应该在测试双打中增加复杂性,您的伪造包含一些逻辑,但是除此之外,测试双打都不应该包含逻辑,这是您放错了责任的症状。
这个问题的一个例子是一个返回另一个模拟的模拟,如果您有一个类似服务的东西可以返回另一个服务,那么您可能想再看一下应用程序的设计。
仅嘲笑/与你的近邻打桩
一个可能具有多个依赖关系的复杂对象可能很难测试,从中我们可以看到的一个症状是测试的设置很复杂,并且测试也很难阅读。 单元测试应该专注于同时测试一件事,并且应该只为邻居设定期望值(认为是Demeter法则)。 您可能必须引入角色来桥接对象及其周围环境。
太多的模拟/存根
您的SUT可能有多个合作者,并且您的测试开始变得更加复杂且难以阅读,就像在我们看到的其他情况下一样,SUT可能承担了太多的责任,以至于您不得不破坏对象成为更专注的小公司。
因此,如果您的服务在构造函数中具有多个类,例如:
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; }
您可以将其重构为:
public ReadCommand(UserRepository userRepository, MessageRepository messageRepository,
MessagePrinter messagePrinter, String username) {
this .userRepository = userRepository;
this .messageRepository = messageRepository;
this .messagePrinter = messagePrinter;
this .username = username; }
现在, MessagePrinter
具有MessageFormatter
和Console
一起工作,因此,当您测试ReadCommand
类时,只需要验证是否调用了打印方法即可。
翻译自: https://www.javacodegeeks.com/2019/04/introduction-to-test-doubles.html