我在搜索如何测试数据库中的内容是否被正确删除时,发现了这个答案:https://dev59.com/e5jga4cB1Zd3GeqPKXJJ#38082803。但是这让我想到,如果删除方法失败并且它实际上没有删除对象,那该怎么办呢?验证方法只检查删除方法是否被调用一次,还是检查它被调用一次且成功了?因为如果它不检查删除是否成功,那么这个测试就毫无意义了。
Mockito.verify()
仅会验证在方法执行期间是否对模拟对象进行了调用。verify()
总是可行?实际上并非如此。
假设要测试删除某些内容的方法:
public class Foo{
MyDao myDao;
public void delete(int id){
myDao.delete(id);
}
}
@Mock
MyDao myDaoMock;
Foo foo;
@Test
public void delete(int id){
foo.delete(id);
Mockito.verify(myDaoMock).delete(id);
}
public void delete(int id){
myDao.delete(id);
myDao.create(id);
}
测试仍然是绿色的...哎呀。
另一种情况,假设一个主要调用依赖方法的方法:
public void doThat(){
Foo foo = fooDep.doThat(...);
Bar bar = barDep.doThat(foo);
FooBar fooBar = fooBarDep.doThat(foo, bar);
fooBis.doOtherThing(...);
// and so for
}
使用验证方法,单元测试将只是以Mockito格式描述/复制实现您的方法。
它不对返回结果进行任何断言。以不良方式更改实现(添加不正确的调用或删除所需的调用)很难通过测试失败检测到,因为测试成为了被调用语句的反映。
Mock验证通常需要谨慎使用。
在某些特定情况下,它可能会有所帮助,但在许多情况下,我看到开发人员滥用它(75%或更多的单元测试类是mock设置/记录/验证),结果产生了少量价值、很难维护且没有公平原因使构建变慢的膨胀单元测试。
事实上,对于主要依赖于具有副作用函数的方法,应优先使用集成测试(即使是部分切片的集成测试)。
Mocks Aren't Stubs of Martin Fowler 是一篇非常好的文章,值得你关注。
这尤其让我印象深刻,因为我观察到了一个模拟者程序员。我真的很喜欢编写测试时,你关注的是行为的结果,而不是它的实现方式。模拟者一直在考虑SUT将如何被实现,以便编写预期结果。这对我来说感觉非常不自然。
尽管这篇Martin Fowler文章很有趣,但我也不完全同意。
我同意Mockist方法,其中开发人员不mock依赖项,因为依赖项不方便,但系统地这样做通常是一个不好的主意。
我们应该始终有一个好的理由引入一个mock,例如:
但我不同意显式地创建存根通常是一个好主意,因为存根代码需要花费时间编写,可能会有错误,我们将不得不维护它。最后,为了使事情变得清晰稳健,存根类也应该进行单元测试。所有这些都有成本。
另一方面,由模拟库生成的Mocks没有所有这些缺陷。
并且没有什么阻止我们使用这些Mocks并具有Stub的思维方式:使Mocks协作为存根,即:
public void deleteFromPerson(person person) {
person = personRepository.returnPerson(person.getId());
personRepository.delete(person);
}
delete
方法时,你只能确定是否调用了delete
,而不能确定删除是否成功。那么,delete
失败的两种可能性是什么?delete
没有正确地调用,可能缺少某些参数或需要做一些准备工作才能进行删除。这种问题无法通过单元测试找到:如果你对如何调用另一个组件的假设是错误的,并且你模拟了另一个组件,那么你实现模拟的方式将仅反映你自己的误解,并且单元测试将成功。然而,这不是单元测试的缺陷:这就是为什么除了单元测试外还存在其他级别(如集成测试)的原因,它们旨在找到单元测试无法找到的那些缺陷。实际上,集成测试旨在找到组件之间交互中的缺陷。delete
可能在运行时失败,无论你是否正确调用了delete
方法。例如,你的代码可能没有写入personRepository
的权限,或者某个并行线程在此期间删除了该人员,等等。然而,示例代码中没有采取任何措施处理这样的运行时情况(好吧,它只是一个示例代码片段,但也可能是有意为之,见davidxxx的评论)。但是,假设应该有一些代码来处理未成功的delete
。delete
的规范),则已经在单元测试期间可以明显地发现delete
可能会失败。在这种情况下,也许它会返回错误代码或抛出异常。当开发人员意识到这一点时,就可以决定通过相应的错误处理代码扩展上述示例代码。而且,错误处理代码可以通过模拟delete
来进行单元测试,以便还可以练习这些错误情况。delete
可能会失败,那么我们首先会遇到集成问题:开发人员的单元测试将根据delete
永远不会失败的假设进行实现。并且这种误解不会在(不完全)模拟的delete
的单元测试中找到。同样,它将不得不在集成测试期间遇到delete
可能在运行时失败的情况。然后,再次需要扩展示例代码,并扩展单元测试等 。PersonRepository.delete()
问题后单元测试一定会改变。这种情况可能发生,但相对较少见。对于任何存储库调用都进行try/catch
将使客户端代码混乱,并且可能会做同样的事情。在大多数情况下,我们没有针对编码或网络问题的特定异常处理。因此,我会让异常传播到上层,并为其提供通用处理。当然,这是个例外情况。 - davidxxx