目录
本文主要内容
- 如何在 SpringBoot 中配置使用 JMockit
- 如何 mock / faking 依赖的对象
- 如何对行为 mock
- 如何 Verification
JMockit 之所以强大,是因其使用了 javaagent 对类的字节码做了修改,在 JVM 的所有 mock 工具中,它是功能最强大的。同时注解又是最少的。
配置
在 SpringBoot 项目中使用 JMockit 隔离代码做单元测试,需要做以下配置
- 引入 JMockit 依赖。
<dependencies><dependency><groupId>org.jmockit</groupId><artifactId>jmockit</artifactId><version>${jmockit.version}</version><scope>test</scope></dependency></dependencies>
- 配置 javaagent。
<plugins><plugin><artifactId>maven-surefire-plugin</artifactId><version>2.22.2</version><!-- or some other version --><configuration><argLine>
-javaagent:"${settings.localRepository}"/org/jmockit/jmockit/${jmockit.version}/jmockit-${jmockit.version}.jar
</argLine></configuration></plugin></plugins>
注意
{jmockit.version}
配置在
properties
标签下
<properties><jmockit.version>1.49</jmockit.version><skipTests>true</skipTests><!--同时加上这个,在 compile 过程中,将忽略测试代码编译--></properties>
模拟
模拟类型与实例
模拟提供一种将被测对象与其依赖隔离开来的机制。在单元测试时,仅需使用注解的方式将被测对象的依赖作为测试类的 field 属性或测试类中某个方法的参数引入到测试类。在执行单元测试时,被测对象的依赖将被重定向到 mock 对象中。如
classTheTestClass(){@MockedprivateFirstDependency firstDependency;// 作为 field 被引入到测试类// Note: the test method can not have a return value.@TestvoidtestMethod(@MockedSecondDenpency seconDependency){// 作为参数被引入到测试类// your test code...}}
使用
JMockit
模拟的对象作为方法参数在测试类使用时,该模拟对象会被自动创建并传递给当前使用的单元测试框架,如
JUnit/TestNG
,因此当模拟对象作为参数时,该参数永不为
null
。
JMockit 共提供 3 种不同的注解用于模拟被测对象的依赖。
- @Mocked 该注解针对的是类型,需要特别强调的是该注解有点霸道,被
@Mocked
注解的类型的所有实例都将被mock
,以及该类的所有父类(除java.lang.Object
外)都会被递归mock
,所有子类也将被递归mock
,除private
修饰的方法外,所有方法也将被 mock。如
publicabstractclassAbsDemoClass{publicvoidhandle(){}}
@Slf4jpublicclassDemoClass extend AbsDemoClass{@Overridepublicvoidhandle(String args){
log.info("args length is {}", args.length());}}
@ExtendWith(JMockitExtension.class)classDemoClassTest{@MockedprivateTargetMockClass instance1;@TestpublicvoidtestPublish(){AbsDemoClass demo1 =newDemoClass();AbsDemoClass demo2 =newDemoClass();
demo1.handle(null);// demo1 为用户 new 的对象,但是自动被 mock 对象覆盖了,不执行真实方法
demo2.handle(null);// demo2 为用户 new 的对象,但是自动被 mock 对象覆盖了,不执行真实方法
instance1.handle(null);}}
- @Injectable 该注解针对的是实例,仅影响被注解的实例。其余实例不受影响。如
@ExtendWith(JMockitExtension.class)classDemoClassTest{@MockedprivateTargetMockClass instance1;@TestpublicvoidtestPublish(){AbsDemoClass demo1 =newDemoClass();AbsDemoClass demo2 =newDemoClass();
demo1.handle(null);// 调用真实方法,抛出了异常
demo2.handle(null);
instance1.handle(null);}}
- @Capturing 该注解使用较少。主要用于子类 / 实现类的
mock
。如当我们就仅知道父类或接口,但需要控制它的所有子类行为、或子类存在多个实现时,就使用@Capturing
。建本节最后一小节——模拟未实现的类。
在测试类中,被模拟的对象未使用会报错么?
答:不会
期望
期望是指一组与测试相关的特定模拟方法 / 构造函数的调用。期望可能涵盖对同一方法或构造函数的多个不同调用,但无需涵盖该类的所有方法调用,换句话说需要什么方法即对该方法建立期望即可。特定调用是否与给定期望匹配不仅取决于方法/构造函数签名,还取决于运行时,例如调用方法的实例、参数值、已匹配的调用数量。因此,可以为给定的期望指定几种类型的匹配约束。
当我们涉及一个或多个调用参数时,可以为每个参数指定一个确切的参数值。如可以为
String
参数指定值 “test string”,从而导致期望仅匹配那些在相应参数中具有此精确值的调用。我们也可以指定更宽松的约束来匹配整组不同的参数值,而不是指定精确的参数值。
如下代码片段显示了对
Dependency#someMethod(int, String)
的期望,它将使用指定的确切参数值匹配对此方法的调用。
@TestvoidtestMethod(@MockedDependency mockInstance){...newExpectations(){{...// An expectation for an instance method:
mockInstance.someMethod(1,"test"); result ="mocked";...}};// 注意,这里仅是建立了一个期望(stub),并不会发生真实的调用}
在测试类中,建立了期望的方法未使用,会报错么?
答:会,将抛出 miss invocations 异常
录制-回放-验证
任何一个单元测试用例都可以被划分为三个互相独立的阶段,每个按顺序依次执行,任意时刻仅会执行其中一个。分别为
- 录制 record
- 回放 replay
- 验证 verify
在代码块中的表现形式如
@TestvoidtestMethod(){// 1. 准备,测试所需数据及依赖对象...// 2. 回放,即调用真实的方法进行测试,通常调用的是 public方法,切记不要调用 private 方法。...// 3. 验证,验证方法调用次数,执行结果等// the test did its job....}
录制技巧
- 录制有返回值的方法 当给定的方法不是 non-void 返回类型时,返回值可通过 Expectations 的 result来设定。如存在方法
DemoClass#doSomething(String args)
。
classDemoClass{publicStringdoSomething(String args){// process args// do something with argstry{...return"string value"}catch(YourException e){thrownewYourException("exception message");}}}
对该方法建立期望并获得返回值可使用如下方式。
@TestvoidtestDoSomething(@MockedDemoClass demoClass){newExpectations(){{
demoClass.doSomething(anyString);// 注意该处的参数,可以是 anyString,也可以是精确值
result ="your expectation value";}...};// replay}
- 录制异常 当需要对调用方法抛出异常测试时,可使用类似的方式,如
@TestvoidtestDoSomething(@MockedDemoClass demoClass){newExpectations(){{
demoClass.doSomething(anyString);
result =newYourException();}...};// replay}
- **灵活匹配方法参数的录制 **
JMockit
提供了如any
、with
等前缀对象对方法进行灵活mock
。如上述用例中,当希望传入任何字符串时均返回同一指定值,可使用anyString
,以any
前缀开始的类型还有 当参数不是基本类型时,可使用以 with 前缀的类型来实现,如withInstanceOf(Class<T> clazz)
,以with
为前缀的类型还有
指定调用计数
调用计数约束可用于建立期望时或验证结果时,Jmockit 提供了三个特殊字段用于计数约束。分别为:
- times
- minTimes
- maxTimes 需要注意的是,任何非负整数值对计数约束都有效,如果指定了 times = 0 或 maxTimes = 0,则与在重放期间发生的预期匹配的第一次调用(如果有)将导致测试失败。
验证
- 不关心调用顺序的验证 不关心方法调用顺序仅关心调用次数可使用时,可通过关键字 Verifications来建立验证。如
newVerifications(){{
instance1.method(args...);
times = your specified value;}{
instance2.method(args...);
times = your specified value;}...}
- 验证调用顺序 当需要验证方法调用顺序时,如 methodA()、在 methodB() 前被调用,则需使用 VerificationsInOrder 关键字。如
@TestvoidverifyingExpectationsInOrder(@MockedDependencyAbc abc){// Somewhere inside the tested code:
abc.aMethod();
abc.doSomething("blah",123);
abc.anotherMethod(5);...newVerificationsInOrder(){{// The order of these invocations must be the same as the order// of occurrence during replay of the matching invocations.
abc.aMethod();
abc.anotherMethod(anyInt);}};}
- 全验证 有时可能需要对测试中涉及的模拟类型/实例的所有调用进行验证。在这种情况下,
new FullVerifications() {...}
块将确保没有未验证的调用。如
@TestvoidverifyAllInvocations(@MockedDependency mock){// Code under test included here for easy reference:
mock.setSomething(123);
mock.setSomethingElse("anotherValue");
mock.setSomething(45);
mock.save();newFullVerifications(){{
mock.setSomething(anyInt);// verifies two actual invocations
mock.setSomethingElse(anyString);
mock.save();// if this verification (or any other above) is removed the test will fail}};}
指定自定义结果
假设有一种场景,我们需要根据回放时接收到的参数来决定记录期望值的结果,应该要怎么做?答案是使用
Delegate()
。如
@TestedCodeUnderTest cut;@TestvoiddelegatingInvocationsToACustomDelegate(@MockedDependencyAbc anyAbc){newExpectations(){{
anyAbc.intReturningMethod(anyInt, anyString);
result =newDelegate(){intaDelegateMethod(int i,String s){return i ==1? i : s.length();}};}};// Calls to "intReturningMethod(int, String)" will execute the delegate method above.
cut.doSomething();}
Delegate 接口是空的,仅用于告诉 JMockit 在重放时的实际调用应该委托给分配对象中的“委托”方法。该方法可以有任何名称,只要它是委托对象中唯一的非私有方法。至于委托方法的参数,要么与记录方法的参数相匹配,要么不存在。在任何情况下,委托方法都可以有一个 Invocation 类型的附加参数作为其第一个参数。在重放期间收到的 Invocation 对象将提供对被调用实例和实际调用参数以及其他功能的访问。委托方法的返回类型不必与记录的方法相同,但它应该兼容以避免以后发生 ClassCastException。
除此之外,构造方法也可以通过委托方法处理。如
@TestvoiddelegatingConstructorInvocations(@MockedCollaborator anyCollaboratorInstance){newExpectations(){{newCollaborator(anyInt);
result =newDelegate(){voiddelegate(int i){if(i <1)thrownewIllegalArgumentException();}};}};// The first instantiation using "Collaborator(int)" will execute the delegate above.newCollaborator(4);}
验证调用参数
可以通过一组特殊的
withCapture(...)
方法捕获调用参数以供以后验证。有三种不同的情况,每种都有自己特定的捕获方法:
- 在一次调用中验证传递给模拟方法的参数:
T withCapture()
@TestvoidcapturingArgumentsFromSingleInvocation(@MockedCollaborator mock){// Inside tested code:...newCollaborator().doSomething(0.5,newint[2],"test");// Back in test code:newVerifications(){{double d;String s;
mock.doSomething(d =withCapture(),null, s =withCapture());assertTrue(d >0.0);assertTrue(s.length()>1);}};}
在多次调用中验证传递给模拟方法的参数:
T withCapture(List<T>)
@TestvoidcapturingArgumentsFromMultipleInvocations(@MockedCollaborator mock){// Inside tested code:
mock.doSomething(dataObject1);
mock.doSomething(dataObject2);...// Back in test code:newVerifications(){{List<DataObject> dataObjects =newArrayList<>();
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.}};}
- 验证传递给模拟构造函数的参数:
List<T> withCapture(T)
@TestvoidcapturingNewInstances(@MockedPerson mockedPerson){// From the code under test:
dao.create(newPerson("Paul",10));
dao.create(newPerson("Mary",15));
dao.create(newPerson("Joe",20));...// Back in test code:newVerifications(){{// Captures the new instances created with a specific constructor.List<Person> personsInstantiated =withCapture(newPerson(anyString, anyInt));// Now captures the instances of the same type passed to a method.List<Person> personsCreated =newArrayList<>();
dao.create(withCapture(personsCreated));// Finally, verifies both lists are the same.assertEquals(personsInstantiated, personsCreated);}};}
联级模拟
存在这样的情况,联级创建一个自身对象,如
result =HttpUtil.createPost(url).addHeaders(header).setConnectionTimeout(2000).setReadTimeout(3000).body(params.toJSONString()).execute().body();
前半部分是创建一个 httpRequest 对象,后半部分是 http 执行并获取响应内容。在这种情况下,我们仅需 mock httpRequest 的 execute 方法,并返回一个 httpResponse 内容即可。
部分模拟
部分模拟有两种方式,一种是建立期望,但对于构造方法、静态方法不能使用期望方式实现,需使用
MockUp<T>(Class<T> argsClass)
的方式来模拟。
模拟未实现的类
如存在一个接口
publicinterfaceService{intdoSomething();}
及一个实现了该接口的实现类
finalclassServiceImplimplementsService{@OverridepublicintdoSomething(){return1;}}
publicfinalclassDemoClass{privatefinalService service1 =newServiceImpl();privatefinalService service2 =newService(){publicintdoSomething(){return2;}};publicintbusinessOperation(){return service1.doSomething()+ service2.doSomething();}}
如果不知道 Service 的具体实现(或 Service 是一个 abstract classs)时,要测试 businessOperation方法应当如何测试?
finalclassDemoClassTest{@CapturingService anyService;@TestvoidmockingImplementationClassesFromAGivenBaseType(){newExpectations(){{ anyService.doSomething();returns(3,4);}};int result =newTestedUnit().businessOperation();assertEquals(7, result);}}
其他
当被测对象中存在
@Value("${xxx}")
时,该如何
mock
?
由于当前我们单元测试并不加载任何 SpringBoot 上下文以及配置文件,因此针对这类私有属性,可以通过反射方式来赋值。如被测对象存在以下两个属性需要从配置文件获取。
@Value("${xxx.xxx.xxx}")privateString apiGatewayIdcs;@Value("${spring.profiles.active}")privateString currentEnv;
可通过反射方式设定值,如
Field apiGatewayIdcs =SuperAndSubPermissionUtil.class.getDeclaredField("apiGatewayIdcs");
apiGatewayIdcs.setAccessible(true);ReflectionUtils.setField(apiGatewayIdcs, superAndSubPermissionUtil,"wj");Field currentEnv =SuperAndSubPermissionUtil.class.getDeclaredField("currentEnv");
currentEnv.setAccessible(true);ReflectionUtils.setField(currentEnv, superAndSubPermissionUtil,"prd");
伪装
在
JMockit
工具包中,Faking API 支持创建假实现。通常,伪造的目标是要伪造的类中的某些些方法或某些构造函数,而大多数其他方法和构造函数保持不变。
假实现在依赖于外部组件或资源(如电子邮件或 Web 服务服务器、复杂库等)的测试中特别有用。通常,假实现将来自可重用的测试基础设施组件,而不是直接来自测试类。
用假实现替换真实实现对于使用这些依赖项的代码是完全透明的,并且可以在单个测试的范围内、单个测试类中的所有测试或整个测试运行中打开和关闭。
伪装方法及类
在 Faking API 的上下文中,假方法是假类中使用
@Mock
注释的任何方法。伪类是扩展
mockit.MockUp<T>
泛型基类的任何类,其中 T 是要伪造的类型。如伪造
javax.security.auth.login.LoginContext
类的若干方法。
publicfinalclassFakeLoginContextextendsMockUp<LoginContext>{@Mockpublicvoid $init(String name,CallbackHandler callback){assertEquals("test", name);assertNotNull(callback);}@Mockpublicvoidlogin(){}@MockpublicSubjectgetSubject(){returnnull;}}
每个
@Mock
方法必须有一个对应的“真实方法 / 构造函数”,在目标真实类中具有相同的签名。对于一个方法,签名由方法名和参数组成;对于构造函数,它只是参数,假方法具有特殊名称
“$init”
。
注意 :没有必要为真实类中的所有方法和构造函数使用假方法。任何此类方法或构造函数,如果在假类中不存在相应的假方法,则将简单地保持“原样”,也就是说,它不会被伪造。
应用伪装类
给定的假类必须应用于相应的真实类才能产生任何效果。这通常针对整个测试类或测试套件进行,但也可以针对单个测试进行。可以从测试类中的任何位置应用伪造:@BeforeClass 方法、@BeforeMethod / @Before / @BeforeEach 方法(TestNG / JUnit 4 / JUnit 5)或来自 @Test 方法。一旦应用了假类,所有假方法和真实类的构造函数的执行都会自动重定向到相应的假方法。如要应用上面的 FakeLoginContext 假类,我们只需实例化它:
@TestpublicvoidapplyingAFakeClass()throwsException{newFakeLoginContext());// Inside an application class which creates a suitable CallbackHandler:newLoginContext("test", callbackHandler).login();...}
由于在测试方法中应用了伪造类,因此 FakeLoginContext 对 LoginContext 的伪造将仅对该特定测试有效。
当实例化 LoginContext 的构造函数调用执行时,会执行 FakeLoginContext 中对应的“$init”假方法。同样,当调用 LoginContext#login 方法时,会执行相应的 fake 方法,在这种情况下,由于该方法没有参数且返回类型为 void,因此它什么也不做。发生这些调用的假类实例是在测试的第一部分中创建的。
伪装未实现类
如上述 Service 接口,我们可以使用一下方式伪装并测试
@Testpublic<TextendsService>voidfakingImplementationClassesFromAGivenBaseType(){newMockUp<T>(){@MockintdoSomething(){return7;}};int result =newDemoClass().businessOperation();assertEquals(14, result);}
版权归原作者 lclqcsj11 所有, 如有侵权,请联系我们删除。