摘要
本文介绍了Mock背景,常见的单元测试场景以及对应的测试方法,最后简单介绍了powermockit测试框架。理解本文内容,会带你入门java单元测试,其中第一章、第二章需要重点理解。
一、背景
Mock是什么?
Mock(模拟的)是一种隔离测试类功能的方法。例如:mock测试不需要真实连接数据库,或读取配置文件,或连接服务器。mock对象模拟真实服务,mock对象会返回与传递给它的某个虚拟输入相对应的虚拟数据。
为什么要使用Mock?
单元测试重点在于验证代码逻辑以及结果是否正确,但是在测试过程中经常会遇到如下痛点问题:
- 接口之间的相互依赖
- 第三方接口调用,例如连接数据库,连接https等
使用Mock框架可以模拟出外部依赖,只注重测试代码逻辑、验证代码结果,满足测试真实目的。
Mock框架使用流程
- 创建 外部依赖 的
Mock
对象, 然后将Mock
对象注入到 测试类 中;- 执行 测试代码;
- 校验 测试代码 是否执行正确。
本文所需的maven依赖
<scope>test</scope>表示仅作用于测试目录。默认作用于范围compile表示被依赖项目需要参与当前项目的编译,包含测试目录。
**mvn -Dmaven.test.skip clean install ** 方式可以不编译测试用例
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-junit4</artifactId>
<version>2.0.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-api-mockito2</artifactId>
<version>2.0.2</version>
<scope>test</scope>
</dependency>
二、单元测试待测内容有哪些分类?
2.1 无框架java代码
使用Junit单元测试包进行测试。
1.被测试类中方法无其他依赖
1)普通无依赖静态方法
静态方法无其他依赖类,可以直接调用方法计算结果。只需要构造期望数据,计算实际数据,最后比对数据。
代码示例:
被测试类:
public class MyMath {
public static int add(int num1,int num2) {
return num1 + num2;
}
}
测试代码:
//测试静态方法
@Test
public void testStaticAdd() {
int num1 = 1;
int num2 = 1;
int expect = 2;
int real = MyMath.add(num1, num2);
Assert.assertEquals(expect, real);
}
2)非静态无依赖方法
代码示例:
被测试类:
public class MyMath {
public int multi(int num1,int num2) {
return num1 * num2;
}
}
测试代码:
@Test
public void testMulti() {
int num1 = 2;
int num2 = 3;
int expect = 6;
int real = new MyMath().multi(num1, num2);
Assert.assertEquals(expect, real);
}
2.被测试类中方法存在其他可实现依赖
存在其他可实现依赖时,和2.1测试思路一致。
代码示例:
被测试类:
public class MyRectangle {
int height;
int width;
MyMath myMath = new MyMath();
public void setHeight(int height) {
this.height = height;
}
public void setWidth(int width) {
this.width = width;
}
public int getArea() {
return myMath.multi(width, height);
}
public int getPerimeter() {
return myMath.multi(2, MyMath.add(width, height));
}
}
测试代码:
@Test
public void testRectangleGetArea() {
int width = 2;
int height = 3;
MyRectangle rectangle = new MyRectangle();
rectangle.setWidth(width);
rectangle.setHeight(height);
int expectedArea = 6;
int realArea = rectangle.getArea();
Assert.assertEquals(expectedArea, realArea);
}
3..被测试类存在难以实现的其他依赖
使用Mock框架,构造出虚拟的依赖,如有需要,可对虚拟依赖进行打桩以满足测试要求。
打桩:用来代替依赖的代码,或未实现的代码。对构造出的虚拟对象,定制其行为,以满足测试逻辑要求。
测试service层代码举例:
service层代码:需要依赖dao层类从数据库获取数据。
public class RectangleService {
RectangleDao rectangleDao = new RectangleDao();//rectangleDao未开发完成
public int getRectangleAreaById(String id) {
MyRectangle myRectangle = rectangleDao.getRectangleById(id);
return myRectangle.getArea();
}
}
Dao层代码:dao层代码未开发或需要连接数据库,不好操作。
public class RectangleDao {
public MyRectangle getRectangleById(String id) {
//代码未开发
return new MyRectangle();
}
}
测试用例代码:mock dao层对象,并通过打桩方式定制其行为。
方式1:将mock的rectangleDao对象,通过java反射方式设置到rectangleService对象中
@RunWith(PowerMockRunner.class)
public class RectangleServiceTest {
/**
* 此处仅测试RectangleService类代码逻辑,因此该类的依赖需要mock出来,并打桩(自定义对象的行为)
*/
@Test
public void testRectangleService() throws Exception{
//构造service内部依赖的rectangleDao对象,
RectangleDao rectangleDao = PowerMockito.mock(RectangleDao.class);
PowerMockito.when(rectangleDao.getRectangleById("1")).thenReturn(new MyRectangle(2,3));
//通过反射的方式,将mock出来的rectangleDao配置到rectangleService中
RectangleService rectangleService = new RectangleService();
Field field = rectangleService.getClass().getDeclaredField("rectangleDao");
field.setAccessible(true);
field.set(rectangleService, rectangleDao);
//构造期望数据,计算实际数据,比对两者
MyRectangle myRectangle = new MyRectangle(2,3);
int expectedArea = myRectangle.getArea();
int actualArea = rectangleService.getRectangleAreaById("1");
Assert.assertEquals(expectedArea, actualArea);
}
}
方式2:将mock的rectangleDao对象,通过注解的方式设置到rectangleService对象中
Note:
- @Mock注解:创建一个Mock对象。
- @InjectMocks注解:创建一个实例对象,将其他用@Mock、@Spy注解创建的对象注入到用该实例中。
- @Before注解:junit中的注解,表示每一个@Test注解测试用例执行前,都会执行一遍。
/**
* 此处仅测试RectangleService类代码逻辑,因此该类的依赖需要mock出来,并打桩(自定义对象的行为)
*/
@RunWith(PowerMockRunner.class)
public class RectangleServiceTest {
@InjectMocks //将其他用@Mock(或@Spy)注解创建的对象设置到下面对象中
RectangleService rectangleService;//创建bean(类似new RectangleService)
@Mock
RectangleDao rectangleDao;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);//初始化上面@Mock和@InjectMocks标注对象
}
@Test
public void testRectangleService() throws Exception{
//打桩
PowerMockito.when(rectangleDao.getRectangleById("1")).thenReturn(new MyRectangle(2,3));
//构造期望数据,计算实际数据,比对两者
MyRectangle myRectangle = new MyRectangle(2,3);
int expectedArea = myRectangle.getArea();
//调用实际数据并对比
int actualArea = rectangleService.getRectangleAreaById("1");
Assert.assertEquals(expectedArea, actualArea);
}
}
显然,第2种方式更加方便,尤其是被测试类中依赖许多其他对象时,注解方式更加高效。
2.2 依赖框架的java代码
- spring-boot框架中,类的依赖通过@Autowired注入,而非new创建,但两者本质一样。在springboot框架中,同样可以使用2.3中的方式进行单元测试。
- 此外,在springboot框架中还可以使用spring-boot-test包进行单元测试。
1.被测试类不存在难以实现的其他依赖
可直接使用junit测试包对代码逻辑进行测试,参考2.1章节。
2.被测试类存在难以实现的其他依赖
方式1:类似2.1.3,直接使用powermock框架对代码进行测试
代码示例:
service层:依赖dao层方法。
和无框架代码区别在于,springboot框架内开发的bean都交给spring IOC容器管理,使用时直接注入而非new对象,但两者本质一样。
@Service
public class RectangleService {
@Autowired
RectangleDao rectangleDao;//rectangleDao未开发完成
public int getRectangleAreaById(String id) {
MyRectangle myRectangle = rectangleDao.getRectangleById(id);
return myRectangle.getArea();
}
}
dao层:和数据库交互。
@Component
public class RectangleDao {
public MyRectangle getRectangleById(String id) {
//代码未开发
return new MyRectangle();
}
}
测试用例:
@RunWith(PowerMockRunner.class)
public class RectangleServiceTest {
@InjectMocks //将其他用@Mock(或@Spy)注解创建的对象设置到下面对象中
RectangleService rectangleService;//创建bean(类似new RectangleService)
@Mock
RectangleDao rectangleDao;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);//初始化上面@Mock和@InjectMocks标注对象
}
@Test
public void testRectangleService() throws Exception{
//打桩
PowerMockito.when(rectangleDao.getRectangleById("1")).thenReturn(new MyRectangle(2,3));
//构造期望数据,计算实际数据,比对两者
MyRectangle myRectangle = new MyRectangle(2,3);
int expectedArea = myRectangle.getArea();
//调用实际数据并对比
int actualArea = rectangleService.getRectangleAreaById("1");
Assert.assertEquals(expectedArea, actualArea);
}
}
方式2:使用spring-boot-test框架对代码进行测试
代码示例:
使用springboot-test的注解。
@RunWith(SpringRunner.class)
@SpringBootTest
public class RectangleServiceTest2 {
@Autowired //注入spring IOC容器管理的bean
RectangleService rectangleService;//创建bean(类似new RectangleService)
@MockBean //mock对象
RectangleDao rectangleDao;
@Test
public void testRectangleService() throws Exception{
//打桩
Mockito.when(rectangleDao.getRectangleById("1")).thenReturn(new MyRectangle(2,3));
//构造期望数据,计算实际数据,比对两者
MyRectangle myRectangle = new MyRectangle(2,3);
int expectedArea = myRectangle.getArea();
//调用实际数据并对比
int actualArea = rectangleService.getRectangleAreaById("1");
Assert.assertEquals(expectedArea, actualArea);
}
}
三、PowerMock框架核心方法
3.1 PowerMock创建mock对象
T PowerMock.mock(Class<T> type);//创建模拟对象,支持final和native方法
public static <T> void spy(Class<T> type);//创建真实对象
代码示例:
//方式1:注解
@Mock
RectangleDao rectangleDao;
//方式2:创建
RectangleDao rectangleDao = PowerMockito.mock(RectangleDao.class);
//方式1:注解
@Spy
RectangleDao rectangleDao;
//方式2:创建
RectangleDao rectangleDao = PowerMockito.spy(new RectangleDao);
3.2 对mock对象方法进行打桩
- 当使用PowerMockito.whenNew方法时,必须加注解@PrepareForTest和@RunWith。注解@PrepareForTest里写的类是需要mock的new对象代码所在的类。
- 当需要mock final方法的时候,必须加注解@PrepareForTest和@RunWith。注解@PrepareForTest里写的类是final方法所在的类。
- 当需要mock静态方法的时候,必须加注解@PrepareForTest和@RunWith。注解@PrepareForTest里写的类是静态方法所在的类。
- 当需要mock私有方法的时候, 只是需要加注解@PrepareForTest,注解里写的类是私有方法所在的类
- 当需要mock系统类的静态方法的时候,必须加注解@PrepareForTest和@RunWith。注解里写的类是需要调用系统方法所在的类
(1)mock非final类(接口、普通类、虚基类)的非final方法:
先mock对象,然后对mock的对象进行打桩顶起方法行为,最后比对结果。
public class RectangleTest {
@Test
public void testObjectNormalMethod(){
MyRectangle myRectangle = PowerMockito.mock(MyRectangle.class);
PowerMockito.when(myRectangle.getArea()).thenReturn(6);
int expectArea = 6;
int actualArea = myRectangle.getArea();
Assert.assertEquals(expectArea,actualArea);
}
}
注:其中when(...).thenReturn(...)表示,当对象
rectangle
调用
getArea(0)
方法,并且参数为
0
时,返回结果为
0
,这相当于定制了
mock
对象的行为结果.
(2)模拟final类或final方法:
final方法所在的类,需要通过@PrepareForTest注解设置,然后mock对象,再然后打桩(指定方法行为),最后对比。
@Test
@PrepareForTest(MyRectangle.class)
public void testObjectFinalMethod(){
MyRectangle myRectangle = PowerMockito.mock(MyRectangle.class);
PowerMockito.when(myRectangle.getFinalArea()).thenReturn(6);
int expectArea = 6;
int actualArea = myRectangle.getFinalArea();
Assert.assertEquals(expectArea,actualArea);
}
(3)mockStatic方法
static方法所在的类,需要通过@PrepareForTest注解设置,mock静态方法所在的类,再然后打桩(指定方法行为),最后对比。
public static void mockStatic(Class<?> classToMock);//模拟类的静态方法
@Test
@PrepareForTest(MyRectangle.class)
public void testObjectStaticMethod(){
PowerMockito.mockStatic(AreaUtils.class);
PowerMockito.when(AreaUtils.getStaticArea(new MyRectangle(2,3))).thenReturn(6);
int expectArea = 6;
int actualArea = AreaUtils.getStaticArea(new MyRectangle(2,3));
Assert.assertEquals(expectArea,actualArea);
}
(4)mock方法的参数
mock不好实际调用的类,然后打桩mock的参数对象的方法行为,最后对比结果。
AreaUtils类:
public class AreaUtils {
public static int getStaticArea(MyRectangle rectangle){
return rectangle.height * rectangle.width;
}
public boolean callArgumentInstance(File file) {
return file.exists();
}
}
测试代码:
@Test
public void testMockObject() {
File file = PowerMockito.mock(File.class);//假设file对象不好实现,这里构造一个file对象
AreaUtils areaUtils = new AreaUtils();
PowerMockito.when(file.exists()).thenReturn(true);//定制file对象的方法行为:file.exists方法时,设置其返回值为true
Assert.assertTrue(areaUtils.callArgumentInstance(file));//传入构造好的file对象。由于造的file对象的file.exists()方法返回值为true,因此调用demo.call方法返回的就是true
}
打桩方法总结:
when().thenReturn()
when().thenThrow()
when().thenCallRealMethod()
when(file.exists()).thenThrow(Exception.class);
whenNew(File.class).withArguments("bbb").thenReturn(file);
版权归原作者 Mr. 阿柴 所有, 如有侵权,请联系我们删除。