EasyMock简介

2023/05/09

1. 简介

之前,我们广泛讨论了JMockitMockito

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

2. Maven依赖

在深入研究之前,让我们将以下依赖项添加到我们的pom.xml中:

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

最新版本的依赖项可以从这里获取。

3. 核心概念

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

使用EasyMock的mock包括四个步骤:

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

录制完成后,我们将其切换到“重播”模式,以便在与任何将使用它的对象协作时,mock的行为与录制的行为相同。

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

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

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

在下一节中,我们通过真实的示例演示这些步骤是如何工作的。

4. Mock的一个实际例子

假设我们有一个Tuyucheng博客的读者,他/她喜欢浏览网站上的文章,然后他/她尝试写文章。

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

public class TuyuchengReader {

    private ArticleReader articleReader;
    private ArticleWriter articleWriter;

    // constructors

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

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

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

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

接下来,我们将mock它们以验证TuyuchengReader的行为。

5. 使用Java代码Mock

让我们从mock一个ArticleReader开始。

5.1 典型的Mock

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

class TuyuchengReaderUnitTest {
    private TuyuchengReader tuyuchengReader;
    private ArticleReader mockArticleReader;
    private ArticleWriter mockArticleWriter;

    @Test
    void givenTuyuchengReader_whenReadNext_thenNextArticleRead() {
        mockArticleReader = mock(ArticleReader.class);
        tuyuchengReader = new TuyuchengReader(mockArticleReader);

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

        TuyuchengArticle article = tuyuchengReader.readNext();
        verify(mockArticleReader);
        assertNull(article);
    }
}

在上面的示例代码中,我们严格遵循使用EasyMock的四个步骤并mock 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
void givenTuyuchengReader_whenReadNextAndSkimTopics_thenAllAllowed() {
	mockArticleReader = strictMock(ArticleReader.class);
	tuyuchengReader = new TuyuchengReader(mockArticleReader);
    
	expect(mockArticleReader.next()).andReturn(null);
	expect(mockArticleReader.ofTopic("easymock")).andReturn(null);
	replay(mockArticleReader);
    
	tuyuchengReader.readNext();
	tuyuchengReader.readTopic("easymock");
    
	verify(mockArticleReader);
}

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

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

@Test
void givenTuyuchengReader_whenReadNextAndOthers_thenAllowed() {
	mockArticleReader = niceMock(ArticleReader.class);
	tuyuchengReader = new TuyuchengReader(mockArticleReader);
    
	expect(mockArticleReader.next()).andReturn(null);
	replay(mockArticleReader);
    
	tuyuchengReader.readNext();
	tuyuchengReader.readTopic("easymock");
    
	verify(mockArticleReader);
}

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

5.3 Mock异常抛出

现在,让我们继续mock接口ArticleWriter,以及如何处理预期的Throwables:

@Test
void givenTuyuchengReader_whenWriteMaliciousContent_thenArgumentIllegal() {
	mockArticleWriter = mock(ArticleWriter.class);
	tuyuchengReader = new TuyuchengReader(mockArticleWriter);
    
	expect(mockArticleWriter.write("easymock", "<body onload=alert('tuyucheng')>")).andThrow(new IllegalArgumentException());
	replay(mockArticleWriter);
    
	Exception expectedException = null;
	try {
		tuyuchengReader.write("easymock", "<body onload=alert('tuyucheng')>");
	} catch (Exception exception) {
		expectedException = exception;
	}
    
	verify(mockArticleWriter);
    
	assertEquals(IllegalArgumentException.class, expectedException.getClass());
}

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

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

6. 使用注解的mock

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

让我们重写前面的代码片段:

@RunWith(EasyMockRunner.class)
public class TuyuchengReaderAnnotatedTest {

    @Mock
    ArticleReader mockArticleReader;

    @TestSubject
    TuyuchengReader tuyuchengReader = new TuyuchengReader();

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

等效于mock(…),mock将被注入到使用@Mock注解的字段中。这些mock将被注入到使用@TestSubject标注的类的字段中。

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

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

请注意,如果我们想使用另一个测试Runner而不是EasyMockRunner,我们可以使用JUnit测试Rule EasyMockRule

public class TuyuchengReaderAnnotatedWithRuleUnitTest {

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

    // Mock Object with Annotation ...

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

7. 使用EasyMockSupport进行Mock

有时我们需要在单个测试中引入多个mock,我们必须手动重复:

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

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

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

@RunWith(EasyMockRunner.class)
public class TuyuchengReaderMockSupportUnitTest extends EasyMockSupport {

    // Mock Object With Annotation ...

    @Test
    public void givenTuyuchengReader_whenReadAndWriteSequencially_thenWorks() {
        expect(mockArticleReader.next())
              .andReturn(null)
              .times(2)
              .andThrow(new NoSuchElementException());
        expect(mockArticleWriter.write("title", "content")).andReturn("Tuyucheng-201801");
        replayAll();

        Exception expectedException = null;
        try {
            for (int i = 0; i < 3; i++) {
                tuyuchengReader.readNext();
            }
        } catch (Exception exception) {
            expectedException = exception;
        }
        String articleId = tuyuchengReader.write("title", "content");
        verifyAll();

        assertEquals(NoSuchElementException.class, expectedException.getClass());
        assertEquals("Tuyucheng-201801", articleId);
    }
}

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

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

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

public class TuyuchengReaderMockDelegationUnitTest {

    EasyMockSupport easyMockSupport = new EasyMockSupport();

    @Test
    public void givenTuyuchengReader_whenReadAndWriteSequencially_thenWorks() {
        ArticleReader mockArticleReader = easyMockSupport.createMock(ArticleReader.class);
        ArticleWriter mockArticleWriter = easyMockSupport.createMock(ArticleWriter.class);
        TuyuchengReader tuyuchengReader = new TuyuchengReader(mockArticleReader, mockArticleWriter);

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

        tuyuchengReader.readNext();
        tuyuchengReader.write("title", "content");
        easyMockSupport.verifyAll();
    }
}

之前,我们使用静态方法或注解来创建和管理mock,在幕后,这些静态和带注解的mock由全局EasyMockSupport实例控制。

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

8. 总结

在本文中,我们简要介绍了EasyMock的基本用法,关于如何生成mock对象,记录和重播它们的行为,以及验证它们的行为是否正确。

如果你可能感兴趣,请查看本文以了解EasyMock、Mockito和JMockit的区别。

与往常一样,本教程的完整源代码可在GitHub上获得。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章