EasyMock与Mockito:设计 vs 可维护性?

58

有一种思考方式是:如果我们关心代码的设计,那么EasyMock是更好的选择,因为它通过期望的概念向您提供反馈。

如果我们关心测试的可维护性(易于阅读、编写和具有较少的易变磨损测试),那么Mockito似乎是更好的选择。

我的问题是:

  • 如果你在大型项目中使用了EasyMock,你是否发现你的测试难以维护?
  • 除了端到端测试之外,Mockito有哪些限制?

1
对我来说,这个问题很简单。Mockito是首选。EasyMock(使用这种奇怪的replay()莫名其妙)非常不直观。使用EasyMock就像把草叉插进你的眼睛里一样。 - Arunav Sanyal
5个回答

124

我不会争论这些框架的测试可读性、大小或测试技术,我认为它们是相等的,但是在一个简单的例子中,我将向您展示它们之间的区别。

假设我们有一个类,负责在某个地方存储一些东西:

public class Service {

    public static final String PATH = "path";
    public static final String NAME = "name";
    public static final String CONTENT = "content";
    private FileDao dao;

    public void doSomething() {
        dao.store(PATH, NAME, IOUtils.toInputStream(CONTENT));
    }

    public void setDao(FileDao dao) {
        this.dao = dao;
    }
}

我们想要测试它:

Mockito:

public class ServiceMockitoTest {

    private Service service;

    @Mock
    private FileDao dao;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        service = new Service();
        service.setDao(dao);
    }

    @Test
    public void testDoSomething() throws Exception {
        // given
        // when
        service.doSomething();
        // then
        ArgumentCaptor<InputStream> captor = ArgumentCaptor.forClass(InputStream.class);
        Mockito.verify(dao, times(1)).store(eq(Service.PATH), eq(Service.NAME), captor.capture());
        assertThat(Service.CONTENT, is(IOUtils.toString(captor.getValue())));
    }
}

EasyMock:

public class ServiceEasyMockTest {
    private Service service;
    private FileDao dao;

    @Before
    public void setUp() {
        dao = EasyMock.createNiceMock(FileDao.class);
        service = new Service();
        service.setDao(dao);
    }

    @Test
    public void testDoSomething() throws Exception {
        // given
        Capture<InputStream> captured = new Capture<InputStream>();
        dao.store(eq(Service.PATH), eq(Service.NAME), capture(captured));
        replay(dao);
        // when
        service.doSomething();
        // then
        assertThat(Service.CONTENT, is(IOUtils.toString(captured.getValue())));
        verify(dao);
    }
}

如您所见,这两个测试用例几乎相同,并且都通过了。 现在,让我们想象一下其他人更改了服务实现并尝试运行这些测试。

新的服务实现:

dao.store(PATH + separator, NAME, IOUtils.toInputStream(CONTENT));

在PATH常量的末尾添加了分隔符。

现在测试结果会是什么样子?首先,两个测试都会失败,但出现不同的错误消息:

EasyMock:

java.lang.AssertionError: Nothing captured yet
    at org.easymock.Capture.getValue(Capture.java:78)
    at ServiceEasyMockTest.testDoSomething(ServiceEasyMockTest.java:36)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)

Mockito:

Argument(s) are different! Wanted:
dao.store(
    "path",
    "name",
    <Capturing argument>
);
-> at ServiceMockitoTest.testDoSomething(ServiceMockitoTest.java:34)
Actual invocation has different arguments:
dao.store(
    "path\",
    "name",
    java.io.ByteArrayInputStream@1c99159
);
-> at Service.doSomething(Service.java:13)

为什么 EasyMock 测试中的结果没有被捕获?store 方法没有执行吗?等等,它执行了吧,为什么 EasyMock 对我们说谎了?

这是因为 EasyMock 把存根和验证两个责任混合在一条语句中。所以当出现问题时,很难理解哪个部分导致了失败。

当然您可以告诉我 - 只需更改测试并将 verify 移到断言之前。哇,您是认真的吗,开发人员应记住由模拟框架强制执行的某些神奇顺序吗?

顺便说一下,这是无法解决问题的:

java.lang.AssertionError: 
  Expectation failure on verify:
    store("path", "name", capture(Nothing captured yet)): expected: 1, actual: 0
    at org.easymock.internal.MocksControl.verify(MocksControl.java:111)
    at org.easymock.classextension.EasyMock.verify(EasyMock.java:211)

尽管如此,它告诉我方法未被执行,但实际上是执行了的,只是使用了其他参数。

为什么Mockito更好?这个框架不会在一个地方混合两个职责,当你的测试失败时,你将很容易理解失败的原因。


1
我决定先尝试使用 Mockito。测试的可维护性已经够混乱了,没有必要再让模拟框架添乱。 - Gary
我知道这是老问题了...但是,你在这里混淆了验证和存根过程,而不是EasyMock。在验证之前进行断言是不好的实践-因此,您实际上可以删除“assertThat”调用,因为验证会为您捕获它:“FileStore.dao(“path”,“name”,capture(Nothing Captured yet)):expected 1,actual: 0”。我会承认Mockito消息在这里更清晰,但在这个过于具体的例子中,这还不足以影响某人对EasyMock的决定! - Bob Flannigon
1
我不确定我在EasyMock中混淆了什么,这就是它的工作方式,所以如果你有一个好的反例,我会很高兴看到它。我认为这是个人意见的问题,无论这个例子是否足够好来影响对这些框架的决策,我很高兴许多人发现它有价值。 - Rrr
2
在这个例子中,使用漂亮的模拟没有任何意义。您应该使用普通的模拟(即EasyMock.createMock(FileDao.class)),然后验证顺序并不重要,异常非常清晰: Unexpected method call store("path\", "name", java.io.ByteArrayInputStream@58651fd0): store("path", "name", capture(Nothing captured yet)): expected: 1, actual: 0 - Ian Jones
2
现在,EasyMock也有类似的@Mock注解,可以自动创建模拟对象并将其注入到被测试的类中。 - Ian Jones
如果看到EasyMock的java.lang.AssertionError: Nothing captured yet这种不可理解的错误信息,我会很生气。仅凭这个错误信息就足以说明EasyMock并没有为开发人员的体验进行优化。 - HenryNguyen

53

如果我们关心代码设计,那么Easymock是更好的选择,因为它通过期望概念给出反馈。

有趣。我发现,“期望概念”让许多开发人员在测试中加入越来越多的期望,只是为了满足UnexpectedMethodCall问题。这会影响到设计吗?

测试不应该因为你修改了代码而失败,它应该是在功能停止工作时才会失败。如果一个人想让测试在任何代码更改时都失败,我建议编写一个断言Java文件md5校验和的测试 :)


32

我是一名EasyMock开发者,所以有些偏见,但我在大型项目上也使用过EasyMock。

我的观点是,EasyMock测试确实会偶尔出错。EasyMock强制你完全记录预期的内容。这需要一定的纪律性。你应该记录期望的内容,而不是被测试方法当前需要的内容。例如,如果不关心模拟对象上调用方法的次数,请放心地使用andStubReturn。同样地,如果不关心参数,请使用anyObject()等。考虑TDD思想可以帮助你做到这一点。

我的分析是,EasyMock测试会更频繁地失败,而Mockito的测试则会在你希望它们失败时不会失败。我更喜欢我的测试会失败。至少我知道了我的开发产生了什么影响。当然,这只是我的个人观点。


1
是的,我也一直在思考同样的问题:使用Mockito(以及类似的Unitils Mock API)编写测试用例变得更加容易,即使在不应该通过的情况下也能够继续顺利地通过。我猜想这可能是Mockito等API被认为“更容易”的主要原因,因为它们可以帮助创建过于“宽松”的测试用例。 - Rogério
6
我很想看到一个对比这两种方法的例子... - Armand
我记得曾经将测试过的代码复制粘贴到单元测试中,并将其“重新格式化”为模拟定义。只是因为这比手动编写更快。如果您开始像这样编写测试,那么肯定有些问题。一个测试代码的副本怎么可能成为一个好的单元测试呢? - Stefan Steinegger
严格方法的另一个问题是你需要在每个单元测试(方法)中从头开始设置每个模拟。虽然“好”的模拟可以在设置方法中大部分设置一次(比如当调用getName时,“Hugo”将被返回)。每个测试方法只需编写与其他测试方法不同的内容。因此,平均而言,你需要额外编写一行代码进行模拟,然后运行并验证你想要验证的内容。这实际上是我在使用模拟编写单元测试时遇到的最大区别。三行代码的单元测试。你将测试更多的情况。 - Stefan Steinegger

7

我认为你不必过于担心这个问题。Easymock和Mockito都可以配置为“严格”或“友好”,唯一的区别是默认情况下Easymock是严格模式,而Mockito是友好模式。

与所有测试一样,没有硬性规定,需要在测试可靠性和可维护性之间取得平衡。通常我发现某些功能或技术领域需要高度可靠性,因此我会使用“严格”的mocks。例如,我们可能不希望debitAccount()方法被调用超过一次!但是,在其他情况下,mock只是一个存根,因此我们可以测试代码的真正“核心”。

在Mockito的早期生命周期中,API兼容性是一个问题,但现在更多的工具支持该框架。Powermock(个人喜爱)现在有一个mockito扩展。


5

说实话,我更喜欢Mockito。我之前一直使用unitils和EasyMock的组合,但这两者经常会导致异常,如IllegalArgumentException: not an interface和MissingBehaviorExceptions。不过,在这些情况下,我的代码和测试代码都是完全正确的。看起来,MissingBehaviorException是由于使用createMock(使用类扩展!!)创建的模拟对象引起的。当使用@Mock时,它确实有效!我不喜欢那种误导性的行为,对我来说,这清楚地表明了开发人员并不知道他们在做什么。一个好的框架应该始终易于使用且不含糊不清。IllegalArgumentException也是由于EasyMock内部的某些混乱原因导致的。此外,录制不是我想要做的事情。我想测试我的代码是否会抛出异常以及是否返回预期结果。结合代码覆盖率来看,这就是适合我的工具。我不希望我的测试因为在上一行或下一行加入1行代码而失败,因为这会提高性能等原因。使用Mockito没有问题,但使用EasyMock则会导致测试失败,即使代码没有错误。那很糟糕,浪费时间和金钱。你想测试预期的行为,你真的关心事情的顺序吗?我想在极少数情况下可能会关心,那就使用Easymock。在其他情况下,我认为你会花更少的时间使用Mockito来编写你的测试。

顺祝商祺, 劳伦斯


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接