最近在公司做需求,要求开发需要有相应的单元测试代码,第一次做单测相关的知识,就在这做一篇总结
一、JUnit
JUnit是Java最基础的测试框架,主要的作用就是断言。
方法名 |
方法描述 |
assertEquals |
断言传入的预期值与实际值是相等的 |
assertNotEquals |
断言传入的预期值与实际值是不相等的 |
assertArrayEquals |
断言传入的预期数组与实际数组是相等的 |
assertNotNull |
断言传入的对象是不为空 |
assertFalse |
断言条件为假 |
assertSame |
断言两个对象引用同一个对象,相当于“==” |
assertThat |
断言实际值是否满足指定的条件 |
常用注解
注解名 |
含义 |
@Test |
表示此方法为测试方法 |
@Before |
在每个测试方法前执行,可做初始化操作 |
@After |
在每个测试方法后执行,可做释放资源操作 |
@BeforeClass |
在类中所有方法前运行。此注解修饰的方法必须是static void |
@Parameters |
指定测试类的测试数据集合 |
@FixMethodOrder |
指定测试类中方法的执行顺序 |
@RunWith |
指定该测试类使用某个运行器 |
JUnit常用方法
我们测试下面这个简单的时间转换工具类,来说明一下具体的用法。
class DateUtil {
companion object {
// 英文全称 如:2017-11-01 22:11:00
val FORMAT_YMDHMS = "yyyy-MM-dd HH:mm:ss"
/**
* 掉此方法输入所要转换的时间输入例如("2017-11-01 22:11:00")返回时间戳
*
* @param time
* @return 时间戳
*/
@Throws(ParseException::class)
fun dateToStamp(time: String): Long {
val sdr = SimpleDateFormat(FORMAT_YMDHMS, Locale.CHINA)
val date = sdr.parse(time)
return date.time
}
/**
* 将时间戳转换为时间
*/
fun stampToDate(lt: Long): String {
val simpleDateFormat = SimpleDateFormat(FORMAT_YMDHMS, Locale.CHINA)
val date = Date(lt)
return simpleDateFormat.format(date)
}
}
}
我们发现预期值和实际结果不符合,测试失败
接下来测试dateToStamp方法,这里写两个断言方法
左下角这个绿色的勾勾就表示运行成功啦
测试异常
比如上述的dateToStamp()方法中,可能会抛出ParseException异常,我们可以验证是否抛出
验证一个方法是否抛出了异常,可以给@Test注解设置expected参数来实现
参数化测试
连续执行三次都成功(这里需要注意,每一次执行都是独立的,相互不影响)
二、Mockito
在实际的单元测试中,我们测试的类之间会有或多或少的耦合,导致我们无法顺利的进行测试,这时我们就可以使用Mockito,Mockito库能够Mock(我喜欢理解为模拟)对象,替换我们原先依赖的真实对象,这样我们就可以避免外部的影响,只测试本类,得到更准确的结果。
常用的Mock方式
class MockitoTest {
/**
* 方法1
*/
@Test
fun testIsNotNull() {
val person1 = mock(Person::class.java)
Assert.assertNotNull(person1)
}
/**
* 方法2 注解方法
*/
@Mock
lateinit var person2: Person
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
}
@Test
fun testIsNotNull2() {
Assert.assertNotNull(person2)
}
/**
* 方法3 MockitoRule
*/
@Mock
lateinit var person3: Person
@Rule
public var mockitoRule = MockitoJUnit.rule()
@Test
fun testIsNotNull3() {
Assert.assertNotNull(person3)
}
}
常用打桩方法
因为Mock出的对象中非void方法都将返回默认值,比如int方法将返回0,对象方法将返回null等,而void方法将什么都不做。“打桩”顾名思义就是将我们Mock出的对象进行操作,比如提供模拟的返回值等,给Mock打基础。
方法名 |
方法描述 |
thenReturn(T value) |
设置要返回的值 |
thenThrow(Throwable… throwables) |
设置要抛出的异常 |
thenAnswer(Answer<?> answer) |
对结果进行拦截 |
doReturn(Object toBeReturned) |
提前设置要返回的值 |
doCallRealMethod() |
调用某一个方法的真实实现 |
doNothing() |
设置void方法什么也不做 |
举例
open class Person constructor(name: String, sex: String) {
val mName: String
val mSex: String
init {
mName = name;
mSex = sex
}
open fun getName(): String {
return mName
}
open fun getSex(): String {
return mSex
}
open fun eat(something: String): String {
return "吃什么吐什么"
}
}
成功返回小明并且抛出异常
thenAnswer——对方法进行拦截
行为测试
前面所说的都是状态测试,但是如果不关心返回结果,而是关心方法有否被正确的参数调用过,这时候就应该使用验证方法了。从概念上讲,就是和状态测试所不同的“行为测试”了
verify(T mock)验证发生的某些行为
方法名 |
方法描述 |
after(long millis) |
在给定的时间后进行验证 |
timeout(long millis) |
验证方法执行是否超时 |
atLeast(int minNumberOfInvocations) |
至少进行n次验证 |
atMost(int maxNumberOfInvocations) |
至多进行n次验证 |
times(int wantedNumberOfInvocations) |
验证调用方法的次数 |
only() |
验证方法只被调用一次,相当于times(1) |
@Test
fun testPersonVerifyAfter() {
Mockito.`when`(person2.getName()).thenReturn("jack")
// 延时一秒验证
println(person2.getName())
println(System.currentTimeMillis())
verify(person2, after(1000)).getName()
println(System.currentTimeMillis())
}
@Test
fun testPersonVerifyAtLeast() {
person2.getSex()
person2.getSex()
// 至少验证2次
verify(person2, atLeast(2)).getSex()
// 该方法验证了两次
verify(person2, times(2)).getSex()
}
// 常用参数匹配器
@Test
fun testPersonAny() {
`when`(person2.eat(ArgumentMatchers.anyString())).thenReturn("一日三餐很固定")
println(person2.eat("很固定?"))
`when`(person2.eat(ArgumentMatchers.contains("面"))).thenReturn("面食")
println(person2.eat("刀削面"))
}
验证执行顺序
将最后的
spy
Mockito框架不支持mock匿名类、final类、static方法、private方法。而PowerMock框架解决了这些问题。
三、PowerMock
package com.example.testdateunit
abstract class Fruit {
private val fruit = "水果"
open fun getFruit(): String {
return fruit
}
}
open class Banana: Fruit() {
companion object {
private val COLOR = "黄色的"
@JvmStatic
fun getColor(): String {
return COLOR
}
}
private fun flavor(): String {
return "甜甜的"
}
fun getBananaInfo(): String {
return flavor() + getColor()
}
fun isLike(): Boolean {
return true
}
}
@PrepareForTest(Banana.class)
@RunWith(PowerMockRunner.class)
public class BananaTest {
@Test
public void testMethod() throws Exception {
Banana banana = PowerMockito.spy(new Banana());
// 返回正常的值
System.out.println(banana.getBananaInfo());
// mock私有方法
PowerMockito.when(banana, "flavor").thenReturn("不甜了");
System.out.println(banana.getBananaInfo());
// 修改静态常量
Whitebox.setInternalState(Banana.class, "COLOR", "红色");
System.out.println(Banana.getColor());
// mock静态方法
PowerMockito.mockStatic(Banana.class);
PowerMockito.when(Banana.getColor()).thenReturn("绿色");
System.out.println(Banana.getColor());
// mock夫类私有变量
MemberModifier.field(Banana.class, "fruit").set(banana, "蔬菜");
System.out.println(banana.getFruit());
// mock fina方法
PowerMockito.when(banana.isLike()).thenReturn(true);
}
}
上面我们有说到使用PowerMock就必须加@RunWith(PowerMockRunner.class),但是我们毕竟有时会使用多个测试框架,可能@RunWith会占用。这时我们可以使用@Rule。代码如下:
@Rule
public PowerMockRule rule = new PowerMockRule();
/**
* 这种方法需要加入依赖
* testCompile "org.powermock:powermock-module-junit4-rule:1.7.3"
* testCompile "org.powermock:powermock-classloading-xstream:1.7.3"
*/