最近有一个需求涉及到的外部系统别较多,只是一个小小的方法有5-6个rpc接口,还有4-5个查询数据库的连接,再加上开发环境,在自测(Junit)时发现环境各种不稳,所以决定将涉及到的相关接口mock掉。
环境准备 && 注意点
jdk + Junit + jmockit
注意:在pom文件中,jmock一定要放在junit的前面加载,不然会报错! 错误信息如下:
JMockit wasn't properly initialized; check that jmockit.jar precedes junit.jar in the classpath (if using JUnit; if not, check the documentation)
at com.alipay.fc.process.bp.engine.bpms.executor.UserTaskCreateExecutorTest$1.(UserTaskCreateExecutorTest.java:494)
at com.alipay.fc.process.bp.engine.bpms.executor.UserTaskCreateExecutorTest.mockBPTaskService(UserTaskCreateExecutorTest.java:494)
at com.alipay.fc.process.bp.engine.bpms.executor.UserTaskCreateExecutorTest.testCreatorAutoDeal2(UserTaskCreateExecutorTest.java:461)
接口准备
现准备如下接口,sellFish
卖鱼,接口如下:
- DAO
//卖鱼 -- delete操作
public interface FishMarketDAO {
/**
* 卖鱼
*/
boolean sellFish(Fish fish, Integer amount);
}
- 业务层的接口
public interface FishMarketInterface {
/**
* 卖鱼
*/
boolean sellFish(Fish fish, Integer amount);
}
- 业务层实现类
public class FishMarketManageImpl implements FishMarketInterface{
@Autowired
private FishMarketDAO fishMarketDAO;
/**
* 卖鱼
*/
@Override
public boolean sellFish(Fish fish, Integer amount) {
System.out.println("开始卖鱼,卖了" + amount + "条");
boolean sellResult = fishMarketDAO.sellFish(fish, amount);
if (sellResult) {
System.out.println("卖鱼成功");
} else {
System.out.println("卖鱼失败");
}
return sellResult;
}
}
- 测试类
@RunWith(JMockit.class)
public class FishTest {
@Tested
private FishMarketManageImpl fishMarketManage;
@Injectable
private FishMarketDAO fishMarketDAO;
@Test
public void testSellFish() {
new Expectations() {
{
fishMarketDAO.sellFish((Fish) any, 1);
result = true;
fishMarketDAO.sellFish((Fish) any, 2);
result = false;
}
};
boolean sellTrue = fishMarketManage.sellFish(new Fish(), 1);
Assert.assertTrue("卖鱼成功:", sellTrue);
boolean sellFalse = fishMarketManage.sellFish(new Fish(), 2);
Assert.assertTrue("卖鱼失败:", sellFalse);
}
}
卖鱼接口测试讲解
- 在测试类中声明需要测试的类,注意,这里可以直接声明一个实现类对象
@Tested
private FishMarketManageImpl fishMarketManage;
JMockit会通过反射自动实例化。
- 声明需要被mock的接口,并注解
@Injectable
假设实际测试中dao连接数据库极不稳定,正常测试100次接口才能成功一次,这种情况下,为了提升效率,我们会将外部接口进行mock。
@Injectable
private FishMarketDAO fishMarketDAO;
- 编写测试类,并注解
@Test
@Test
public void testSellFish() {
}
- 在测试类中写上期望mock的接口及对应方法的返回值
通过JMockit的Expectations可以实现该功能。这里演示mock最简单的public方法。
new Expectations() {
{
fishMarketDAO.sellFish((Fish) any, 1);
result = true;
fishMarketDAO.sellFish((Fish) any, 2);
result = false;
}
};
这里的(Fish) any
表示允许传入的是任何fish,后面的1、2表示:当传入1时,即卖一条鱼,对应的result=true
,即卖鱼成功,也即对应业务层接口中的成功
支路。
if (sellResult) {
System.out.println("卖鱼成功");
}
当传入2时,即fishMarketDAO.sellFish((Fish) any, 2);
表示卖出两条鱼,对应的result=false
,即卖鱼失败,也即对应业务层接口中的失败
支路。
else {
System.out.println("卖鱼失败");
}
这样就可以充分测试方法中的每一条支路。
备注:JMockit的mockit.Invocations抽象类中提供了几个任意值,如同上面的(Fish)any,你也可以将第二个参数置为anyInt,表示不管卖多少条鱼,都返回true或false
NonStrictExpectations/Expectations方式
@Test
public void testSellFish2() {
//录制预期模拟行为
new NonStrictExpectations() {
{
fishMarketDAO.sellFish((Fish) any, anyInt);
result = true; //卖任何数量的任何鱼都是true
}
};
boolean sell1 = fishMarketManage.sellFish(new Fish(), 1);
Assert.assertTrue("卖鱼1条:", sell1); //sell1=true
boolean sell2 = fishMarketManage.sellFish(new Fish(), 2);
Assert.assertTrue("卖鱼2条:", sell2); //sell2=true
//以下代码为了验证你的预期是否有被调用,也可以不写
new Verifications() {
{
fishMarketDAO.sellFish((Fish) any, 1);
times=1; //预期调用“卖一条任何鱼”一次 ==》true
fishMarketDAO.sellFish((Fish) any, 2);
times=1; //预期调用“卖两条任何鱼”一次 ==》true
fishMarketDAO.sellFish((Fish) any, 3);
times=1; //预期调用“卖三条任何鱼”一次 ==》false
}
};
}
以上测试的堆栈如下:
开始卖鱼,卖了1条
卖鱼成功
开始卖鱼,卖了2条
卖鱼成功
mockit.internal.MissingInvocation: Missing 1 invocation to:
com.mybank.bkpartner.mciservicewindowtest.FishMarketDAO#sellFish(com.mybank.bkpartner.mciservicewindowtest.Fish, Integer)
with arguments: any com.mybank.bkpartner.mciservicewindowtest.Fish, 3
on mock instance: com.mybank.bkpartner.mciservicewindowtest.$Impl_FishMarketDAO@5470be88
at com.mybank.bkpartner.mciservicewindowtest.FishTest$3.(FishTest.java:63)
at com.mybank.bkpartner.mciservicewindowtest.FishTest.testSellFish2(FishTest.java:56)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.lang.reflect.Method.invoke(Method.java:597)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.lang.reflect.Method.invoke(Method.java:597)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:51)
at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:237)
at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
Caused by: Missing invocations
at com.mybank.bkpartner.mciservicewindowtest.FishTest$3.(FishTest.java:62)
... 9 more
可以看出,卖出1条和2条鱼是可以成功的,当执行到预期调用“卖三条任何鱼”一次
时,就报错了Missing 1 invocation
,可以看到
mock静态方法
- 业务层中新增静态方法
sayHello
与catchFish
抓鱼方法
public static String sayHello() {
return "基本的hello";
}
/**
* 抓鱼
*/
@Override
public boolean catchFish(Fish fish, Integer amount) {
System.out.println("要打招呼:" + sayHello());
System.out.println("开始抓鱼,抓了" + amount + "条");
boolean catchResult = fishMarketDAO.catchFish(fish, amount);
if (catchResult) {
System.out.println("抓鱼成功");
} else {
System.out.println("抓鱼失败");
}
return catchResult;
}
- 测试方法
@Test
public void testCatchFish() {
new Expectations(FishMarketManageImpl.class) {
{
FishMarketManageImpl.sayHello();
result = "我是mock的hello语句";
fishMarketDAO.catchFish((Fish) any, 1);
result = true;
}
};
boolean catchResult = fishMarketManage.catchFish(new Fish(), 1);
Assert.assertTrue("抓鱼成功:", catchResult);
}
- 输出堆栈
Connected to the target VM, address: '127.0.0.1: ', transport: 'socket'
要打招呼:我是mock的hello语句
开始抓鱼,抓了1条
抓鱼成功
Disconnected from the target VM, address: '127.0.0.1: ', transport: 'socket'
Process finished with exit code 0
mock私有方法
- 业务层中新增私有方法
getPrivateHello
,并在catchFish
方法中调用
private String getPrivateHello() {
return "基本的私有hello";
}
/**
* 抓鱼
*/
@Override
public boolean catchFish(Fish fish, Integer amount) {
System.out.println("要打招呼:" + sayHello());
System.out.println("有私聊:" + getPrivateHello());
System.out.println("开始抓鱼,抓了" + amount + "条");
boolean catchResult = fishMarketDAO.catchFish(fish, amount);
if (catchResult) {
System.out.println("抓鱼成功");
} else {
System.out.println("抓鱼失败");
}
return catchResult;
}
- 测试方法
@Test
public void testCatchFish() {
new Expectations(FishMarketManageImpl.class) {
{
FishMarketManageImpl.sayHello();
result = "我是mock的hello语句";
Deencapsulation.invoke(fishMarketManage, "getPrivateHello");
result = "我是mock的私聊hello语句";
fishMarketDAO.catchFish((Fish) any, 1);
result = true;
}
};
boolean catchResult = fishMarketManage.catchFish(new Fish(), 1);
Assert.assertTrue("抓鱼成功:", catchResult);
}
- 输出堆栈
Connected to the target VM, address: '127.0.0.1: ', transport: 'socket'
要打招呼:我是mock的hello语句
有私聊:我是mock的私聊hello语句
开始抓鱼,抓了1条
抓鱼成功
Disconnected from the target VM, address: '127.0.0.1: ', transport: 'socket'
Process finished with exit code 0