2. Maven 依赖配置

在开始之前,先在 pom.xml 中添加以下依赖:

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

最新版本可在 Maven 中央仓库 查找。

3. 核心概念

使用 EasyMock 生成模拟对象时,我们可以模拟目标对象的行为,并最终验证其是否符合预期。操作分为四个步骤:

  1. 创建目标类的模拟对象
  2. 记录预期行为(包括动作、结果、异常等)
  3. 在测试中使用模拟对象
  4. 验证行为是否符合预期

记录完成后,需将模拟对象切换到"回放"模式,使其在协作时按记录行为执行。最后通过验证确保一切正常。

这些步骤对应 org.easymock.EasyMock 的核心方法:

  1. **mock(...)**:生成目标类(具体类或接口)的模拟对象。创建后处于"记录"模式,所有操作都会被记录
  2. **expect(...)**:设置预期行为(调用、结果、异常等)
  3. **replay(...)**:切换到"回放"模式,触发方法调用时返回记录的结果
  4. **verify(...)**:验证所有预期是否满足,且没有未预期的调用

4. 实战模拟示例

假设有一个 Baeldung 博客读者,喜欢浏览文章并尝试写作。先创建模型类:

public class BaeldungReader {

    private ArticleReader articleReader;
    private IArticleWriter articleWriter;

    // 构造方法

    public BaeldungArticle readNext(){
        return articleReader.next();
    }

    public List<BaeldungArticle> readTopic(String topic){
        return articleReader.ofTopic(topic);
    }

    public String write(String title, String content){
        return articleWriter.write(title, content);
    }
}

包含两个私有成员:articleReader(具体类)和 articleWriter(接口)。接下来我们将模拟它们来验证 BaeldungReader 的行为。

5. Java 代码模拟

5.1 基础模拟

当读者跳过文章时,预期 articleReader.next() 会被调用:

@Test
public void whenReadNext_thenNextArticleRead(){
    ArticleReader mockArticleReader = mock(ArticleReader.class);
    BaeldungReader baeldungReader
      = new BaeldungReader(mockArticleReader);

    expect(mockArticleReader.next()).andReturn(null);
    replay(mockArticleReader);

    baeldungReader.readNext();

    verify(mockArticleReader);
}

⚠️ 踩坑提示:即使不关心 mockArticleReader.next() 的返回值,也必须用 expect(...).andReturn(...) 指定返回值。对于有返回值的方法,直接调用会报错。

对于 void 方法,需用 expectLastCall()

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

5.2 回放顺序控制

若需严格验证调用顺序:

@Test
public void whenReadNextAndSkimTopics_thenAllAllowed(){
    ArticleReader mockArticleReader
      = strictMock(ArticleReader.class);
    BaeldungReade baeldungReader
      = new BaeldungReader(mockArticleReader);

    expect(mockArticleReader.next()).andReturn(null);
    expect(mockArticleReader.ofTopic("easymock")).andReturn(null);
    replay(mockArticleReader);

    baeldungReader.readNext();
    baeldungReader.readTopic("easymock");

    verify(mockArticleReader);
}

使用 strictMock(...) 会严格检查方法调用顺序。而 niceMock(...) 则允许任意调用:

@Test
public void whenReadNextAndOthers_thenAllowed(){
    ArticleReader mockArticleReader = niceMock(ArticleReader.class);
    BaeldungReade baeldungReader = new BaeldungReader(mockArticleReader);

    expect(mockArticleReader.next()).andReturn(null);
    replay(mockArticleReader);

    baeldungReader.readNext();
    baeldungReader.readTopic("easymock"); // 未预期但允许

    verify(mockArticleReader);
}

5.3 异常模拟

模拟接口 IArticleWriter 并验证异常抛出:

@Test
public void whenWriteMaliciousContent_thenArgumentIllegal() {
    // 模拟和初始化代码

    expect(mockArticleWriter
      .write("easymock","<body onload=alert('baeldung')>"))
      .andThrow(new IllegalArgumentException());
    replay(mockArticleWriter);

    // 执行写入并捕获异常

    verify(mockArticleWriter);
    assertEquals(
      IllegalArgumentException.class, 
      expectedException.getClass());
}

通过 expect(...).andThrow(...) 记录预期行为:当检测到 XSS 攻击时抛出 IllegalArgumentException

6. 注解方式模拟

EasyMock 支持通过注解注入模拟对象。需使用 EasyMockRunner 运行测试

@RunWith(EasyMockRunner.class)
public class BaeldungReaderAnnotatedTest {

    @Mock
    ArticleReader mockArticleReader;

    @TestSubject
    BaeldungReader baeldungReader = new BaeldungReader();

    @Test
    public void whenReadNext_thenNextArticleRead() {
        expect(mockArticleReader.next()).andReturn(null);
        replay(mockArticleReader);
        baeldungReader.readNext();
        verify(mockArticleReader);
    }
}

@Mock 注解的字段会被自动注入模拟对象,并注入到 @TestSubject 注解的类中。

若需使用其他测试运行器,可用 JUnit 规则 EasyMockRule

public class BaeldungReaderAnnotatedWithRuleTest {

    @Rule
    public EasyMockRule mockRule = new EasyMockRule(this);

    // 其他代码...

    @Test
    public void whenReadNext_thenNextArticleRead(){
        expect(mockArticleReader.next()).andReturn(null);
        replay(mockArticleReader);
        baeldungReader.readNext();
        verify(mockArticleReader);
    }
}

7. 使用 EasyMockSupport 批量管理

当测试中需要多个模拟对象时,手动重复 replay()verify() 很繁琐:

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

EasyMockSupport 类提供优雅解决方案

public class BaeldungReaderMockSupportTest 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(); // 批量回放

        // 执行读写操作
 
        verifyAll(); // 批量验证
 
        assertEquals(
          NoSuchElementException.class, 
          expectedException.getClass());
        assertEquals("BAEL-201801", articleId);
    }
}

关键点:

  • replayAll() / verifyAll() 批量操作
  • times(...) 指定方法调用次数,避免重复代码

也可通过委托方式使用:

EasyMockSupport easyMockSupport = new EasyMockSupport();

@Test
public void whenReadAndWriteSequencially_thenWorks(){
    ArticleReader mockArticleReader = easyMockSupport
      .createMock(ArticleReader.class);
    IArticleWriter mockArticleWriter = easyMockSupport
      .createMock(IArticleWriter.class);
    BaeldungReader baeldungReader = new BaeldungReader(
      mockArticleReader, mockArticleWriter);

    expect(mockArticleReader.next()).andReturn(null);
    expect(mockArticleWriter.write("title", "content"))
      .andReturn("");
    easyMockSupport.replayAll();

    baeldungReader.readNext();
    baeldungReader.write("title", "content");

    easyMockSupport.verifyAll();
}

8. 总结

本文介绍了 EasyMock 的核心用法:

  • 生成模拟对象
  • 记录和回放行为
  • 验证行为符合预期

✅ 优势:简单粗暴的 API,适合快速构建测试模拟
❌ 局限:相比 Mockito 等工具,社区活跃度稍低

对比 EasyMock、Mockito 和 JMockit 的详细分析,可参考这篇对比文章。完整示例代码见 GitHub 仓库


原始标题:Introduction to EasyMock | Baeldung