测试驱动的开发是软件开发的重要组成部分。 如果未测试代码,则该代码已损坏。 所有代码都必须经过测试,理想情况下,应在模型代码之前编写测试。 但是有些事情比其他事情更容易测试。 如果您要编写一个简单的类来表示货币,则可以很容易地测试出可以将$ 1.23加到$ 2.28并获得$ 4.03,而不是$ 3.03或4.029999998。 测试创建像7.465美元这样的货币并不是难事。 但是,您如何测试将7.50美元转换为5.88欧元的方法,尤其是当通过连接到实时数据库并每秒更新一次信息来找到汇率时? 每当您运行该程序时, amount.toEuros()
的正确结果可能会更改。
答案是模拟对象 。 测试未连接到提供最新汇率信息的真实服务器,而是连接到始终返回相同汇率的模拟服务器。 这样您便可以测试出可预测的结果。 毕竟,目标是测试toEuros()
方法中的逻辑,而toEuros()
测试服务器是否发送了正确的值。 (让构建服务器的开发人员对此有所担心。)这种模拟对象有时称为false 。
模拟对象对于测试错误条件也很有用。 例如,如果toEuros()
方法尝试获取最新汇率,但网络中断,会发生什么情况? 您可以从计算机上拔下以太网电缆,然后运行测试,但是编写模拟网络故障的模拟对象的工作量大大减少。
模拟对象也可以用来监视类的行为。 通过将断言放置在模拟代码中,您可以验证被测代码是否在正确的时间将正确的参数传递给其协作者。 模拟可以让您查看和测试类的私有部分,而无需通过其他不必要的公共方法公开它们。
最后,模拟对象有助于从测试中删除较大的依赖项。 它们使测试更加统一。 包含模拟对象的测试失败很可能是被测试方法的失败,而不是其依赖项之一。 这有助于隔离问题并简化调试。
EasyMock是Java编程语言的开源模拟对象库,可帮助您快速轻松地创建用于所有这些目的的模拟对象。 通过动态代理的魔力,EasyMock使您能够只用一行代码就可以创建任何接口的基本实现。 通过添加EasyMock类扩展,您也可以为类创建模拟。 可以出于各种目的对这些模拟进行配置,范围从用于填充方法签名的简单伪参数到用于验证较长方法调用序列的多调用间谍。
介绍EasyMock
我将从一个具体的示例开始,以演示EasyMock的工作方式。 清单1是假设的ExchangeRate
接口。 像任何接口一样,它仅说明实例的作用而无需指定实例的操作方式。 例如,它并没有说汇率数据是来自雅虎财务部门,政府还是其他地方。
清单1. ExchangeRate
import java.io.IOException;
public interface ExchangeRate {
double getRate(String inputCurrency, String outputCurrency) throws IOException;
}
清单2是假定的Currency
类的框架。 它实际上相当复杂,并且很可能包含错误。 (我会为您悬念:存在错误-实际上有很多错误。)
清单2. Currency
类
import java.io.IOException;
public class Currency {
private String units;
private long amount;
private int cents;
public Currency(double amount, String code) {
this.units = code;
setAmount(amount);
}
private void setAmount(double amount) {
this.amount = new Double(amount).longValue();
this.cents = (int) ((amount * 100.0) % 100);
}
public Currency toEuros(ExchangeRate converter) {
if ("EUR".equals(units)) return this;
else {
double input = amount + cents/100.0;
double rate;
try {
rate = converter.getRate(units, "EUR");
double output = input * rate;
return new Currency(output, "EUR");
} catch (IOException ex) {
return null;
}
}
}
public boolean equals(Object o) {
if (o instanceof Currency) {
Currency other = (Currency) o;
return this.units.equals(other.units)
&& this.amount == other.amount
&& this.cents == other.cents;
}
return false;
}
public String toString() {
return amount + "." + Math.abs(cents) + " " + units;
}
}
乍一看,关于Currency
类设计的重要事项可能并不明显。 汇率是从班级外部传递的。 它不是在类内部构造的。 这对于使我能够模拟汇率至关重要,因此测试可以在不与真实汇率服务器对话的情况下运行。 它还使客户端应用程序可以提供不同的汇率数据源。
清单3演示了一个JUnit测试,该测试验证汇率为1.5时,2.50美元是否可以转换为3.75欧元。 EasyMock用于创建始终提供值1.5的ExchangeRate
对象。
清单3. CurrencyTest
类
import junit.framework.TestCase;
import org.easymock.EasyMock;
import java.io.IOException;
public class CurrencyTest extends TestCase {
public void testToEuros() throws IOException {
Currency testObject = new Currency(2.50, "USD");
Currency expected = new Currency(3.75, "EUR");
ExchangeRate mock = EasyMock.createMock(ExchangeRate.class);
EasyMock.expect(mock.getRate("USD", "EUR")).andReturn(1.5);
EasyMock.replay(mock);
Currency actual = testObject.toEuros(mock);
assertEquals(expected, actual);
}
}
运行此测试,测试通过。 发生了什么? 让我们逐行查看测试。 首先构造测试对象和预期结果:
Currency testObject = new Currency(2.50, "USD");
Currency expected = new Currency(3.75, "EUR");
尚无新内容。
接下来,我通过将该接口的Class
对象传递给静态EasyMock.createMock()
方法来创建ExchangeRate
接口的模拟版本:
ExchangeRate mock = EasyMock.createMock(ExchangeRate.class);
到目前为止,这是最奇怪的部分。 请注意,我从来没有编写实现ExchangeRate
接口的类。 此外,绝对不可能输入EasyMock.createMock()
方法来返回ExchangeRate
的实例,该实例是它从不知道的,而我只是为本文创建的。 即使通过奇迹返回ExchangeRate
,当我需要模拟其他接口的实例时会发生什么呢?
我第一次看到这个,我几乎从椅子上掉下来了。 我不相信该代码可以编译,但是确实可以。 这里有深奥的黑魔法,它来自Java 5泛型和动态代理的组合,这些代理早在Java 1.3中就已引入(请参阅参考资料 )。 幸运的是,您不需要了解使用它的方式(也不会因发明这些技巧的程序员的聪明而震惊)。
下一步同样令人惊讶。 为了告诉模拟程序期望什么,我将方法作为EasyMock.expect()
方法的参数进行调用。 然后,我调用andReturn()
来指定调用此方法后应显示的内容:
EasyMock.expect(mock.getRate("USD", "EUR")).andReturn(1.5);
EasyMock记录了此调用,因此它知道以后要播放什么。
接下来,我准备通过调用EasyMock.replay()
方法来模拟播放其记录的数据:
EasyMock.replay(mock);
这是我感到有些困惑的设计作品之一。 EasyMock.replay()
实际上不会重播该模拟。 相反,它将重置模拟,以便下次调用其方法时将开始重播。
现在已经准备好模拟,我将其作为参数传递给被测方法:
Currency actual = testObject.toEuros(mock);
最后,我验证答案是否符合预期:
assertEquals(expected, actual);
这就是全部。 每当您有一个需要返回某些结果以进行测试的接口时,您都可以创建一个快速模拟。 它真的很容易。 ExchangeRate
接口足够小且简单,以至于我可以轻松地手动编写模拟类。 但是,接口越大,越复杂,为每个单元测试编写单独的模拟就越麻烦。 EasyMock使您可以在一行代码中创建大型接口(如java.sql.ResultSet
或org.xml.sax.ContentHandler
的实现,然后为其提供足够的行为来运行您的测试。
测试异常
模拟的更流行的用途之一是测试异常条件。 例如,您无法轻松地按需创建网络故障,但可以创建一个模拟故障的模拟。
当getRate()
抛出IOException
时, Currency
类应该返回null
。 清单4测试了这一点:
清单4.测试方法抛出正确的异常
public void testExchangeRateServerUnavailable() throws IOException {
Currency testObject = new Currency(2.50, "USD");
ExchangeRate mock = EasyMock.createMock(ExchangeRate.class);
EasyMock.expect(mock.getRate("USD", "EUR")).andThrow(new IOException());
EasyMock.replay(mock);
Currency actual = testObject.toEuros(mock);
assertNull(actual);
}
这里的新内容是andThrow()
方法。 您可能已经猜到了,这只是将getRate()
方法设置为在调用时抛出指定的异常。
只要方法签名支持,您就可以抛出自己喜欢的任何类型的异常(检查,运行时还是错误)。 这对于测试极不可能的情况(例如,找不到内存不足错误或未定义类)或指示虚拟机错误的条件(例如,没有可用的UTF-8字符编码)特别有用。
设定期望
EasyMock不仅提供罐装答案以响应罐装输入。 它还可以检查输入内容是否正确。 例如,假设toEuros()
方法具有清单5所示的错误,该错误以欧元返回结果,但获得加元的汇率。 这可能使某人赚很多钱或失去很多钱。
清单5.一个有问题的toEuros()
方法
public Currency toEuros(ExchangeRate converter) {
if ("EUR".equals(units)) return this;
else {
double input = amount + cents/100.0;
double rate;
try {
rate = converter.getRate(units, "CAD");
double output = input * rate;
return new Currency(output, "EUR");
} catch (IOException e) {
return null;
}
}
}
但是,我不需要为此进行其他测试。 清单4的testToEuros
将已经捕获了该错误。 当使用清单4中的错误代码运行此测试时,测试失败并显示以下错误消息:
"java.lang.AssertionError:
Unexpected method call getRate("USD", "CAD"):
getRate("USD", "EUR"): expected: 1, actual: 0".
请注意,这不是我所做的断言。 EasyMock注意到,我传递的参数并没有加总,使测试用例不合格。
默认情况下,EasyMock仅允许测试用例使用指定的参数调用指定的方法。 有时候,这有点太严格了,所以有一些方法可以放松一下。 例如,假设我确实希望允许将任何字符串传递给getRate()
方法,而不仅仅是USD
和EUR
。 然后,我可以指定希望使用EasyMock.anyObject()
而不是显式字符串,如下所示:
EasyMock.expect(mock.getRate(
(String) EasyMock.anyObject(),
(String) EasyMock.anyObject())).andReturn(1.5);
我可以有点挑剔,并指定EasyMock.notNull()
以仅允许非null
字符串:
EasyMock.expect(mock.getRate(
(String) EasyMock.notNull(),
(String) EasyMock.notNull())).andReturn(1.5);
静态类型检查将防止将非String
传递给此方法。 但是,现在除了USD
和EUR
之外,我还允许String
。 您可以使用EasyMock.matches()
使用正则表达式来使其更加明确。 在这里,我需要一个三个字母的大写ASCII String
:
EasyMock.expect(mock.getRate(
(String) EasyMock.matches("[A-Z][A-Z][A-Z]"),
(String) EasyMock.matches("[A-Z][A-Z][A-Z]"))).andReturn(1.5);
使用EasyMock.find()
代替EasyMock.matches()
将接受任何String
包含一个三资字母的子String
。
EasyMock对于原始数据类型具有类似的方法:
-
EasyMock.anyInt()
-
EasyMock.anyShort()
-
EasyMock.anyByte()
-
EasyMock.anyLong()
-
EasyMock.anyFloat()
-
EasyMock.anyDouble()
-
EasyMock.anyBoolean()
对于数字类型,还可以使用EasyMock.lt(x)
接受小于x
任何值,或使用EasyMock.gt(x)
接受大于x
任何值。
检查较长的期望序列时,可以捕获一个方法调用的结果或参数,并将其与传递给另一方法调用的值进行比较。 最后,您可以定义自定义匹配器,这些自定义匹配器检查可以想象的参数的几乎所有细节,尽管这样做的过程有些复杂。 但是,对于大多数测试,像EasyMock.anyInt()
, EasyMock.matches()
和EasyMock.eq()
这样的基本匹配器就足够了。
严格的模拟和订单检查
EasyMock不仅检查是否使用正确的参数调用了预期的方法。 它还可以验证您以正确的顺序调用了这些方法,并且仅调用了这些方法。 默认情况下不执行此检查。 要打开它,请在测试方法的末尾调用EasyMock.verify(mock)
。 例如,清单6现在将失败,如果toEuros()
方法调用getRate()
一次以上:
清单6.检查getRate()
仅被调用一次
public void testToEuros() throws IOException {
Currency expected = new Currency(3.75, "EUR");
ExchangeRate mock = EasyMock.createMock(ExchangeRate.class);
EasyMock.expect(mock.getRate("USD", "EUR")).andReturn(1.5);
EasyMock.replay(mock);
Currency actual = testObject.toEuros(mock);
assertEquals(expected, actual);
EasyMock.verify(mock);
}
究竟进行这种检查EasyMock.verify()
多少取决于它在哪种可用模式下运行:
- 正常—
EasyMock.createMock()
:必须使用指定的参数调用所有预期的方法。 但是,调用这些方法的顺序无关紧要。 调用意外方法会导致测试失败。 - Strict —
EasyMock.createStrictMock()
:必须使用指定参数的预期参数调用所有预期方法。 调用意外方法会导致测试失败。 - 好的—
EasyMock.createNiceMock()
:必须使用指定的参数以任何顺序调用所有期望的方法。 调用意外方法不会导致测试失败。 漂亮的模拟为未明确模拟的方法提供了合理的默认值。 返回数字的方法返回0
。 返回布尔值的方法返回false
。 返回对象的方法返回null
。
在较大的接口和较大的测试中,检查方法调用的顺序和次数更为有用。 例如,考虑org.xml.sax.ContentHandler
接口。 如果要测试XML解析器,则需要输入文档并验证解析器是否以正确的顺序在ContentHandler
中调用了正确的方法。 例如,考虑清单7中的简单XML文档:
清单7.一个简单的XML文档
<root>
Hello World!
</root>
根据SAX规范,当解析器解析此文档时,应按以下顺序调用这些方法:
-
setDocumentLocator()
-
startDocument()
-
startElement()
-
characters()
-
endElement()
-
endDocument()
但是,只是为了使事情变得有趣,对setDocumentLocator()
的调用是可选的。 允许解析程序多次调用characters()
。 他们不需要在单个调用中传递最大连续文本长度,实际上大多数不需要。 即使对于清单7这样的简单文档,也很难用传统方法进行测试,但是EasyMock使其变得简单明了,如清单8所示:
清单8.测试XML解析器
import java.io.*;
import org.easymock.EasyMock;
import org.xml.sax.*;
import org.xml.sax.helpers.XMLReaderFactory;
import junit.framework.TestCase;
public class XMLParserTest extends TestCase {
private XMLReader parser;
protected void setUp() throws Exception {
parser = XMLReaderFactory.createXMLReader();
}
public void testSimpleDoc() throws IOException, SAXException {
String doc = "<root>\n Hello World!\n</root>";
ContentHandler mock = EasyMock.createStrictMock(ContentHandler.class);
mock.setDocumentLocator((Locator) EasyMock.anyObject());
EasyMock.expectLastCall().times(0, 1);
mock.startDocument();
mock.startElement(EasyMock.eq(""), EasyMock.eq("root"), EasyMock.eq("root"),
(Attributes) EasyMock.anyObject());
mock.characters((char[]) EasyMock.anyObject(),
EasyMock.anyInt(), EasyMock.anyInt());
EasyMock.expectLastCall().atLeastOnce();
mock.endElement(EasyMock.eq(""), EasyMock.eq("root"), EasyMock.eq("root"));
mock.endDocument();
EasyMock.replay(mock);
parser.setContentHandler(mock);
InputStream in = new ByteArrayInputStream(doc.getBytes("UTF-8"));
parser.parse(new InputSource(in));
EasyMock.verify(mock);
}
}
该测试演示了几个新技巧。 首先,它使用严格的模拟,因此需要顺序。 例如,您不希望解析器在startDocument()
之前调用endDocument()
。
其次,我正在测试的方法都返回void
。 这意味着我不能像使用EasyMock.expect()
那样将它们作为参数传递给EasyMock.expect()
getRate()
。 (EasyMock愚弄了编译器很多事情,但是愚蠢到使愚蠢的编译器相信void
是合法的参数类型。)相反,我只是在模拟中调用void方法,EasyMock捕获了结果。 如果需要更改期望的某些细节,则在调用模拟方法后立即调用EasyMock.expectLastCall()
。 另外,请注意,您不能只传递任何旧的String
, int
和数组作为期望参数。 所有这些都必须先用EasyMock.eq()
包装,以便可以在期望中捕获它们的值。
清单8使用EasyMock.expectLastCall()
来调整期望方法的次数。 默认情况下,方法应每个使用一次。 但是,我通过调用.times(0, 1)
setDocumentLocator()
使setDocumentLocator()
可选。 这表示必须在0到1次之间调用该方法。 当然,您可以更改这些参数以期望方法被调用1到10次,3到30次或您喜欢的任何其他范围。 对于characters()
,我真的不知道它将被调用多少次,除了它必须至少被调用一次,所以我希望它是.atLeastOnce()
。 如果这是一种非void
方法,则可以直接将times(0, 1)
和atLeastOnce()
应用于期望值。 但是,由于被模拟的方法返回void
,因此我必须使用EasyMock.expectLastCall()
来定位它们。
最后,请注意使用EasyMock.anyObject()
和EasyMock.anyInt()
作为characters()
的参数。 这说明了允许解析器将文本传递到ContentHandler
的多种不同方式。
嘲弄与现实
EasyMock值得吗? 它没有做任何手动编写模拟类无法完成的事情,并且在手动编写类的情况下,您的项目将是一个依赖项或两个依赖项。 例如, 清单3是一种情况,其中使用匿名内部类手动编写的模拟可能几乎一样紧凑,并且对于还不熟悉EasyMock的开发人员来说更容易辨认。 但是,出于文章目的,这是一个故意简短的示例。 当模拟较大的接口(例如org.w3c.dom.Node
(25个方法)或java.sql.ResultSet
(139个方法并不断增长))时,EasyMock可以节省大量时间,并且可以以最小的成本生成更短,更清晰的代码。
现在请注意:模拟对象可能被带到了太远的地方。 可能会产生如此多的嘲笑,即使代码被严重破坏,测试也总是可以通过。 模拟的次数越多,测试的内容就越少。 依赖库中以及一个方法与其调用的方法之间的交互中存在许多错误。 模拟依赖项可以隐藏许多您真正希望发现的错误。 在任何情况下,模仿都不应该是您的首选。 如果可以使用真正的依赖关系,请这样做。 模拟是真实类的次等替代品。 但是,如果由于某种原因不能可靠,自动地对真实类进行测试,则使用模拟进行测试绝对优于完全不进行测试。
翻译自: https://www.ibm.com/developerworks/java/library/j-easymock/index.html