我一直在阅读(并试验)几个Java模拟API,如Mockito,EasyMock,JMock和PowerMock。我出于不同的原因喜欢他们每个人,但最终还是决定了Mockito。 请注意但是,这是不关于使用哪个框架的问题 - 这个问题确实适用于任何模拟框架,尽管解决方案将会看起来不同,因为API(显然)不同。
与许多内容一样,您阅读教程,按照示例进行操作,然后在沙盒项目中修改一些代码示例。但是,当真正使用这个东西的时候,你开始窒息 - 这就是我的所在。
我真的,非常喜欢嘲笑的想法。是的,我知道关于嘲笑导致“脆弱”测试的抱怨,这些测试过于严重地与被测试的类相结合。但是在我自己实现这种认识之前,我真的想让嘲笑有机会看看它是否能为我的单元测试增加一些好处。
我现在正试图在我的单元测试中积极使用模拟。 Mockito允许存根和嘲弄。假设我们有一个Car
对象,它有一个getMaxSpeed()
方法。在Mockito中,我们可以像这样存根:
Car mockCar = mock(Car.class);
when(mockCar.getMaxSpeed()).thenReturn(100.0);
这会将Car
对象“存根”为始终返回100.0
作为我们汽车的最高速度。
我的问题就是,在编写了一些单元测试之后......我所做的只是勾结我的合作者!我没有使用我可以使用的单一模拟方法(verify
等)!
我意识到我陷入了“顽固的心态”,我发现它无法打破。所有这些阅读,以及在我的单元测试中使用模拟的所有这些兴奋......我想不出一个用于行为验证的用例。
所以我备份并重新阅读了Fowler的article和其他BDD风格的文献,但我仍然只是“没有得到”测试双重合作者的行为验证价值。
我知道我错过了什么,我只是不确定是什么。有人能给我一个具体的例子(甚至是一组例子!),比如使用这个Car
类,并证明行为验证单元测试何时有利于状态验证测试?
提前感谢任何朝着正确方向的推动!
答案 0 :(得分:2)
好吧,如果被测对象调用具有计算值的协作者,并且测试应该测试计算是否正确,那么验证模拟合作器是正确的做法。例如:
private ResultDisplayer resultDisplayer;
public void add(int a, int b) {
int sum = a + b; // trivial example, but the computation might be more complex
displayer.display(sum);
}
显然,在这种情况下,您必须模拟显示器,并验证其显示方法是否已被调用,值为5,如果2和3是add
方法的参数。
如果您对协作者所做的一切都是没有参数的调用getter,或者是测试方法的直接输入的参数,那么存根可能就足够了,除非代码可能从两个不同的协作者获得值并且您想要验证已经召集了合适的合作者。
示例:
private Computer noTaxComputer;
private Computer taxComputer;
public BigDecimal computePrice(Client c, ShoppingCart cart) {
if (client.isSubjectToTaxes()) {
return taxComputer.compute(cart);
}
else {
return noTaxComputer.compute(cart);
}
}
答案 1 :(得分:2)
我喜欢@JB Nizet的回答,但这是另一个例子。假设您想在进行一些更改后使用Hibernate将Car持久化到数据库。所以你有这样一个类:
public class CarController {
private HibernateTemplate hibernateTemplate;
public void setHibernateTemplate(HibernateTemplate hibernateTemplate) {
this.hibernateTemplate = hibernateTemplate;
}
public void accelerate(Car car, double mph) {
car.setCurrentSpeed(car.getCurrentSpeed() + mph);
hibernateTemplate.update(car);
}
}
要测试加速方法,您可以使用存根,但不会进行竞争测试。
public class CarControllerTest {
@Mock
private HibernateTemplate mockHibernateTemplate;
@InjectMocks
private CarController controllerUT;
@Test
public void testAccelerate() {
Car car = new Car();
car.setCurrentSpeed(10.0);
controllerUT.accelerate(car, 2.5);
assertThat(car.getCurrentSpeed(), is(12.5));
}
}
此测试通过并检查计算,但我们不知道汽车的新速度是否已保存。为此,我们需要添加:
verify(hibernateTemplate).update(car);
现在,假设如果您尝试加速超过最大速度,则预计加速和更新不会发生。在这种情况下,您需要:
@Test
public void testAcceleratePastMaxSpeed() {
Car car = new Car();
car.setMaxSpeed(20.0);
car.setCurrentSpeed(10.0);
controllerUT.accelerate(car, 12.5);
assertThat(car.getCurrentSpeed(), is(10.0));
verify(mockHibernateTemplate, never()).update(car);
}
此测试不会通过我们当前的CarController实现,但它不应该。它表明你需要做更多的工作来支持这种情况,其中一个要求就是在这种情况下你不会尝试写入数据库。
基本上,验证应该用于它的确切含义 - 验证发生了什么(或没有发生)。如果它发生或不发生的事实并不是你想要测试的,那么就跳过它。以我做的第二个例子为例。有人可能会争辩说,由于价值没有改变,更新是否被调用并不重要。在这种情况下,您可以跳过第二个示例中的验证步骤,因为accelerate
的实现无论如何都是正确的。
我希望听起来好像我正在使用验证的情况。它可以使你的测试非常脆弱。但它也可以“验证”应该发生的重要事情确实发生了。
答案 2 :(得分:1)
我对此的看法是每个测试用例都应该包含EITHER
assert
s OR verify
s。但不是两者。
在我看来,在大多数测试类中,最终会出现“存根和断言”测试用例和“验证”测试用例的混合。测试用例是执行“存根和断言”还是执行“验证”取决于协作者返回的值对测试是否重要。我需要两个例子来说明这一点。
假设我有一个Investment
类,其值为美元。它的构造函数设置初始值。它有一个addGold
方法,它将Investment
的值增加黄金的金额乘以每盎司美元的黄金价格。我有一个名为PriceCalculator
的合作者,负责计算黄金的价格。我可能会写一个这样的测试。
public void addGoldIncreasesInvestmentValueByPriceTimesAmount(){
PriceCalculator mockCalculator = mock( PriceCalculator.class );
when( mockCalculator.getGoldPrice()).thenReturn( new BigDecimal( 400 ));
Investment toTest = new Investment( new BigDecimal( 10000 ));
toTest.addGold( 5 );
assertEquals( new BigDecimal( 12000 ), toTest.getValue());
}
在这种情况下,协作者方法的结果对测试很重要。我们存根,因为我们此时没有测试PriceCalculator
。无需验证,因为如果未调用该方法,则投资值的最终值将不正确。所以我们需要的只是assert
。
现在,假设每当有人从Investment
撤回超过$ 100000时,Investment
班级都要求通知IRS。它使用名为IrsNotifier
的协作者来执行此操作。所以对此的测试可能看起来像这样。
public void largeWithdrawalNotifiesIRS(){
IrsNotifier mockNotifier = mock( IrsNotifier.class );
Investment toTest = new Investment( new BigDecimal( 200000 ));
toTest.setIrsNotifier( mockNotifier );
toTest.withdraw( 150000 );
verify( mockNotifier ).notifyIRS();
}
在这种情况下,测试不关心协作者方法notifyIRS()
的返回值。或者它可能是无效的。重要的是该方法被调用。对于这样的测试,您将使用verify
。在这样的测试中可能存在存根(设置其他协作者,或者从不同方法返回值),但是您不太可能想要使用您验证的相同方法存根。
如果您发现自己在同一个协作者方法上使用了存根和验证,那么您应该问问自己为什么。真正试图证明的测试是什么?返回值对测试很重要吗?因为这通常是测试代码的味道。
希望这些例子对你有所帮助。