Easymock 简介
1. 简介
在本教程中,我们将介绍另一个模拟工具——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 的模拟包括四个步骤:
- 创建目标类的模拟
- 记录其预期的行为,包括动作、结果、异常等。
- 在测试中使用模拟
- 验证它是否按预期运行
录制完成后,我们将其切换到“重播”模式,以便模拟在与将使用它的任何对象协作时表现得与录制的一样。
最终,我们验证一切是否按预期进行。
上面提到的四个步骤与org.easymock.EasyMock 中的方法有关:
- mock(…) :生成目标类的模拟,无论是具体类还是接口。一旦创建,模拟就处于“记录”模式,这意味着 EasyMock 将记录模拟对象所采取的任何动作,并以“重播”模式重播它们
- expect(…) :使用此方法,我们可以为相关的录制操作设置期望,包括调用、结果和异常
- replay(…) :将给定的模拟切换到“重播”模式。然后,任何触发先前记录的方法调用的动作都将重放“记录的结果”
- 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);
}
}
在这里,我们模拟了articleReader和articleWriter。在将这些模拟设置为“重播”模式时,我们使用了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 有任何名称冲突或有任何类似情况,这可能有助于避免混淆。