参考资料:
官方文档:
背景
今天再设计跟单拆分工程的单元测试时遇到了这么一个问题。
单元测试的目标方法是:BillTraceRuleHelper.operateBillTraceRule
,该方法用于将跟单规则请求 BillTraceRuleRequest
转化为跟单规则 BillTraceRule
,该方法中会有一个检查请求有效性的方法:checkRuleRequest
,该方法要求请求中需要包含详尽的参数,为了 偷懒 使用了jmock的mockUp方法对上面的方法进行mock,使其始终返回true
代码如下:private void mockCheckRuleRequest(){
new MockUp<BillTraceRuleHelper>(){
public ClientMessage checkRuleRequest(BillTraceRuleRequest req){
return new ClientMessage();
}
};
}
由此,产生了两个问题:
问题一、业务执行结果不符合预期
使用mock的测试
|
预期结果是update
操作能修改站点信息,delete
操作删除的信息中包含更改后的黑牛二部,然而实际结果却不是这样的。
以下是截取的部分update SQL语句:set RULE_SUBSCRIBE_SITE ='黑牛二部'
where RULE_KEY = 'fakeRuleKey'
and CUSTOMER_DATA_SOURCE = NULL
and RULE_TYPE = 'fakeRuleType'
这句update并未成功,gt_bill_trace_rule
未发生变化。
不使用Mock的测试
相比较使用mock的情况,本次测试的区别在于添加了以下两个参数request.setCusstomerDataSource("天网");
request.setRspMessageType("自定义反馈类型");
这样测试的结果就达到了预期。
为了找出造成这种差别的原因,我在PL/SQL中执行第一个测试的SQL脚本,发现select * from gt_bill_trace_rule t
where t.rule_key = 'fakeRuleKey'
and customer_data_source = null;
语句结果为空!这表明跟mock机制无关,其实这是一个SQL语法问题
在数据库中如果不填入数据,那么那一项就为null,而null并不是一个字符串,因此我们无法通过 =、!=、<> 等来判断是否为null。null作为一个bool类型,应该用is 或is not来判断是否为null。
问题二、在mock时进行调试无法跳转到被测试的方法中
在operateBillTraceRule()
中添加断点,调试时无法进入。
跟踪调用,发现进入的位置为JdkDynamicAopProxy.invoke
方法,如果mockUp
方法是定义AOP的切面的话,那么进入的位置应该是checkRuleRequest()
这个方法,那么为什么operateBillTraceRule()
也被动态代理了呢?
通过查阅API doc发现,mockUp方法进行造假的对象是其泛型参数T(即BillTraceRuleHelper
),而检查的方法只是指定造假的方法,动态代理的对象依然是这个helper
T - ++specifies the type (class, interface, etc.) to be faked++;
multiple interfaces can be faked by defining a type variable in the test class or test method, and using it as the type argument;
if a type variable is used and it extends a single type, then all implementation classes extending or implementing that base type are also faked;
if the type argument itself is a parameterized type, then only its raw type is considered
MockUp的两种方式
mock-up 的类继承了虚类mockit.MockUp<T>
,T是被模拟的类或接口。在运行时,模拟的方法或者构造函数会被拦截和重定向到相应的方法中(代理的机制),执行结束后将结果返回给调用者,一般而言,调用者是测试类,模拟类是一个依赖。
模拟类通过会在Junit中被定义成一个static、内部类或者匿名类。
如果定义完模拟类后,修改了实类某个被模拟方法的名字,将会导致IllegalArgumentExecption
java.lang.IllegalArgumentException: Matching real methods not found for the following mocks:
匿名类
I have a base class named TheBaseClass
:public class TheBaseClass {
public void doSomething(int i){
System.out.println(i);
}
public void doSomethingElse(){
System.out.println("do something else...");
}
}
在测试类中,定义了一个内部类,该内部类对doSomething
方法进行了模拟private void mocked(){
new MockUp<TheBaseClass>(){
public void doSomething(int i){
System.out.println(++i);
}
};
}
然后在单元测试中使用实类:
public void testMock(){
mocked();
TheBaseClass bb = new TheBaseClass();
bb.doSomething(1);
bb.doSomethingElse();
}
执行结果为:2
do something else...
非匿名类
这次,我在测试类中定义了一个内部类MockedBase
:class MockedBase extends MockUp<TheBaseClass>{
public void doSomething(int i){
System.out.println(--i);
}
}
在单元测试中,我需要新建一个MockedBase(否则无法触发代理):
public void testMock(){
new MockedBase();
TheBaseClass mb = new TheBaseClass();
mb.doSomething(1);
mb.doSomethingElse();
}
//output:
0
do something else
小结
- 我们不管将模拟类定义成内部类还是匿名类,其定义的模拟方法会被拦截,没有模拟的方法就还是原生的
- 如果从层级角度来看的话,每次新建一个模拟类相当于将其中声明的模拟方法置为可调用的最高层,相当于屏蔽了对原生方法。
- 模拟类需要新建才能生效
思考
如果MockUp
的对象是一个接口,而这个接口有一个实现类,那么最终执行的依然是实现类,而不是@Mock
的方法。这是为啥嘞?