JAVA单元测试Mock实践

摘要

本文介绍了Mock背景,常见的单元测试场景以及对应的测试方法,最后简单介绍了powermockit测试框架。理解本文内容,会带你入门java单元测试,其中第一章、第二章需要重点理解。

一、单元测试背景

单元测试是什么?

单元测试对软件中的最小可测试单元进行检查和验证。此处的单元可以是方法,类,功能,功能模块,或子系统。

单元测怎么做?

(1)代码走查人工校验业务逻辑

(2)自动化UT覆盖测试业务逻辑

例如:

  • 业务语句覆盖
  • 分支语句覆盖
  • 条件语句覆盖
  • 分支-条件语句覆盖
  • 路径覆盖等

二、Mock背景

Mock是什么?

Mock(模拟的)是一种隔离测试类功能的方法。例如:mock测试不需要真实连接数据库,或读取配置文件,或连接服务器。mock对象模拟真实服务,mock对象会返回与传递给它的某个虚拟输入相对应的虚拟数据。

为什么要使用Mock?

单元测试重点在于验证代码逻辑以及结果是否正确,但是在测试过程中经常会遇到如下痛点问题:

  • 接口之间的相互依赖
  • 第三方接口调用,例如连接数据库,连接https等

使用Mock框架可以模拟出外部依赖,只注重测试代码逻辑、验证代码结果,满足测试真实目的。

Mock框架使用流程

  1. 创建 外部依赖 的Mock 对象, 然后将Mock 对象注入到 测试类 中;

  2. 执行 测试代码

  3. 校验 测试代码 是否执行正确。

 本文所需的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
    }

(5)Mock无返回值方法

待测试方法执行逻辑中包含了serviceObject.method()方法,打桩方式如下:

PowerMockito.doNothing().when(serviceObject).method(param);

(6)Mock私有方法

待测试业务逻辑包含一些私有方法,打桩时需要加入@PrepareForTest注解
 

MockPrivateMethod mockPrivateMethod = PowerMockito.mock(MockPrivateMethod.class);

PowerMockito.when(mockPrivateMethod, "returnTrue").thenReturn(false);

打桩方法总结:

when().thenReturn()

when().thenThrow()

when().thenCallRealMethod()

when(file.exists()).thenThrow(Exception.class);

whenNew(File.class).withArguments("bbb").thenReturn(file);

3.3 测试私有方法

适用于公有方法中大量调用逻辑复杂的私有方法情况

(1)method中的业务逻辑代码 不存在其他bean依赖

import org.powermock.reflect.Whitebox;
...
Service service = new Service();
Whitebox.invokeMethod(service,"method",param);

(2)method中业务代码包含其他bean依赖

@InjectMocks
TestBean testBean;//待测试Bean
@Mock
ReplyBean replyBean;//待测试bean中依赖的bean
...
PowerMockito.doNothing().when(replyBean).methon1(param);//对ReplyBean的方法进行打桩
Method timeoutRun = PowerMockito.method(TestBean.class, "run", param.class);//获取待测试bean的私有方法
timeoutRun.invoke(testBean,param);//调用待测试bean的私有方法

3.4 结果校验方式

(1)常见assert校验

assertEquals(a,b)

assertFalse()和assertTrue()

ssertNull(a)和assertNotNull(a)

(2)方法调用次数校验

对象方法:Mockito.verify(object,Mockito.times(1)).method(param1,param2,...);

静态方法:PowerMockito.verifyStatic(LogUtils.class,Mockito.timeout(1));

3.3 FAQ

(1)运行UT抛出InvalidUseOfMatchersException异常

mock方法行为填入的参数必须全部为Mockito.any(*.class),不能某些参数是真实参数,某些是any()参数

PowerMockito.when(instance.method(Mockito.any(IT.class),Mockito.any(Asse.class),Mockito.any(Asse.class),
Mockito.any(Agen.class),Mockito.anyList(),Mockito.any(Processor.class))).thenReturn(true);

  • 3
    点赞
  • 37
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java单元测试中的Mock是指在测试过程中,对于某些不容易构造或者不容易获取的对象,用一个虚拟的对象来代替它,以便测试能够顺利进行。Mock可以模拟出一个对象的行为,使得测试人员可以在不依赖于真实对象的情况下进行测试。常用的Java Mock框架有Mockito和PowerMock等。下面是一个使用Mockito进行单元测试的例子: 假设我们有一个UserService类,其中有一个getUserById方法,该方法依赖于一个UserDao对象,我们可以使用Mockito来模拟UserDao对象的行为,从而测试getUserById方法的正确性。 ```java import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.class) public class UserServiceTest { @Mock private UserDao userDao; @InjectMocks private UserService userService; @Test public void testGetUserById() { User user = new User(); user.setId(1L); user.setName("Alice"); when(userDao.getUserById(1L)).thenReturn(user); assertEquals(user, userService.getUserById(1L)); } } ``` 在上面的例子中,我们使用了MockitoJUnitRunner来运行测试,使用@Mock注解来模拟UserDao对象,使用@InjectMocks注解来注入UserService对象。在testGetUserById方法中,我们使用when方法来指定当调用userDao.getUserById(1L)方法时,返回一个预设的User对象,然后使用assertEquals方法来判断userService.getUserById(1L)方法的返回值是否与预期相同。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值