Contents

Easymock 简介

1. 简介

过去,我们广泛讨论了JMockitMockito

在本教程中,我们将介绍另一个模拟工具——EasyMock

2. Maven依赖

在开始之前,让我们将以下依赖项添加到我们的pom.xml中:

<dependency>
    <groupId>org.easymock</groupId>
    <artifactId>easymock</artifactId>
    <version>3.5.1</version>
    <scope>test</scope>
</dependency>

最新版本总是可以在这里 找到。

3. 核心概念

在生成 mock 时,我们可以模拟目标对象,指定它的行为,最后验证它是否按预期使用。

使用 EasyMock 的模拟包括四个步骤:

  1. 创建目标类的模拟
  2. 记录其预期的行为,包括动作、结果、异常等。
  3. 在测试中使用模拟
  4. 验证它是否按预期运行

录制完成后,我们将其切换到“重播”模式,以便模拟在与将使用它的任何对象协作时表现得与录制的一样。

最终,我们验证一切是否按预期进行。

上面提到的四个步骤与org.easymock.EasyMock 中的方法有关:

  1. mock(…) :生成目标类的模拟,无论是具体类还是接口。一旦创建,模拟就处于“记录”模式,这意味着 EasyMock 将记录模拟对象所采取的任何动作,并以“重播”模式重播它们
  2. expect(…) :使用此方法,我们可以为相关的录制操作设置期望,包括调用、结果和异常
  3. replay(…) :将给定的模拟切换到“重播”模式。然后,任何触发先前记录的方法调用的动作都将重放“记录的结果”
  4. verify(…) :验证是否满足所有期望,并且没有在模拟上执行意外调用

在下一节中,我们将使用真实世界的示例展示这些步骤如何在实际中发挥作用。

4. Mocking 的一个实际例子

在继续之前,让我们看一下示例上下文:假设我们有一个 Blogdemo 博客的读者,他喜欢浏览网站上的文章,然后他/她尝试写文章。

让我们从创建以下模型开始:

public class BlogdemoReader {
    private ArticleReader articleReader;
    private IArticleWriter articleWriter;
    // constructors
    public BlogdemoArticle readNext(){
        return articleReader.next();
    }
    public List<BlogdemoArticle> readTopic(String topic){
        return articleReader.ofTopic(topic);
    }
    public String write(String title, String content){
        return articleWriter.write(title, content);
    }
}

在这个模型中,我们有两个私有成员:articleReader(一个具体类)和articleWriter(一个接口)。

接下来,我们将模拟它们以验证BlogdemoReader的行为。

5. 用 Java 代码模拟

让我们从模拟 ArticleReader开始。

5.1. 典型的Mock

我们希望在读者跳过文章时调用*articleReader.next()*方法:

@Test
public void whenReadNext_thenNextArticleRead(){
    ArticleReader mockArticleReader = mock(ArticleReader.class);
    BlogdemoReader blogdemoReader
      = new BlogdemoReader(mockArticleReader);
    expect(mockArticleReader.next()).andReturn(null);
    replay(mockArticleReader);
    blogdemoReader.readNext();
    verify(mockArticleReader);
}

在上面的示例代码中,我们严格遵循 4 步过程并模拟ArticleReader类。

虽然我们真的不关心*mockArticleReader.next()*返回什么,但我们仍然需要使用*expect(…).andReturn(…)mockArticleReader.next()指定一个返回值。*

使用expect(…),EasyMock 期望该方法返回一个值或抛出一个异常。

如果我们只是这样做:

mockArticleReader.next();
replay(mockArticleReader);

EasyMock 会抱怨这一点,因为如果方法返回任何内容,它需要调用expect(…).andReturn(…)

如果它是一个void方法,我们可以使用expectLastCall() 来期待它的动作,如下所示:

mockArticleReader.someVoidMethod();
expectLastCall();
replay(mockArticleReader);

5.2. 重播顺序

如果我们需要按特定顺序重放操作,我们可以更严格:

@Test
public void whenReadNextAndSkimTopics_thenAllAllowed(){
    ArticleReader mockArticleReader
      = strictMock(ArticleReader.class);
    BlogdemoReade blogdemoReader
      = new BlogdemoReader(mockArticleReader);
    expect(mockArticleReader.next()).andReturn(null);
    expect(mockArticleReader.ofTopic("easymock")).andReturn(null);
    replay(mockArticleReader);
    blogdemoReader.readNext();
    blogdemoReader.readTopic("easymock");
    verify(mockArticleReader);
}

在这个片段中,我们*使用*strictMock(…) 来检查方法调用的顺序。对于由mock(…)strictMock(…)创建的模拟,任何意外的方法调用都会导致AssertionError

要允许任何方法调用模拟,我们可以使用niceMock(…)

@Test
public void whenReadNextAndOthers_thenAllowed(){
    ArticleReader mockArticleReader = niceMock(ArticleReader.class);
    BlogdemoReade blogdemoReader = new BlogdemoReader(mockArticleReader);
    expect(mockArticleReader.next()).andReturn(null);
    replay(mockArticleReader);
    blogdemoReader.readNext();
    blogdemoReader.readTopic("easymock");
    verify(mockArticleReader);
}

在这里,我们没想到会调用blogdemoReader.readTopic(…),但 EasyMock 不会抱怨。使用niceMock(…), EasyMock 现在只关心目标对象是否执行了预期的操作。

5.3. 模拟异常抛出

现在,让我们继续模拟接口IArticleWriter,以及如何处理预期的Throwables

@Test
public void whenWriteMaliciousContent_thenArgumentIllegal() {
    // mocking and initialization
    expect(mockArticleWriter
      .write("easymock","<body onload=alert('blogdemo')>"))
      .andThrow(new IllegalArgumentException());
    replay(mockArticleWriter);
    // write malicious content and capture exception as expectedException
    verify(mockArticleWriter);
    assertEquals(
      IllegalArgumentException.class, 
      expectedException.getClass());
}

在上面的代码片段中,我们希望articleWriter足够可靠,可以检测XSS(跨站点脚本)攻击。

所以当读者试图在文章内容中注入恶意代码时,作者应该抛出一个IllegalArgumentException。我们使用*expect(…).andThrow(…)*记录了这种预期的行为。

6. 带注释的模拟

EasyMock 还支持使用注解注入模拟。要使用它们,我们需要使用EasyMockRunner 运行我们的单元测试,以便它处理@Mock@TestSubject 注释。

让我们重写之前的片段:

@RunWith(EasyMockRunner.class)
public class BlogdemoReaderAnnotatedTest {
    @Mock
    ArticleReader mockArticleReader;
    @TestSubject
    BlogdemoReader blogdemoReader = new BlogdemoReader();
    @Test
    public void whenReadNext_thenNextArticleRead() {
        expect(mockArticleReader.next()).andReturn(null);
        replay(mockArticleReader);
        blogdemoReader.readNext();
        verify(mockArticleReader);
    }
}

等效于mock(…),一个 mock 将被注入到带有*@Mock注解的字段中。这些模拟将被注入到使用@TestSubject*注解的类的字段中。

在上面的代码片段中,我们没有显式初始化blogdemoReader 中的articleReader字段。当调用blogdemoReader.readNext()时,我们可以隐式调用mockArticleReader

那是因为mockArticleReader被注入到articleReader字段中。

请注意,如果我们想使用另一个测试运行程序而不是EasyMockRunner,我们可以使用 JUnit 测试规则EasyMockRule

public class BlogdemoReaderAnnotatedWithRuleTest {
    @Rule
    public EasyMockRule mockRule = new EasyMockRule(this);
    //...
    @Test
    public void whenReadNext_thenNextArticleRead(){
        expect(mockArticleReader.next()).andReturn(null);
        replay(mockArticleReader);
        blogdemoReader.readNext();
        verify(mockArticleReader);
    }
}

7. 使用EasyMockSupport 进行模拟

有时我们需要在一个测试中引入多个模拟,我们不得不手动重复:

replay(A);
replay(B);
replay(C);
//...
verify(A);
verify(B);
verify(C);

这很丑陋,我们需要一个优雅的解决方案。

幸运的是,我们在 EasyMock 中有一个 EasyMockSupport 类来帮助处理这个问题。它有助于跟踪模拟,以便我们可以像这样批量重放和验证它们:

//...
public class BlogdemoReaderMockSupportTest extends EasyMockSupport{
    //...
    @Test
    public void whenReadAndWriteSequencially_thenWorks(){
        expect(mockArticleReader.next()).andReturn(null)
          .times(2).andThrow(new NoSuchElementException());
        expect(mockArticleWriter.write("title", "content"))
          .andReturn("BAEL-201801");
        replayAll();
        // execute read and write operations consecutively
 
        verifyAll();
 
        assertEquals(
          NoSuchElementException.class, 
          expectedException.getClass());
        assertEquals("BAEL-201801", articleId);
    }
}

在这里,我们模拟了articleReaderarticleWriter。在将这些模拟设置为“重播”模式时,我们使用了EasyMockSupport提供的静态方法replayAll() ,并使用verifyAll() 批量验证它们的行为。

我们还在expect阶段引入了times(…) 方法。它有助于指定我们期望该方法被调用多少次,这样我们就可以避免引入重复的代码。

我们也可以通过委托使用EasyMockSupport

EasyMockSupport easyMockSupport = new EasyMockSupport();
@Test
public void whenReadAndWriteSequencially_thenWorks(){
    ArticleReader mockArticleReader = easyMockSupport
      .createMock(ArticleReader.class);
    IArticleWriter mockArticleWriter = easyMockSupport
      .createMock(IArticleWriter.class);
    BlogdemoReader blogdemoReader = new BlogdemoReader(
      mockArticleReader, mockArticleWriter);
    expect(mockArticleReader.next()).andReturn(null);
    expect(mockArticleWriter.write("title", "content"))
      .andReturn("");
    easyMockSupport.replayAll();
    blogdemoReader.readNext();
    blogdemoReader.write("title", "content");
    easyMockSupport.verifyAll();
}

以前,我们使用静态方法或注释来创建和管理模拟。在后台,这些静态和带注释的模拟由全局EasyMockSupport实例控制。

在这里,我们显式地实例化了它,并通过委托将所有这些模拟置于我们自己的控制之下。如果我们的测试代码与 EasyMock 有任何名称冲突或有任何类似情况,这可能有助于避免混淆。