基于 JTester 框架的 Mock 实践总结


诚信安全开发部 Mock Guideline 基于 JTester 框架的 Mock 实践总结 CSD CSD CSD CSD 质量----单元测试小组 2011-3-82011-3-82011-3-82011-3-8 文档介绍了一些 JMockit 的基础知识,并针对开发中的常用场景,详细说明了 Mock 的 解决之道。 Mock Guideline——基于 JTester 框架的 Mock 实践总结 2 目录 Mock 基础知识.................................................................................................................................3 1. 为什么要 Mock.................................................................................................................3 2. 如何引入 Mock.................................................................................................................3 3. record-replay-verify 模型.................................................................................................4 4. Mock 普通公有方法.........................................................................................................5 5. Mock 私有方法.................................................................................................................9 6. Mock 构造函数...............................................................................................................12 7. Mock 静态方法...............................................................................................................13 8. Mock 静态块.................................................................................................................. 13 9. 验证方法入参.................................................................................................................14 10. Mock 方法代理...........................................................................................................19 11. Strict 和NonStrict.......................................................................................................20 12. 使用times , minTimes , maxTimes.............................................................................24 13. 级联mock...................................................................................................................25 Mock 常见场景 GuideLine............................................................................................................. 31 1. Mock Morgan..................................................................................................................31 2. Mock TpRemote..............................................................................................................33 3. Mock Napoli 消息接口...................................................................................................33 4. 如何验证抛出异常分支.................................................................................................35 5. 如何mock 日期类..........................................................................................................36 6. 如何mock 日志类..........................................................................................................38 基于场景编写单元测试.................................................................................................................41 变更记录表.....................................................................................................................................42 Mock Guideline——基于 JTester 框架的 Mock 实践总结 3 Mock Mock Mock Mock 基础知识 1. 为什么要 Mock 因为单元测试当中,我们只关注被测的单元,而不关心其他的依赖内容。比如我们测试 的一段诚保帐户关闭的逻辑,需要调用远程接口查询 CRM 的帐户信息,需要查询本地数据 库中的递延数据,需要发送异步消息通知其他应用诚保状态变化——这些都是外部依赖。 但是我们关注的却是服务方法这个单元中业务的内部流转过程,比如在所有外部环境都 正常的情况下,我们拿到一种帐户信息、查询到递延记录、应该如何做出反应,而后应该通 知给其他应用什么信息内容。我们并不关心,如何去获取帐户信息、递延记录、异步消息发 送的具体过程。 所以我们需要屏蔽掉这些外部依赖。 Mock 让我们有了一套仿真的环境,不用担心在检查单元内的内部流转的过程时还会因 为环境的关系导致验证过程失败。 当然,最后需要给出的建议是:由于外部环境的多样化性,单元测试应该设计一些异常 场景,比如远程接口超时抛出异常,我们的单元中应该有相应的处理机制,以免真实环境中 真的出现环境问题,我们的应用呈现出非常脆弱的一面。 2. 如何引入 Mock 1. 引入jmockit 包,添加到工程的 pom.xml 中 com.alibaba.external test.jmockit 0.998 2. Mock 第一个实例,Hello world! Mock Guideline——基于 JTester 框架的 Mock 实践总结 4 public class DemoTest extends TestCase { /** * mock 掉外部服务 */ @Mocked DemoService demoService; public void testService() { new Expectations() { { demoService.getMsg(); result = "Hello world!"; } }; // 最简单的测试 Hello world. assertEquals(demoService.getMsg(), "Hello world!"); } } //接口类 interface DemoService { /** * 测试消息 *@return */* String getMsg(); } 以上是基于 Junit 的mock 实例,后面例子都将结合现有的 JTester 的框架。如何在框架中 引入 JTester 框架,参照 JTester 使用详解介绍。 3. record-replay-verify 模型 经典的测试模型都是分成三个阶段:Arrange、Act、Assert(AAA)。 1、Arrange 阶段:数据或者依赖的服务的准备和注入 2、Act 阶段:目的测试执行 3、Assert 阶段:把执行完的测试结果和期望值进行比较。 示例: @Test public void testMethod() { // 1. Preparation: whatever is required before the unit under test can be exercised. ... // 2. The unit under test is exercised, normally by calling a non-private method // or constructor. Mock Guideline——基于 JTester 框架的 Mock 实践总结 5 ... // 3. Verification: whatever needs to be checked to make sure the exercised unit // did its job. ... } 同样的,基于行为的 Mock 测试,同样也是基于类似的三个阶段:recordrecordrecordrecord、replayreplayreplayreplay、verifyverifyverifyverify。 1、 record:在这个阶段,各种在实际执行中期望被调用的方法都会被录制。 2、 repaly:在这个阶段,执行单元测试 Case,原先在 record 阶段被录制的调用都可能有机 会被执行到。这里有“有可能”强调了并不是录制了就一定会严格执行。 3、 verify:在这个阶段,断言测试的执行结果或者其他是否是原来期望的那样。 当然,这个例子只是众多 Mock 样式中的一个。 4. Mock 普通公有方法 工作中最常用的就是Mock一个接口或者实现类的公有方法,下面一步一步展示简单的Mock: 1. 业务类 Demo1Service.java public interface Demo1Service { /**测试返回值为字符串类型*/ String sayName(); /**测试返回值为 int 类型 */ int sayAge(); /**测试返回值为 boolean 类型*/ boolean isTp(); } 2. 业务实现类 Demo1ServiceImpl.java Mock Guideline——基于 JTester 框架的 Mock 实践总结 6 public class Demo1ServiceImpl implements Demo1Service { //服务类中依赖的 DAO 需要 Mock 掉,详见 Mock Mock Mock Mock 场景 1111 private Demo1DAO demo1DAO; @Override public boolean isTp() { return demo1DAO.isTp(); } @Override public int sayAge() { return demo1DAO.getAge(); } @Override public String sayName() { return demo1DAO.getName(); } public void setDemo1DAO(Demo1DAO demo1dao) { demo1DAO = demo1dao; } } 3. Mock Mock Mock Mock 场景1111:测试Demo1Service 服务类,Mock Demo1DAO Demo1DAO Demo1DAO Demo1DAO 类所有方法,如下: Mock Guideline——基于 JTester 框架的 Mock 实践总结 7 @SpringApplicationContext( {"applicationContext.xml" }) public class Demo1ServiceTest extends JTester { @SpringBeanByName//在容器中根据名称查询 bean 为demo1Service 的service 注入 Demo1Service demo1Service; //不指定具体的方法,将全部 mock @Mocked @SpringBeanFor //注入 Demo1DAO 到容器中 Demo1DAO demo1DAO; //不录制具体方法返回值,返回默认的 mock 值 @Test public void testService_default() { want.string(demo1Service.sayName()).isNull(); want.string(demo1Service.sayName()).isNull(); want.number(demo1Service.sayAge()).isEqualTo(0); want.bool(demo1Service.isTp()).isEqualTo(false); //mock 类默认返回值,应用 Java 数据类型初始化默认值 //Object = null、int = 0、boolean = false、long = 0L、float = 0.0F、double = 0.0D } @Test public void testService() { //录制期望返回值 ,根据调用情况录制方法,严格按照调用方法的顺利与次数 new Expectations() { { demo1DAO.getName(); result = "mayun"; demo1DAO.getAge(); result = 18; } }; want.string(demo1Service.sayName()).isEqualTo("mayun"); want.number(demo1Service.sayAge()).isEqualTo(18); /** * 此处调用第二次,将会抛出异常,不可遇知的调用异常。 * java.lang.AssertionError: Unexpected invocation of: int */ //want.number(demo1Service.sayAge()).isEqualTo(18); /** * isTp 方法没有录制,将会抛出异常,不可遇知的调用异常。 * java.lang.AssertionError: Unexpected invocation of: int*/ //want.bool(demo1Service.isTp()).isEqualTo(false); } } Mock Guideline——基于 JTester 框架的 Mock 实践总结 8 4. Mock Mock Mock Mock 场景2222:测试Demo1Service 服务类,Mock Demo1DAO Demo1DAO Demo1DAO Demo1DAO 类部分方法,如下: @SpringApplicationContext( {"applicationContext.xml" }) public class Demo1_1ServiceTest extends JTester { @SpringBeanByName Demo1Service demo1Service; /** * mock 方法, 通过显示的添加@Mocked(methods = {"filter1", "filter2", ...}, inverse=true/false)来 * 实现。 filter1 就是方法名称,这个标签指定只有符合 filter 规则的方法才会被 Mock,不符合 * 的还是按照原先的实现执行。Inverse 为true,刚好相反,说明除了 methods 的方法外,其他 * 的方法都是被 Mock 的。@Mocked methods 标签同时支持正则。 */ @Mocked(methods = {"getName", "getAge" }) @SpringBeanFor Demo1DAO demo1DAO; /** * 测试具体实现类 */ @Test public void testService() { //录制方法,根据调用情况录制方法,严格按照调用方法的顺利与次数 new Expectations() { { demo1DAO.getName(); result = "xiaoming"; demo1DAO.getAge(); result = 18; } }; want.string(demo1Service.sayName()).isEqualTo("xiaoming"); want.number(demo1Service.sayAge()).isEqualTo(18); /** * isTp 方法,没有指定必须 mock,可以随意调用.
* 注:如果是接口该方法返回默认 mock 值. */ want.bool(demo1Service.isTp()).isEqualTo(true); want.bool(demo1Service.isTp()).isEqualTo(true); } } Mock Guideline——基于 JTester 框架的 Mock 实践总结 9 5. Mock 私有方法 通常,单元测试针对的是 公共/保护/默认的 方法。 但是,有时被测试的方法,可能调用 到私有方法,而这些私有方法可能调用到外部接口,而这个外部接口严重依赖外部环境, 对于这种特殊情况以下实例讲解。 1. 业务类 Demo2Service.java public interface Demo2Service{ /** * 返回姓名 *@return String */ String sayName(); } 2. 业务实现类 Demo2ServiceImpl.java Mock Guideline——基于 JTester 框架的 Mock 实践总结 10 public class Demo2ServiceImpl implements Demo2Service { //外部接口 private MorganMemberService morganMemberService; @Override public String sayName() { if (isTp()) { // 业务逻辑省略... return "TP Member"; } // 业务逻辑省略... return "Free Member"; } /** * 需要 mock 的方法 * *@return boolean */ private boolean isTp() { // 模拟调用 dubbo 接口获取会员是否为 TP,,调用代码省略... return true; } //验证有参数私有方法 mock public String sayNameIsValid(String str) { if (isValid(str)) { // 业务逻辑省略... return "valid"; } // 业务逻辑省略... return "inValid"; } private boolean isValid(String str) { // 业务逻辑省略掉 return true; } } public void setMorganMemberService(MorganMemberService morganMemberService) { this.morganMemberService = morganMemberService; } Mock Guideline——基于 JTester 框架的 Mock 实践总结 11 3. Mock Mock Mock Mock 场景:测试Demo2ServiceImpl 服务类,Mock 私有方法 isTpisTpisTpisTp()()()()、isValidisValidisValidisValid,代码 如下: public class Demo2ServiceTest extends JTester { /** * mock 私有方法 isTp();
* 注:mock 私有方法需要指具体的实现类 */ @Mocked(methods = "isTp") Demo2ServiceImpl demo2ServiceImpl; /** *测试 demo2ServiceImpl.sayName()方法 */ @Test public void testSayName_1() { /** * 录制私有方法 isTp 返回值 */ new Expectations() { { /* * 该方法模拟实例上调用非访问的方法(private 方法) *@param mock 需要 mock 的实例对象 *@param methodName 预期的方法的名称 *@param methodArgs 调用零或多个非空预期的参数值 */ //无参私有方法调用 invoke(demo2ServiceImpl, "isTp"); result = false; //有参私有方法调用,withAny(..),是String 类型的任意字符串 invoke(demo2ServiceImpl, "isValid", withAny(String.class)); result = true; } }; //返回值正确 want.string(demo2ServiceImpl.sayName()).isEqualTo("Free Member"); want.string(demo2ServiceImpl.sayNameIsValid("valid")); } } Mock Guideline——基于 JTester 框架的 Mock 实践总结 12 6. Mock 构造函数 通常,构造函数都会做一些初始化数据的操作,比如说一个业务场景满足间隔 30 天去运行 某个任务,根据数据库查出来的日期与当前时间去比较(代码中一般是,new Date()), 这 样会因为时间流而导致测试用例失败。 如何Mock 构造函数,代码如下: 1. 业务类 Demo3Service.java public class Demo3Service { private String name; //设置默认值 public Demo3Service(){ name = "xiaoming"; } public void setName(String name) { this.name = name; } public String sayName() { return name; } } 2. Mock Mock Mock Mock 场景:测试Demo3Service 服务类,Mock 默认构造函数,动态改变默认初始 值,代码如下: Mock Guideline——基于 JTester 框架的 Mock 实践总结 13 public class Demo3ServiceTest extends JTester { @Test public void testSayName() { new MockUp() { // 变量名必须为 it,相当于 this 关键字. Demo3Service it; // mock 默认的构造函数 @SuppressWarnings("unused") @Mock public void $init() { //动态改变属性值 it.setName("xiaowang"); } }; want.string(new Demo3Service().sayName()).isEqualTo("xiaowang"); } } 如何Mock Date 日期类默认构造函数,参考实例:如何 mock 日期类 7. Mock 静态方法 与Mock Mock Mock Mock 普通方法相同 8. Mock 静态块 有一些类在静态块中做一些初始化工作,比如说读配置文件等。而配置文件很大程度上都是 依赖外部环境的。幸运的是,静态块同样可以被 Mock。 如何Mock 静态块,代码如下: 1. 业务类 Demo4Service.java public class Demo4Service { static String name; //静态块,初始化代码. static { name = "xiaoming"; } public static String sayName() { return name; } } 2. Mock Mock Mock Mock 场景:测试Demo4Service 服务类,Mock 静态块,动态改变静态块初始值, 代码如下: Mock Guideline——基于 JTester 框架的 Mock 实践总结 14 public class Demo4ServiceTest extends JTester { @Test public void testStaticMethod() { new MockUp() { // Mock 静态块代码 @SuppressWarnings("unused") @Mock public void $clinit() { // 静态块初始化代码... } }; //静态块代码被 Mock,为 null. want.string(Demo4Service.sayName()).isNull(); } } 9. 验证方法入参 问题:我们现在遇到这样一个业务方法,这个方法没有返回值,其逻辑过程是——根据 调用者提供的会员 ID,接着进行一些业务操作,比如会员信息查询等,最后根据获得的 信息发送贸易通提醒。 解决方法:我们应该根据贸易通 API 的入参来判断业务服务类中的逻辑是否正确的执行 了处理。 贸易通实现代码 风格-1,入参特点——基本类型(对于 Mock 框架而言): public class AliwwServiceImpl implements AliwwService { //API 风格-1:直接指定用户名和消息内容进行贸易通提醒 // 入参特点:基本类型(对于 Mock 框架而言) public void sendMessage(String memberId, String msg) { System.out.println("memberId=" + memberId + ", msg=" + msg); } } 针对风格-1的业务代码: Mock Guideline——基于 JTester 框架的 Mock 实践总结 15 public class BizVoidMethodServiceImpl implements BizVoidMethodService { AliwwService aliwwService; public void bizCodeWithNoReturnValue(String memberId) { String msg = "hello, " + memberId; aliwwService.sendMessage(memberId, msg); } …… } 测试代码: @SpringApplicationContext("applicationContext.xml") public class BizVoidMethodServiceTest extends JTester { @SpringBeanByName BizVoidMethodService bizVoidMethodService; @SpringBeanFor @Mocked AliwwService aliwwService; @Test public void testVoidMethod_入参是普通的字符串或者基本类型(){ new Expectations() { { aliwwService.sendMessage("abc", "hello, abc"); } }; bizVoidMethodService.bizCodeWithNoReturnValue("abc"); } } 贸易通实现代码 风格-2,入参特点——使用 VO 包装参数: Mock Guideline——基于 JTester 框架的 Mock 实践总结 16 public class AliwwServiceImpl implements AliwwService { //API 风格-2:使用 VO 包装参数进行贸易通提醒 // 入参特点:类型为 JavaBean public void sendMessage(MessageVO msgVO) { System.out.println( "memberId=" + msgVO.getMemberId() + ", msg=" + msgVO.getMsg()); } } 针对风格-2的业务代码: public class BizVoidMethodServiceImpl implements BizVoidMethodService { AliwwService aliwwService; public void bizCodeWithNoReturnValueUseMsgVO(String memberId) { MessageVO msgVO = new MessageVO(); msgVO.setMemberId(memberId); msgVO.setMsg("hello, " + memberId); aliwwService.sendMessage(msgVO); } …… } 测试代码: Mock Guideline——基于 JTester 框架的 Mock 实践总结 17 @SpringApplicationContext("applicationContext.xml") public class BizVoidMethodServiceTest extends JTester { @SpringBeanByName BizVoidMethodService bizVoidMethodService; @SpringBeanFor @Mocked AliwwService aliwwService; @Test public void testVoidMethod_入参是个对象(){ final String memberId = "abc"; new Expectations() { { aliwwService.sendMessage( (MessageVO) with(new PropertiesArrayMatcher( new String[] {"memberId", "msg" }, new Object[] { memberId, "hello, abc" }) ) ); } }; bizVoidMethodService.bizCodeWithNoReturnValueUseMsgVO(memberId); } } 贸易通实现代码 风格-3,入参特点——数组对象: public class AliwwServiceImpl implements AliwwService { //API 风格-3:对一批用户进行批量的贸易通提醒 // 入参特点:数组对象 public void sendMessage(String[] memberIds, String msg) { for (String memberId : memberIds) { System.out.println("memberId=" + memberId + ", msg=" + msg); } } } 针对风格-3的业务代码: Mock Guideline——基于 JTester 框架的 Mock 实践总结 18 public class BizVoidMethodServiceImpl implements BizVoidMethodService { AliwwService aliwwService; public void bizCodeWithNoReturnValueForMultiMembers(String... memberId) { aliwwService.sendMessage(memberId, "hello, everybody"); } …… } 测试代码: Mock Guideline——基于 JTester 框架的 Mock 实践总结 19 @SpringApplicationContext("applicationContext.xml") public class BizVoidMethodServiceTest extends JTester { @SpringBeanByName BizVoidMethodService bizVoidMethodService; @SpringBeanFor @Mocked AliwwService aliwwService; @Test public void testVoidMethod_入参是数组(){ new Expectations() { { /** * 对入参数组进行 mock,这里没有明确顺序 */ aliwwService.sendMessage( (String[]) with( IsArrayContainingInAnyOrder .arrayContainingInAnyOrder( new String[] {"member1", "member2" }) ), "hello, everybody" ); } }; // 因为 mock 过程中没有严格要求顺序, // 所以这里入参顺序与 mock 中的不一致也是没有问题的 // 我们这里只关注数组的元素内容一致就 ok了 bizVoidMethodService.bizCodeWithNoReturnValueForMultiMembers("memb er2", "member1"); } } 从上面三种场景,我们就可以知道,对应的方法的入参断言是如何进行的了。 10. Mock 方法代理 前面我们已经知道,公有,私有、包括静态块等都可以 mock。是不是觉得 Mock 的方式非 常像代理呢?对了,JMockit 也有自己的代理。如果在执行某个方法的时候,我们需要进行 拦截代理,Delegate 就派上大用场了。示例代码: Mock Guideline——基于 JTester 框架的 Mock 实践总结 20 public class DelegatesMockTest extends JTester { @Mocked Person person; @Test public void testGetSex() { new Expectations() { { /** Person 的getSex 被代理,不管原来逻辑如何,执行的都是被代理的逻辑 */ person.getSex((Boolean) any); result = new Delegate() { String getSex(boolean sexy) { if (sexy) { return "woman"; } else { return "man"; } } }; } }; want.string(person.getSex(true)).isEqualTo("woman"); } } 11. Strict 和NonStrict 1. JMockit 中strict and non-strict mocks 的区别 封装在 new Expectations() {...}块中的代码期望都是被严格执行的。他们在 record 后,都需要在 replay 阶段被严格调用(按照调用录制的先后顺序进行调用,如果调用 顺序不一 致,则 test Fail)。严格调用还包括以下情况: i. new Expectations() {...}块中已经录制的 invocation,在replay 阶 段却没有 发生调用,test Fail。示例代码: Mock Guideline——基于 JTester 框架的 Mock 实践总结 21 public class InvocationClass { private static TpRemoteService tp2RemoteService; public void setTp2RemoteService(TpRemoteService tp2RemoteService) { this.tp2RemoteService = tp2RemoteService; } /** *@param args */ public static void testMethod() { TpMemberIdsQueryParam param = new TpMemberIdsQueryParam(); List list = tp2RemoteService.listTpMemberIds(param); TpScoreCalcParam param2 = new TpScoreCalcParam(); tp2RemoteService.calcTpScore(param2); } Mock Guideline——基于 JTester 框架的 Mock 实践总结 22 public class TestStrict extends BaseTestCase { @Mocked TpRemoteService tp2RemoteService; @Test public void testTpRemoteService() { new Expectations() { { /** 手工注入 Service */ this.setField(InvocationClass.class, "tp2RemoteService", tp2RemoteService); tp2RemoteService.listTpMemberIds((TpMemberIdsQueryParam) any); tp2RemoteService.calcTpScore((TpScoreCalcParam) any); } }; InvocationClass.testMethod(); } } ii. 一个未被期望的调用发生。一个比如 TpRemoteService 被声明 Mock。 在new Expectations() {...}块中录制了方法 listTpMemberIds。但是在 replay 阶 段,listTpMemberIds 被正确调用,但是除了这个方法的调 用,还调用到了 TpRemoteService 的calcTpScore 方法。这个是没有在 new Expectations() {...}块进行录制,test Fail。 iii. new Expectations() {...} 块中录制的调用,如果 tpRemoteService.calcTpScore((TpScoreCalcParam) any);没有声明 time,则 默认time=1,也就是期望在调用中被调用一次。如果在 replay 阶段发生了多 次调用,则 test Fail。 2. non-strict mocks 是不严格的 mock。 录制发生在 new NonStrictExpectations (){...}块中。它不要求录制过的调用一定 Mock Guideline——基于 JTester 框架的 Mock 实践总结 23 会在replay 阶段发生,也不严格要求调用一定按照录制的顺序进行调用。上面列举 的Expectations 中的三种情况,到了 NonStrictExpectations 中,都是被允许的, 单元测试正常执 行。 3. Strict and non-strict mocks 的混合使用 在new Expectations() {...}块中允许声明 non-strict mocks 的类型和对象。例如通过在类 型或者 对象上标注@NonStrict, 就把可以把类型和对象声明为 non-strict mock。 示例代码: public class InvocationClass { private static TpRemoteService tp2RemoteService; public void setTp2RemoteService(TpRemoteService tp2RemoteService) { this.tp2RemoteService = tp2RemoteService; } public static void testMethod() { } } Mock Guideline——基于 JTester 框架的 Mock 实践总结 24 public class TestMixStrict extends BaseTestCase { @Test public void testTpRemoteService() { new Expectations() { @NonStrict TpRemoteService tp2RemoteService; { /** 手工注入 Service */ this.setField(InvocationClass.class, "tp2RemoteService", tp2RemoteService); tp2RemoteService.listTpMemberIds((TpMemberIdsQueryParam) any); } }; /**未调用事先录制的方法,未出错*/ InvocationClass.testMethod(); } } 按照 Strict mock 原则,此处录制的调用没有调用到,test Fail。但此处单元测试通过,因为 TpRemoteService 已经被声明为 NonStrict 。单元测试原则上每个分支都需要覆盖,所以建议使 用strict。 12. 使用 times , minTimes , maxTimes 从上面 Expectations 块的情况看,我们要求 replay 的时候,被调用的方法要按照我们事先录 制好的顺序来执行,默认每个录制的方法的执行次数都是 1。如果发生循环调用的时候,我 们难道需要去重复的录制同样的调用,这种情况一直是我们在编码中极力避免的事情。还好, JMockit 提供了 times 来指定该录制需要被执行几次。如下面代码: Mock Guideline——基于 JTester 框架的 Mock 实践总结 25 @Test public void testService() { new Expectations() { { /** 被调用 1次*/ subService.sayOverrideMethod(); result = "mockOverrideMethod"; times = 1; /** 被调用 1-2 次*/ subService.sayInheritMethod(); result = "mockInheritMethod"; maxTimes = 2;minTimes=1; } }; want.string(subService.sayOverrideMethod()).isEqualTo("mockOverrideMethod"); /** 只能调用一次,两次就 Fail */ // want.string(subService.sayOverrideMethod()).isEqualTo("mockOverrideMethod"); want.string(subService.sayInheritMethod()).isEqualTo("mockInheritMethod"); } 如果实际的执行情况不符合上述期望,则 Test Fail。同时实际的调用是按照次序来进行的, 如果times 被指定了大于 1,则该调用需要连续被执行多次。 13. 级联 mock 问题:我们调用外部接口服务,但是这种接口服务好像不太给力,返回的数据结构就是个接 口,而当我们希望从这个数据中再获取某些信息时,发现这个接口中的这些数据又是一个接 口对象。 举例说明: 这是我们依赖的服务接口: public interface MorganMemberService { Mock Guideline——基于 JTester 框架的 Mock 实践总结 26 Person findPersonByMemberId(String memberId); } 业务代码: public class QueryMemberServiceImpl implements QueryMemberService { private MorganMemberService morganMemberService; /** * 这里使用了会员服务的接口,但是其返回值类型 Person 是一个接口, * 而Person 中的 PersonInfo 也是一个接口 * 但是我们关心的只是会员信息中的手机号码 */ public boolean isMobilePhoneValid(String memberId) { Person person = morganMemberService.findPersonByMemberId(memberId); // 下面标红色的代码可能存在 NPE 异常,因为是示例代码,大家不要纠结哦~~ String mobileNo = (person == null ? null : person.getPersonInfo().getMobileNo()); return mobileNo != null && mobileNo.length() == 11; } public void setMorganMemberService(MorganMemberService morganMemberService) { this.morganMemberService = morganMemberService; } } 来看看 JMockit 如何对层次型的返回结果进行 mock 单元测试代码: @SpringApplicationContext("applicationContext.xml") public class TestMorganMemberService extends JTester { @SpringBeanByName QueryMemberService queryMemberService; Mock Guideline——基于 JTester 框架的 Mock 实践总结 27 /** * 对会员服务的接口进行级联 Mock 声明 */ @SpringBeanFor @Cascading MorganMemberService morganMemberService; @Test public void testQueryMemberMobile_指定我们期望的手机号码(){ new Expectations() { { /** * 声明过 Cascading 后的会员服务的层次型调用过程, * 就能像下面这样的语法进行编写 * 终于达到我们的目的——只关注会员的手机号码 */ morganMemberService.findPersonByMemberId("member_1") .getPersonInfo().getMobile No(); result = "12345678901"; } }; want.bool(queryMemberService.isMobilePhoneValid("member_1")).is(true); } @Test public void testQueryMemberMobile_查不到会员信息(){ new Expectations() { { morganMemberService.findPersonByMemberId("member_2"); result = null; } }; want.bool(queryMemberService.isMobilePhoneValid("abc")).is(false); } Mock Guideline——基于 JTester 框架的 Mock 实践总结 28 @Test public void testQueryMemberMobile_会员信息中没有手机号码(){ new Expectations() { { morganMemberService.findPersonByMemberId("abc").getPersonInfo().getMobileNo(); result = null; } }; want.bool(queryMemberService.isMobilePhoneValid("abc")).is(false); } } 另外一种情况: 我们对接口稍作调整,新增一个业务方法: public interface QueryMemberService { public boolean isMobilePhoneValid(String memberId); public boolean isMemberValid(String memberId); } 业务代码如下: public boolean isMemberValid(String memberId) { Person person = morganMemberService.findPersonByMemberId(memberId); if (person == null) { return false; } PersonInfo personInfo = person.getPersonInfo(); Mock Guideline——基于 JTester 框架的 Mock 实践总结 29 if (personInfo == null || personInfo.getMobileNo() == null || personInfo.getCommonPhoneNumber() == null) { return false; } return personInfo.getMobileNo().length() == 11 && personInfo.getCommonPhoneNumber().length() == 8; } 单元测试代码: 我们将返回值做级联 Mock,屏蔽复杂的数据结构 @Cascading Person person; @Test public void testQueryMemberMobile_指定我们期望的手机号码_和_电话号码(){ /** * 由于这里 Mock 的是返回值结果,所以我们不关心其方法的调用顺序, * 只要得到的属性值是我们所期望的就可以了 */ new NonStrictExpectations() { { person.getPersonInfo().getMobileNo(); result = "12345678901"; person.getPersonInfo().getCommonPhoneNumber(); result = "12345678"; } }; new Expectations() { { morganMemberService.findPersonByMemberId("member_1"); result = person; Mock Guideline——基于 JTester 框架的 Mock 实践总结 30 } }; want.bool(queryMemberService.isMemberValid("member_1")).is(true); } Mock Guideline——基于 JTester 框架的 Mock 实践总结 31 Mock Mock Mock Mock 常见场景 GuideLineGuideLineGuideLineGuideLine 1. Mock Morgan Morgan 中我们经常用的 Service 应该是 MorganMemberService。而MorganMemberService 是 一个Dubbo 服务,在单元测试中必须 Mock 掉。而困扰我们的是 MorganMemberService 的 很多方法值却是接口,而不是一个具体类。统计了服务,其中共有两个接口:Person 和 VAccount 。下面就以代码示例介绍如何对一个返回接口值的方法进行 Mock 。 MorganMemberDemonService 依赖到 MorganMemberService,我们对 MorganMemberService 的方法进行 Mock。 public class MorganMemberDemonService { private MorganMemberService morganMemberService; public void setMorganMemberService(MorganMemberService morganMemberService) { this.morganMemberService = morganMemberService; } public boolean isTP(String memberId) { VAccount vaccount = morganMemberService.findVAccountByMemberId(memberId); return vaccount.isTP(); } Person findPersonByMemberId(String memberId) { return morganMemberService.findPersonByMemberId(memberId); } } Mock Guideline——基于 JTester 框架的 Mock 实践总结 32 public class MorganMemberDemonServiceTest extends BaseTestCase { @SpringBeanByName MorganMemberDemonService morganMemberDemonService; @Mocked @SpringBeanFor MorganMemberService morganMemberService; @Test public void testService() throws Exception { new NonStrictExpectations() { @Cascading @NonStrict Person person; @Cascading @NonStrict VAccount vaccount; { /** 第一步:Mock 返回值接口 */ person.getPersonInfo().getCommonPhoneNumber(); result = "110"; /** 第二步:Mock 服务方法 */ morganMemberService.findPersonByMemberId((String) any); result = person; /** 第一步:Mock 返回值接口 */ vaccount.isTP(); result = true; /** 第二步:Mock 服务方法 */ morganMemberService.findVAccountByMemberId((String) any); result = vaccount; } }; /** 断言取出的 Person 的手机号 */ Person person = morganMemberDemonService.findPersonByMemberId("a"); want.string(person.getPersonInfo().getCommonPhoneNumber()).isEqualTo("110"); /** 断言是否是 Tp */ want.bool(morganMemberDemonService.isTP("a")).isEqualTo(true); } } Mock Guideline——基于 JTester 框架的 Mock 实践总结 33 上面例子中,返回 Person 和VAccount 的方法都被顺利 Mock 了。其他返回接口的服务方法 的Mock 实现类似。例子中涉及到级联 Cascading 知识,具体见级联章节。 2. Mock TpRemote 在开发中,很多时候我们需要通过调用 TpRemoteService 来查询对应用户的诚信通类型。而 很多时候我们希望根据传入的 memberId 来判断,返回哪种类型的诚信通类型。这种意图可 以通过代理的方式来实现。 @Test public void testFindMemberTpType() { new NonStrictExpectations() { { tpRemoteService.findTpType((TpTypeQueryParam) any); result = new Delegate() { @SuppressWarnings("unused") TpTypeModel findTpType(TpTypeQueryParam param) { //从TpTypeQueryParam 提取出 memberId 判断,决定返回哪种诚信通类型,代码略 TpTypeModel typeModel = new TpTypeModel(); typeModel.setType(TpType.ENTERPRISE_TP); return typeModel;//根据 memberId 判断后,返回值 } }; } }; TpTypeModel type = creditTpService.findMemberTpType("memberId"); want.object(type.getType()).isEqualTo(TpType.ENTERPRISE_TP); } 3. Mock Napoli 消息接口 Napoli 消息的发送和接收同其他 Service 服务的 Mock。下面以发送端服务为例说明。 业务服务类: Mock Guideline——基于 JTester 框架的 Mock 实践总结 34 public class NapoliDemon { private JmsMessageSender dwMessageSender; public void setDwMessageSender(JmsMessageSender dwMessageSender) { this.dwMessageSender = dwMessageSender; } public void send(Object message) { dwMessageSender.send(message); } } 单元测试类: public class NapoliTDemonest extends JTester { NapoliDemon napoliDemon = new NapoliDemon(); @Mocked JmsMessageSender dwMessageSender; @Test public void testSend() { new Expectations() { { this.setField(napoliDemon, "dwMessageSender", dwMessageSender); dwMessageSender.send((Object) any); } }; Object obj = new Object(); dwMessageSender.send(obj); } } new Expectations()块中,只是注入这个 Mock 服务的实例。录制的时候只是一个空操作。Mock Napoli 服务的原则应该就是使得发送消息的服务对于我们是透明的,不把这块纳入到我们的 业务逻辑中去。所以在业务代码中,应该把接收消息、发送消息跟我们具体的业务方法独立 Mock Guideline——基于 JTester 框架的 Mock 实践总结 35 开来。 4. 如何验证抛出异常分支 业务代码: public class ThrowExceptionServiceImpl implements ThrowExceptionService { public int doWhenEqual1ThenThrowException(int val) throws BizException { /** * 入参等于 1的时候,就会抛出异常! *(OMG~好可怕的业务代码,还好只是个示例☺) */ if (val == 1) { throw new BizException("some error message"); } return val; } } Mock 代码: @SpringApplicationContext("applicationContext.xml") public class TestExceptionThrowOut extends JTester { @SpringBeanByName ThrowExceptionService throwExceptionService; /** * 在Test 标注中设置两个属性 * expectedExceptions - 将会抛出的异常类型 * expectedExceptionsMessageRegExp - 异常对象中的信息内容(这个属性是可选的) */ @Test(expectedExceptions = { BizException.class }, expectedExceptionsMessageRegExp = "some error message") public void test_抛出异常_判断异常内容() throws BizException { throwExceptionService.doWhenEqual1ThenThrowException(1); } Mock Guideline——基于 JTester 框架的 Mock 实践总结 36 } 那样如何在代码中显示抛出异常呢?比如我调用到一个服务的方法,我希望这个方法抛 出异常,于是顺着分支会有一个处理逻辑。在测试的时候我需要测试这样的分支。于是, 我们可以这样来处理。 public class CaseTest extends JTester { @Mocked SubService subService; @Test public void testService() { new Expectations() { { subService.sayOverrideMethod(); result = new IllegalStateException(); } }; try { subService.sayOverrideMethod(); /** 如果没有抛出异常,走到下一步,主动去 Fail,一个逆向的测试 */ want.fail(); } catch (Exception e) { want.object(e.getClass()).isEqualTo(IllegalStateException.class); } } } 5. 如何 mock 日期类 问题:常常遇到一些单元测试需要依赖于时间进行各种操作,而我们的代码中通常使用的是 new Date()的方式来获取系统当前的日期时间,如何将这个时间点固定在我们期望的值上 Mock Guideline——基于 JTester 框架的 Mock 实践总结 37 呢?让我们的单元测试不会因为时间流失而失效。 参考代码: public class TestMockDate extends JTester { @Test public void testMockDate_指定日期必须是 2012 年12 月21 日(){ /** * Mock 掉java.util.Date 类 */ new MockUp() { /** * 这里声明一个"it"对象,可以用于在构造器内部访问被 Mock 的对象自身, * 如同我们常用的"this"关键字 */ Date it; /** * 这里对 Date 类的构造方法进行 Mock * 指定日期为 2012-12-21 */ @SuppressWarnings( {"unused" }) @Mock public void $init() { Calendar c = Calendar.getInstance(); c.set(Calendar.YEAR, 2012); c.set(2012, Calendar.DECEMBER, 21); it.setTime(c.getTimeInMillis()); } }; /** * 对Date 类的各个属性进行对比 */ want.object(new Date()) .is( Mock Guideline——基于 JTester 框架的 Mock 实践总结 38 new PropertiesArrayMatcher( new String[] {"year", "month", "date" }, new Object[] { 112, Calendar.DECEMBER, 21 } ) ); /** * 信不过断言?那就 System.out 看看是不是你要的日期 */ System.out.println(new Date()); } } 6. 如何 mock 日志类 业务代码: public class BizLoggerServiceImpl implements BizLoggerService { private Logger logger = LoggerFactory.getLogger("bizLogger"); public void bizWillLog(String[] memberIds) { if (memberIds != null) { if (logger.isInfoEnabled()) { logger.info(memberIds); } } else { logger.error("[BizLoggerServiceImpl.bizWillLog] param memberIds is null"); } } } 测试代码: @SpringApplicationContext("applicationContext.xml") public class TestMockLogger extends JTester { Mock Guideline——基于 JTester 框架的 Mock 实践总结 39 @SpringBeanByName /** * 不要惊慌,我们在 Expecations 代码段落中没有对原始服务类做任何期望, * 所以不会修改原有的代码 * 加这个 Mocked 标注,主要为了后续能够在 Expecations 中, * 使用 setField 方法替换掉原始服务类中的 logger 对象 */ @Mocked BizLoggerService bizLoggerService; @Test public void test_log_入参不为空(){ new Expectations() { // 这里我们创造一个假的 logger 类,这样就能屏蔽 logger 的具体实现 @Mocked Logger logger; { /** * 替换了原始服务类代码中的 logger 对象 */ setField(bizLoggerService, "logger", logger); logger.isInfoEnabled(); result = true; /** * 来自 hamcrest 框架的 with 工具 API,在判断各种参数方面还是非常方便的 */ logger.info( (String[]) with(IsArrayContainingInAnyOrder .arrayContainingInAnyOrder(new String[] {"1", "2" }) ) ); } Mock Guideline——基于 JTester 框架的 Mock 实践总结 40 }; // 入参的数组元素顺序和上面 with 语法中的顺序并不相同,但是不影响其执行过程 bizLoggerService.bizWillLog(new String[] {"2", "1" }); } @Test public void test_log_入参的数组为空(){ new Expectations() { @Mocked Logger logger; { setField(bizLoggerService, "logger", logger); /** * 业务操作的结果:当入参为空时,必然输出这么一个字符串 */ logger.error("[BizLoggerServiceImpl.bizWillLog] param memberIds is null"); } }; bizLoggerService.bizWillLog(null); } } Mock Guideline——基于 JTester 框架的 Mock 实践总结 41 基于场景编写单元测试 无论TDD、BDD 这些理念,都是为了我们更好的覆盖一个 API 所面对的功能需求。 TDD 主张我们要站在使用者的角度来思考一个 API 的设计,所以其用途应该说 50%为了 保障质量、另外 50%确实为了保障设计。 1、在质量层面 我们通常使用的是边写代码边写单元测试,或者先写代码再写单元测试的方式。 基本属于单元测试辅助策略,那么单元测试可以保障未来代码被改动的时候,需要我们 相应调整单元测试代码,起到了代码变更监控的作用。 2、在设计层面 如果一个类非常难以进行单元测试,我们可以认定它基本上是需要进行重构了,因为 大量依赖外部 API 和意图不明确的逻辑,说明其设计本身就是有问题的。 不去争论测试先行、测试辅助的问题(业内已有足够多的讨论),咱们现下需要面对的 问题是:如何写出好的单元测试? 这时候大家一定会想到两个指标名词——行覆盖率?还是分支覆盖率? 可是无论哪一个指标,真的能保证单元测试的质量么?当然不行。因为它们有一个前提 就是假定我们的 API 内部逻辑是正确的。 好了,关子卖到这里,咱们换个思路——我们应该关注的是 API 是否符合需求! 需求是什么?对于一个 API,正是——它会该给谁用,调用者关心的是什么,调用者应 用的功能场景是什么。 3、单元测试的效率 写出好的单元测试的代码不应该以牺牲开发效率为代价。单元测试会降低效率、造成时 间上的浪费吗?这取决于所说的效率是什么,以及所说的时间对象是谁。在一个纯粹编写新 代码的周期里,写单元测试的程序员所写代码可能会比不写单元测试的少。如果所说的效率 是指这个,那么单元测试确实会降低程序员的效率。 但是,我们很容易发现这种牵强的效率定义的问题所在。代码行并不是衡量效率的标 准,它只是所写代码的行数。从单个类到整个系统,我们可以发现很多代码行数已经远超出 了实际所需。我们需要的并不是更多的代码,而是准确的代码。单元测试可以让我们随时进 行代码层次上的真实性检查,它可以告诉我们是否在开发正确的东西。 目前我们所要做:纯熟掌握单元测试和 Mock 的技巧,把测试代码的开发效率提升到一 个新的高度、速度和精度。 Mock Guideline——基于 JTester 框架的 Mock 实践总结 42 变更记录表 文档版本 变更内容 变更时间 撰稿人 V1.0 初稿 2010.03.08 刘檩、潘俊俊、郭凯
还剩41页未读

继续阅读

下载pdf到电脑,查找使用更方便

pdf的实际排版效果,会与网站的显示效果略有不同!!

需要 10 金币 [ 分享pdf获得金币 ] 1 人已下载

下载pdf

pdf贡献者

lofe

贡献于2015-09-03

下载需要 10 金币 [金币充值 ]
亲,您也可以通过 分享原创pdf 来获得金币奖励!
下载pdf