JMockit提供了两套API,一套叫做Expectations,用于基于行为的单元测试;一套叫做Faking,用于基于状态的单元测试。
基于Expectations的单元测试创建mock对象,并且record相应的行为,之后调用被测代码(CodeUnderTest),然后进行verify。
Mocking主要考察被测试类与其依赖之间是否正确交互,交互的形式为方法调用。也就是说,Mocking考察被测试类是否正确调用了其依赖,正确调用包括:
- 调用了那些方法
- 通过怎样的参数
- 调用了多少次
- 调用的相对顺序
关于Mocking和Faking,有两篇文章对其进行了区分:
Mocks Aren't Stubs
Stub, Mock and Proxy Testing
2.1 创建并使用mock对象
JMockit可以mock任意class、interface。可以将mock对象声明为域或者方法的参数。默认情况下,mock对象的所有 非private 的方法(包括除了object的其他继承方法)都会被mock,对这些方法的调用不会执行原有代码,而是会转交给JMockit处理。进行mock风格的测试需要三个步骤:expectation--> 方法调用 --> verication,示例如下:
@Mocked Dependency mockedDependency
@Test
public void test(final @Mocked AnotherDenpendency anotherDependency) {
new Expectations() {{
mockedDependency.mockedMethod();
result = 1;
}};
codeUnderTest();
new Verifications(){{
anotherDependency.anotherMethod();
times = 1;
}};
}
JMockit会对@Mocked
注解的对象进行依赖注入,所以在Expectation、Verication以及CodeUnderTest中可以直接使用mock对象,不需要手动实例化。
在CodeUnderTest中通过new创建了一个Dependency并调用了其方法,JMockit会自动将这个方法调用转移到mock对象上。
public class CodeUnderTest {
public int testMethod() {
Dependency dependency = new Dependency();
return dependency.mockMethod();
}
}
public class Dependency {
public int mockMethod() {
return 1;
}
}
Dependency类的mockMethod方法原本返回值为1,在Expectation中将其返回值设置为2,则在测试过程中该方法将会返回2。
@Mocked
Dependency dependency;
@Test
public void TestMethod() throws Exception {
new NonStrictExpectations() {{
dependency.mockMethod();
result = 2;
}};
CodeUnderTest codeUnderTest = new CodeUnderTest();
assertEquals(2, codeUnderTest.testMethod());
}
2.2 Expectations
Expectations中定义了mock对象将会被调用的方法以及方法的返回值。Expectations中出现的方法必须被调用,而调用的方法不必全部出现在Expectation中。
但是,如果定义了mock对象,并在测试代码中调用了它的某个方法,而该方法没有出现在Expectation中,JMockit并不会执行其原有代码,而是返回null或者原始类型的初始值。例如:
public class CodeUnderTest {
public int testMethod() {
Dependency dependency = new Dependency();
return dependency.mockMethod();
}
}
public class Dependency {
public int mockMethod() {
return 1;
}
}
@RunWith(JMockit.class)
public class MyTest {
@Mocked
Dependency dependency;
@Test
public void TestMethod() throws Exception {
CodeUnderTest codeUnderTest = new CodeUnderTest();
assertEquals(0, codeUnderTest.testMethod());
}
}
2.3 record-replay-verify模型
record : 录制将要被调用的方法和返回值
replay:调用录制的方法
verify:基于行为的验证
在record阶段实例化Expectations, 在verify阶段实例化Verifications。一个测试方法可以包括任意个(包括0)Expectation/Verification。
@Test
public void testMethod(Parameter p) {
//常规准备代码
//record
new Expectations(){};
//replay
//调用测试代码
//verify
new Verifications(){};
//其他验证代码
}
2.4 Regular v.s. Strict Expectation
NonStrictExpections
中的方法至少被调用一次,否则会出现missing invocation
错误。之所以说它是常规的,是因为其中的方法可以调用多次,也可以颠倒顺序,其中没出现的方法也可以调用。
StrictExpectations
中方法调用的次数和顺序都必须严格执行。同时,如果出现了在StrictExpectations
中没有声明的方法,会出现unexpected invocation
错误。
- 可以混合使用
StrictExpectations
和NonStrictExpections
,不过一般一个mock对象只出现在其中之一。StrictExpectations
包含了隐式的verification。
2.5 为Expectation录制结果
对于返回值非空的函数(包括构造器),可以通过result
设置返回值或抛出异常,该值在replay阶段生效。
可以record多个结果,
mockObject.mockMethod();
result = new Object();
result = new Object();
result = new SomeException();
等价于
mockObject.mockMethod();
returns(new Obejct(), new Object());
result = new SomeException();
Note:
- 返回值可以使用returns()函数,异常值必须要使用result。
- 异常值需要在CodeUnderTest中捕获,否则无法通过测试。
- 假设在StrictExpectations中录制了n个结果,在replay阶段并不强制要求调用n次该函数,调用1次即可。
2.6 调用特定对象的方法
通常,声明了mock对象并在Expectation中进行了record,则在replay阶段对该类其他对象的调用也会返回record的结果。也就是说,在replay阶段,JMockit并不关心调用的是哪个对象,只要是该mock类的对象就会引用record中的结果。
示例如下:
@Test
public void TestMethod(@Mocked final Dependency dependency) throws Exception {
new NonStrictExpectations() {{
dependency.intReturnMethod();
returns(1, 2, 3);
}};
Dependency dependency1 = new Dependency();
assertEquals(1, dependency1.intReturnMethod());
Dependency dependency2 = new Dependency();
assertEquals(2, dependency2.intReturnMethod());
Dependency dependency3 = new Dependency();
assertEquals(3, dependency3.intReturnMethod());
}
在大多数情况下,CodeUnderTest使用mock类的某一个对象,所以是在CodeUnderTest中创建的还是作为参数传给它的并不重要。但是,如果CodeUnderTest中包含多个mock对象,而我们需要
- 只mock其中某个对象,其它的并不mock
- 指定调用某个mock对象的方法
这时,使用 @Injectable 可以mock某个对象。当然,即使用 @Mock 注解mock该类所有对象,也有其他方法限制Expectation中的匹配。
@Injectable
public class Dependency {
public String mockMethod() {
return "realMethod";
}
}
@Test
public void TestMethod(@Injectable final Dependency mockDependency) {
new NonStrictExpectations() {{
mockDependency.mockMethod();
result = "mockMethod";
}};
assertEquals("mockMethod", mockDependency.mockMethod());
assertEquals("realMethod", new Dependency().mockMethod());
}
其他对象不受影响,正常执行原有代码。
note:
- 需要将mock对象传递给CodeUnderTest
- static方法和constructor无法被mock
声明多个mock对象
声明多个mock对象可以限制和Expectation的匹配。
示例如下:
//参数中的anotherDependency只是起到占位作用,不会被真正使用
@Test
public void TestMethod(@Mocked final Dependency mockDependency,
@Mocked Dependency anotherDependency) {
new NonStrictExpectations() {{
mockDependency.mockMethod();
result = "mockMethod";
}};
//这个会被mock
assertEquals("mockMethod", mockDependency.mockMethod());
//JMockit仍然拦截了这次调用,但是由于在Expectation中没有record,所以返回null
assertNull("realMethod", new Dependency().mockMethod());
}
这种方法看起来比较奇怪,主要用于当CodeUnderTest中包含多个Dependency类的对象,而想要测试其中某个确定对象会被调用。
mock特定constructor产生的实例
有两种方式可以实现这个效果,方式1:
@Test
//mockDependency不会被使用
public void TestMethod(@Mocked Dependency mockDependency) {
new NonStrictExpectations() {{
Dependency dependency1 = new Dependency("dependency1");
dependency1.mockMethod(); result="dependency1";
Dependency dependency2 = new Dependency("dependency2");
dependency2.mockMethod(); result="dependency2";
}};
//可以创建多个对象,会匹配到同一个Expectation
assertEquals("dependency1", new Dependency("dependency1").mockMethod());
assertEquals("dependency1", new Dependency("dependency1").mockMethod());
assertEquals("dependency2", new Dependency("dependency2").mockMethod());
//JMockit仍然拦截了这次调用,但是由于在Expectation中没有record,所以返回null
assertNull(new Dependency("dependency2").mockMethod());
}
方式2:
@Test
public void TestMethod(@Mocked final Dependency mockDependency1,
@Mocked final Dependency mockDependency2) {
new NonStrictExpectations() {{
new Dependency("dependency1");
result = mockDependency1;
new Dependency("dependency2");
result = mockDependency2;
mockDependency1.mockMethod();
result = "dependency1";
mockDependency2.mockMethod();
result = "dependency2";
}};
assertEquals("dependency1", new Dependency("dependency1").mockMethod());
assertEquals("dependency1", new Dependency("dependency1").mockMethod());
assertEquals("dependency2", new Dependency("dependency2").mockMethod());
assertNull("dependency3", new Dependency("dependency3").mockMethod());
}
两种方法等效。
2.7 灵活的参数匹配
在record和verify阶段进行方法匹配时,
- 对于原始类型对象,数值相同即可;
- 对于Object的子类,需要equals()返回true;
- 对于数组,需要长度相等且每个对象equals()返回true;
除此之外,如果不关心replay时的具体参数,可以使用anyXyz或者withXyz(...)方法。
使用"any"
@Test
public void someTestMethod(@Mocked final DependencyAbc abc)
{
final DataItem item = new DataItem(...);
new Expectations() {{
abc.voidMethod(anyString, (List<?>) any);
}};
new UnitUnderTest().doSomething(item);
new Verifications() {{
abc.anotherVoidMethod(anyLong);
}};
}
- 任何的基本类型都有对应的anyXyz,anyString对应任意字符串。
- any对应任意的对象,在使用时需要进行显式类型转换: (CastClass) any
- mockit.Invocations类中有可以使用所有anyXyz
- 使用时参数位置需要一致
使用"with"
any的限制太宽松,with可以选择特定的子集。
@Test
public void someTestMethod(@Mocked final DependencyAbc abc) {
final DataItem item = new DataItem(...);
new Expectations() {{
abc.voidMethod("str", (List<?>) withNotNull());
abc.stringReturningMethod(withSameInstance(item), withSubstring("xyz"));
}};
new UnitUnderTest().doSomething(item);
new Verifications() {{
abc.anotherVoidMethod(withAny(1L));
}};
}
也可以自定义with方法。
使用"null"
null可以与任何对象匹配,好处是避免类型转换,但是需要有一个any或者with
。
@Test
public void TestMethod(@Mocked final Dependency mock) {
new StrictExpectations() {{
//测试会失败,因为没有any或者with
mock.mockMethod(2, null);
//测试通过
mock.mockMethod(anyInt, null);
}};
mock.mockMethod(new Integer(2), "hello world");
}
如何需要的是null,则应该用 withNull() 方法。
varargs
要么使用常规的参数,要么使用any/with,不能混合使用。
2.8 调用次数的限制
在record和verify阶段可以使用times
,minTimes
,maxTimes
来限制。
默认为minTimes = 1。
2.9 显式验证
对于NonStrictExpectation,可以进行verification。对于StrictExpectation则没有必要。在new Verifications(){}
中的方法至少被调用一次。
验证某个方法没被调用
times = 0
验证顺序调用
普通的new Verifications(){}
没有验证其中方法的调用顺序。new VerificationsInOrder(){}
用来验证(相对)顺序。
验证部分顺序
使用unverifiedInvocations()
方法固定不需要验证的方法的位置。
第一种场景是验证部分方法的顺序,其余方法不需要验证:
@Test
public void TestMethod(@Mocked final Dependency mock) {
mock.mockMethod1();
mock.mockMethod2();
mock.mockMethod3();
mock.mockMethod4();
new VerificationsInOrder(){
{
// 下面的代码会失败:
// Unexpected invocation of: Dependency#mockMethod2()
// 如果两个方法相连,则其在replay中也必须直接相连
// unverifiedInvocations();
// mock.mockMethod1();
// mock.mockMethod4();
// 成功
mock.mockMethod1();
unverifiedInvocations();
mock.mockMethod4();
}
};
}
第二种场景是关心部分方法顺序,另一些方法也需要验证,但是不关心顺序。这时需要两个Verification块:
@Test
public void TestMethod(@Mocked final Dependency mock) {
mock.mockMethod1();
mock.mockMethod2();
mock.mockMethod3();
mock.mockMethod4();
new VerificationsInOrder(){{
mock.mockMethod1();
unverifiedInvocations();
mock.mockMethod4();
}};
new Verifications(){{
mock.mockMethod3();
mock.mockMethod2();
}};
}
多个verification块时,其相对顺序会引起比较诡异的事:
@Test
public void TestMethod(@Mocked final Dependency mock) {
mock.mockMethod1();
mock.mockMethod2();
mock.mockMethod3();
mock.mockMethod4();
//下面的代码会失败
//MissingInvocation: Missing invocation of:Dependency#mockMethod2()
//颠倒一下两个verification的顺序则会通过
//原因似乎是Verifications会将验证过的方法删除
new Verifications(){{
mock.mockMethod3();
mock.mockMethod2();
}};
new VerificationsInOrder(){{
mock.mockMethod1();
mock.mockMethod2();
mock.mockMethod4();
}};
}
full verification
new FullVerifications() {...}
可以保证replay阶段调用的所有方法在verify代码块中都有相应的匹配,顺序可以不一致。
full verification in order
使用new FullVerificationsInOrder()
限制full verification的目标类型
默认使用full verification时,所有mock类的所有调用都必须显式验证。如果需要限定验证的类或者实例,使用FullVerifications(xxx.class)
或者FullVerifications(mockObject)
。
验证没有调用发生
使用空的FullVerifications(xxx.class)
或者FullVerifications(mockObject)
可以验证在指定类/实例上没有调用方法。但是如果Expectation中有minTimes和times的方法会被正常验证。
2.10 在verification中捕获调用参数
单次调用捕获
使用withCapture()
捕获最后一次调用的参数。
@Test
public void capturingArgumentsFromSingleInvocation(@Mocked final Collaborator mock)
{
new Collaborator().doSomething(0.5, new int[2], "test");
new Verifications() {{
double d;
String s;
mock.doSomething(d = withCapture(), null, s = withCapture());
assertTrue(d > 0.0);
assertTrue(s.length() > 1);
}};
}
多次调用捕获
使用withCapture(List)
捕获所有参数。
@Test
public void capturingArgumentsFromMultipleInvocations(@Mocked final Collaborator mock)
{
mock.doSomething(dataObject1);
mock.doSomething(dataObject2);
new Verifications() {{
List<DataObject> dataObjects = new ArrayList<>();
mock.doSomething(withCapture(dataObjects));
assertEquals(2, dataObjects.size());
DataObject data1 = dataObjects.get(0);
DataObject data2 = dataObjects.get(1);
// Perform arbitrary assertions on data1 and data2.
}};
}
捕获新实例
使用withCapture(new XX())
@Test
public void capturingNewInstances(@Mocked Person mockedPerson) {
new Person("Paul", 10);
new Person("Mary", 15);
new Person("Joe", 20);
new Verifications() {{
List<Person> personsInstantiated = withCapture(new Person(anyString, anyInt));
}};
}
2.11 使用Delegate在Expectation中定制result
使用场景:在Expectation中需要根据replay时的参数值决定返回值。
原理:JMockit拦截调用,转交给Delegate处理。
@Test
public void delegatingInvocationsToACustomDelegate(@Mocked final DependencyAbc anyAbc){
new Expectations() {{
anyAbc.intReturningMethod(anyInt, null);
result = new Delegate() {
int aDelegateMethod(int i, String s)
{
return i == 1 ? i : s.length();
}
};
}};
// Calls to "intReturningMethod(int, String)" will execute the delegate method above.
new UnitUnderTest().doSomething();
}
- delegate方法的参数应该与原始方法一致,返回值需要兼容或者为异常。
- 可以delegate构造器,这时返回值设置为空。
- delegate参数中可以有一个 Invocation对象,从而获得调用者的引用。
2.12 级联mock
出现obj1.getObj2(...).getYetAnotherObj().doSomething(...)
时可能需要mock多个对象。对于一个mock对象:
- Expectation中进行了record,则会返回record的result;
- 如果没有record,JMockit会自动创建一个返回被注解@Injectable的子对象
public class Dependency {
public CascadeDependency getCascadeDependency() {
//JMockit会拦截这个方法,返回一个非null对象
return null;
}
public CascadeDependency getAnotherCascadeDependency() {
//JMockit会拦截这个方法,返回一个非null对象
return null;
}
public String getString() {
//仍旧返回null
return null;
}
public Object getObject() {
//仍旧返回null
return null;
}
public List<Object> getList() {
//返回empty集合
return null;
}
}
@Test
public void TestMethod(@Mocked Dependency dependency) {
CascadeDependency first = dependency.getCascadeDependency();
CascadeDependency second = dependency.getCascadeDependency();
//调用另一个方法
CascadeDependency third = dependency.getAnotherCascadeDependency();
//所有都不会为null
assertNotNull(first);
assertNotNull(second);
assertNotNull(third);
//相同方法返回JMockit创建的同一个对象
assertSame(first, second);
//不同方法返回JMockit创建的同一个对象
assertNotSame(first, third);
//String返回null
assertNull(dependency.getString());
//Object返回null
assertNull(dependency.getObject());
//返回empty集合
assertNotNull(dependency.getList());
assertEquals(0, dependency.getList().size());
}
@Test
public void TestMethod(@Mocked Dependency dependency,
@Mocked CascadeDependency cascadeDependency) {
CascadeDependency first = dependency.getCascadeDependency();
CascadeDependency second = dependency.getAnotherCascadeDependency();
//因为子对象也@Mocked,所以会返回同一个对象
assertSame(first, second);
}
JMockit返回的非空对象实际上进行了@Injectable标识,所以:
@Test
public void TestMethod(@Mocked Dependency dependency) {
//虽然CascadeDependency没有出现在参数中,
//但是JMockit对其进行了@Injectable
//而由于没有在Expectation中record mockMethod的result,所以返回空
assertNull(dependency.getCascadeDependency().mockMethod());
//不影响CascadeDependency的其他实例
assertNotNull(new CascadeDependency().mockMethod());
}
也可以在Expectation中使用result指定返回对象,从而禁止JMockit自动生成。
@Test
public void TestMethod(@Mocked final Dependency dependency) {
//在Expectation中指定了返回结果,因此JMockit不会生成CascadeDependency
new NonStrictExpectations(){{
dependency.getCascadeDependency();
result = null;
result = new CascadeDependency();
}};
//第一次返回null
assertNull(dependency.getCascadeDependency());
//第二次返回新对象
assertNotNull(dependency.getCascadeDependency().mockMethod());
}
mock级联调用特别适合static factory,getCurrentInstance()
永远不会返回null。
@Test
public void TestMethod(@Mocked final Dependency dependency) {
assertSame(dependency, dependency.getCurrentInstance());
}
在Builder模式中也很方便验证,
@Test
public void createOSProcessToCopyTempFiles(@Mocked final ProcessBuilder pb) throws Exception{
Process copy = new ProcessBuilder().command(cmdLine).directory(wrkDir).inheritIO().start();
new Verifications() {{ pb.command(withSubstring("copy")).start(); }};
}
2.13 部分mock
有时候只需要mock部分方法,这时候可以用new Expectations(object)
,object可以是实例,也可以是class对象。在replay阶段,如果在Expectation中没有进行record,则会调用原有代码。
@Test
public void partiallyMockingASingleInstance() {
final Collaborator collaborator = new Collaborator(2);
new Expectations(collaborator) {{
collaborator.getValue(); result = 123;
// 静态方法也可以
Collaborator.doSomething(anyBoolean, "test");
}};
// Mocked:
assertEquals(123, collaborator.getValue());
Collaborator.doSomething(true, "test");
// Not mocked:
assertEquals(45, new Collaborator(45).getValue());
}
- Note:上面的代码中没有出现@Mocked注解
没有record的方法也可以verify,
@Test
public void partiallyMockingA() {
final Collaborator collaborator = new Collaborator(123);
new Expectations(collaborator) {};
int value = collaborator.getValue();
collaborator.simpleOperation(45, "testing", new Date());
// 没有record也可以verify
new Verifications() {{ c1.simpleOperation(anyInt, anyString, (Date) any); }};
}
另一种实现部分mock的方法:同时标注@Tested和@Mocked。
2.14 mock接口
有些实现类是匿名的:
public interface Service { int doSomething(); }
public final class TestedUnit {
private final Service service = new Service() {
public int doSomething() { return 2; }
};
public int businessOperation() {
return service.doSomething();
}
}
使用@Capturing
标注基类/接口,所有实现类会被mock:
@Capturing Service anyService;
@Test
public void mockingImplementationClassesFromAGivenBaseType() {
new Expectations() {{
anyService.doSomething();
returns(3);
}};
int result = new TestedUnit().businessOperation();
assertEquals(3, result);
}
@Capturing
是@Mock
的增强版,有一个可选参数maxInstances
用于捕获前面指定数量的对象,其默认值为Integer.MAX_VALUE
。
@Test
public void TestMethod(@Capturing(maxInstances = 2) final Dependency dependency1,
@Capturing(maxInstances = 2) final Dependency dependency2,
@Capturing final Dependency remain) {
new NonStrictExpectations() {{
dependency1.getValue();
result = 1;
dependency2.getValue();
result = 2;
remain.getValue();
result = 3;
}};
assertEquals(1, new Dependency().getValue());
assertEquals(1, new Dependency().getValue());
assertEquals(2, new Dependency().getValue());
assertEquals(2, new Dependency().getValue());
assertEquals(3, new Dependency().getValue());
}
上面的@Capturing
是出现在参数列表中的,如果是作为field声明的,maxInstances
会失效,@Capturing
退化为@Mock
。
2.15 自动注入被测试类
用@Tested
标注被测试类,在运行测试方法时,如果该实例仍然为null,JMockit会自动组装相关mock对象,进行初始化。在组装被测试类过程中,相关mock对象必须使用@Injectable标记,非mock对象除了使用@Injectable标记,还需要有明确初始值。
public class SomeTest {
@Tested CodeUnderTest tested;
@Injectable Dependency dep1;
@Injectable AnotherDependency dep2;
@Injectable int someIntegralProperty = 123;
@Test
public void someTestMethod(@Injectable("true") boolean flag) {
tested.exerciseCodeUnderTest();
}
}
注入先根据类型匹配,再根据参数名称匹配。