摘要
本文介绍了Mock背景,常见的单元测试场景以及对应的测试方法,最后简单介绍了powermockit测试框架。理解本文内容,会带你入门java单元测试,其中第一章、第二章需要重点理解。
Mock(模拟的)是一种隔离测试类功能的方法。例如:mock测试不需要真实连接数据库,或读取配置文件,或连接服务器。mock对象模拟真实服务,mock对象会返回与传递给它的某个虚拟输入相对应的虚拟数据。
单元测试重点在于验证代码逻辑以及结果是否正确,但是在测试过程中经常会遇到如下痛点问题:
使用Mock框架可以模拟出外部依赖,只注重测试代码逻辑、验证代码结果,满足测试真实目的。
创建 外部依赖 的
Mock
对象, 然后将Mock
对象注入到 测试类 中;执行 测试代码;
校验 测试代码 是否执行正确。
<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>
使用Junit单元测试包进行测试。
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.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);
}
使用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种方式更加方便,尤其是被测试类中依赖许多其他对象时,注解方式更加高效。
- spring-boot框架中,类的依赖通过@Autowired注入,而非new创建,但两者本质一样。在springboot框架中,同样可以使用2.3中的方式进行单元测试。
- 此外,在springboot框架中还可以使用spring-boot-test包进行单元测试。
可直接使用junit测试包对代码逻辑进行测试,参考2.1章节。
方式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);
}
}
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);
- 当使用PowerMockito.whenNew方法时,必须加注解@PrepareForTest和@RunWith。注解@PrepareForTest里写的类是需要mock的new对象代码所在的类。
- 当需要mock final方法的时候,必须加注解@PrepareForTest和@RunWith。注解@PrepareForTest里写的类是final方法所在的类。
- 当需要mock静态方法的时候,必须加注解@PrepareForTest和@RunWith。注解@PrepareForTest里写的类是静态方法所在的类。
- 当需要mock私有方法的时候, 只是需要加注解@PrepareForTest,注解里写的类是私有方法所在的类
- 当需要mock系统类的静态方法的时候,必须加注解@PrepareForTest和@RunWith。注解里写的类是需要调用系统方法所在的类
先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
对象的行为结果.
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);
}
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);
}
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);