单元测试基本概念 及 动机
单元测试基本步骤
- 初始化——准备一些测试前提条件。例如新建需要测试的类的实例
- 调用被测试的方法
- 验证结果——测试结果是否与预期一致
- 释放资源或删除文件(optional)
单元测试重点
单元测试的测试重点是测试类的 public
方法,对于其中的 private
方法的具体实现不关心。
单元测试方法
- 对于直接返回结果方法:可以用
Junit4
测试框架的assert
语句 - 对于
void
类型的方法:需要利用mock
的测试框架,例如Mockito
单元测试动机
TDD(Test Drived Development)
在项目初期采用TDD开发,在方法实现前,先写对应的单元测试代码,需要注意方法的Input 和 Output,不去想具体实现细节。也可以考虑方法的一些异常,各种情况的预期结果。这样会对方法的边界以及架构有更清晰的认识。
JUnit4 测试框架 和 Kotlin.test 库的使用
JUnit 4 框架
JUnit4 框架是Java中最基础的测试框架。
验证方法返回结果
class Calculator {
fun divide(a: Int, b: Int): Double{
if(b == 0) throw IllegalArgumentException("Cannot divided by zero")
return a.toDouble()/b
}
fun plus(a: Int, b: Int): Int{
return a+b
}
}
class CalculatorTest {
@Test
fun testDivide(){
val calculator = Calculator()
val result = calculator.divide(2,1)
val result1 = calculator.plus(1,1)
// 浮点数必须输入误差参数
Assert.assertEquals(2.0,result,0.0001)
Assert.assertEquals(2,2)
}
}
更多验证结果的用法,参看JUnit4 官方文档:Assertions
验证 Exception
验证Exception需要在 @Test
注解上加上 expected
参数,表明期望出现的异常。如果测试代码中没有抛出异常则会报错。
不过这种验证异常的方法还是有所限制的:不能验证异常中的 message,也无法验证出现异常中其他属性的值,不过 JUnit 4 提供了另外一种方式可以验证异常,可以解决这些问题。
// 验证异常
@Test(expected = IllegalArgumentException:: class)
fun testDivide(){
val calculator = Calculator()
calculator.divide(2,0)
}
初始化 @Before
和收尾工作 @After
例如在 CalculatorTest
中的两个方法 testDivide()
和 testPlus()
都需要有相同的初始化工作—— 创建 Calculator
对象。而 @Before
和 @After
可以很好地将相同的初始化和收尾工作抽象出来。
class CalculatorTest {
lateinit var calculator: Calculator
@Before
fun setup(){
calculator = Calculator()
}
@After
fun cleanup(){
}
// 验证异常
@Test(expected = IllegalArgumentException:: class)
fun testDivide(){
calculator.divide(2,0)
}
@Test
fun testPlus(){
val result1 = calculator.plus(1,1)
Assert.assertEquals(2,result1)
}
}
Output:
Before 运行1次
After 运行1次
Before 运行1次
After 运行1次
Process finished with exit code 0
我们可以看到, setup()
总共运行了两次,并且 count
并没有变,说明两次 setup()
是并行的。
@BeforeClass
和@AfterClass
其实在例子中 setup()
和 cleanup()
方法不需要在每个测试方法之前都运行一次, @BeforeClass
标记的方法会在该类的测试方法运行前运行一遍,只会执行一次,然后在所有测试方法运行完后会运行一次 @AfterClass
标记的方法。
不过 @BeforeClass
和 @AfterClass
注解,标记的方法应该为静态方法。
class CalculatorTest {
companion object{
lateinit var calculator: Calculator
var count = 0
var count1 = 0
@BeforeClass
@JvmStatic
fun setup(){
calculator = Calculator()
count += 1
print("Before 运行${count}次")
}
@AfterClass
@JvmStatic
fun cleanup(){
count1 += 1
print("After 运行${count1}次")
}
}
// 验证异常
@Test(expected = IllegalArgumentException:: class)
fun testDivide(){
calculator.divide(2,0)
}
@Test
fun testPlus(){
val result1 = calculator.plus(1,1)
Assert.assertEquals(2,result1)
}
}
Output:
Before 运行1次After 运行1次
Process finished with exit code 0
JUnit 的其他一些方法
忽略某些测试方法
有时因为一些原因,例如正式代码还没有实现,想让 JUnit 暂时不允许某些测试方法,这时就可以使用 @Ignore
注解,例如:
class CalculatorTest {
lateinit var calculator: Calculator
@Test
@Ignore("not implemented yet")
fun testSubtract() {}
...
fail 方法
有时候可能需要故意让测试方法运行失败,例如在 catch 到某些异常时,这时可以使用fail方法:
Assert.fail()
Assert.fail(message)
TestRule
JUnit 4 中的 TestRule
可以达到同样的效果, Rule
在测试类中声明后,测试类中的所有测试方法都要遵守Rule。
TestRule
可以很方便地添加额外代码或者重新定义测试行为。Junit 4 中自带的 Rule
有 ErrorCollector
、ExpectedException
、ExternalResource
、TemporaryFolder
、TestName
、TestWatcher
、Timeout
、Verifier
,其中 ExpectedException
可以验证异常的详细信息,Timeout
可以指定测试方法的最大运行时间。
Timeout 示例:
public class ExampleTest {
@Rule
public Timeout timeout = new Timeout(1000); //使用Timeout这个 Rule,
@Test
public void testMethod1() throws Exception {
//your tests
}
@Test
public void testMethod2() throws Exception {
//your tests2
}
//other test methods
}
如上述代码所示,每个 testMethod
的运行时间都不会长于 1 秒钟。
自定义Rule示例: CustomRule
class CustomRule: TestRule {
lateinit var calculator: Calculator
// 用于记录运行次数
var count = 0
var count1 = 0
override fun apply(base: Statement?, description: Description?): Statement= object: Statement(){
fun before(){
// Add something do before
calculator = Calculator()
count += 1
println("before test with $count")
}
fun after(){
// Add something do after
count1 += 1
println("after test with $count1")
}
override fun evaluate() {
before()
base?.evaluate()
after()
}
}
}
Test Class:
class CalculatorTest {
// 每个测试方法前都会运行
@Rule
@JvmField
val customRule = CustomRule()
// classRule 方式 测试类所有测试运行前后才会执行一次
companion object {
@ClassRule
@JvmField
val customRule = CustomRule()
}
// 验证异常
@Test(expected = IllegalArgumentException:: class)
fun testDivide(){
customRule.calculator.divide(2,0)
}
@Test
fun testPlus(){
val result1 = customRule.calculator.plus(1,1)
Assert.assertEquals(2,result1)
}
}
Output:
before test with 1
after test with 1
before test with 1
after test with 1
kotlin.test 库
Kotlin 语言还提供了一个 kotlin.test
库,它定义了一些全局函数,可以在编写测试代码不用导入 org.junit.Assert
,还可以使用高阶函数作为验证语句的参数。
kotlin.test
库提供一些全局函数,如 assertEquals
、 expect
,更多详细内容请看 Package kotlin.test。
@Test
fun testPlus(){
val result1 = customRule.calculator.plus(1,1)
expect(6,{customRule.calculator.plus(1,5)})
}
Mock(模拟)的概念
在实际开发中,软件中是充满依赖关系的,我们会基于Dao(数据访问类)写service类,而又基于service类写操作类。
与JUnit的区别
在传统的JUnit单元测试中,我们没有消除对对象的依赖。
如存在A对象方法依赖B对象方法,在测试A对象的时候,我们需要构造出B对象,这样子增加了测试的难度,或者使得我们对某些类的测试无法实现。这与单元测试的思路相违背。
而Mock这种测试可以让你无视代码的依赖关系去测试代码的有效性。
核心思想就是如果代码按设计正常工作,并且依赖关系也正常,那么他们应该会同时工作正常。
Mockito mocking 框架的使用
Junit 4 测试框架可以验证有直接返回值的方法,Mocking 框架可以对 void
方法做测试。
void
方法的输出结果其实是调用了另外一个方法,所以需要验证该方法是否有被调用,调用时参数是否正确。Mocking 框架可以验证方法的调用.
目前流行的 Mocking 框架有 Mockito、JMockit、EasyMock、PowerMock 等。
选择Mockito 框架的原因是:
(1)Mockito 是 Java 中最流行的 mocking 框架;
(2)Google 的 Google Sample 下的开源库中使用也是 Mockito 框架。下面介绍 Mockito 框架一些概念和用法,以及 Kotlin 中 mockito-kotlin 库的使用。
Mockito
首先,需要再 build.gradle
中添加依赖:
testImplementation 'org.mockito:mockito-core:2.13.0'
// 如果需要 mock final 类或方法的话,还要引入 mockito-inline 依赖
testImplementation 'org.mockito:mockito-inline:2.13.0'
然后Mockito框架的示例如下:
class MockTest {
val mockList: MutableList<String> = Mockito.mock(mutableListOf<String>()::class.java)
@Test
fun listAdd(){
mockList.add("one")
mockList.add("two") // 添加元素不同无法通过测试
verify(mockList).add("one") // 只检查 .add("one") 是否执行成功
}
}
mock 和 spy
创建 mock
对象是 Mockito 框架生效的基础,有两种方式 mock
和 spy
。
mock
对象的属性和方法都是默认的,例如返回 null
、默认原始数据类型值(0 对于
int
/ Integer
)或者空的集合,简单来说只有类的空壳子——假执行。
而 spy
对象的方法是真实的方法——真执行,不过会额外记录方法调用信息,所以也可以验证方法调用。
@Test
fun listAdd(){
mockList.add("one")
mockList.add("two") // 添加元素不同无法通过测试
spyList.add("one")
verify(mockList).add("one") // 只检查 .add("one") 是否执行成功
println("this is mockList $mockList")
println("this is spyList $spyList")
// this is mockList Mock for ArrayList, hashCode: 1304117943
// this is spyList [one]
}
Mockito 还提供了 @Mock
等注解来简化创建 mock 对象的工作
class CalculatorTest {
@Mock
lateinit var calculator: Calculator
@Spy
lateinit var dataBase: Database
@Spy
var record = Record("Calculator")
@Before
fun setup() {
// 必须要调用这行代码初始化 Mock
MockitoAnnotations.initMocks(this)
}
}
除了显式地调用 MockitoAnnotations.initMocks(this)
外,还可以使用 MockitoJUnitRunner
或者 MockitoRule
。使用方式如下:
@RunWith(MockitoJUnitRunner.StrictStubs::class)
class CalculatorTest {
@Mock
lateinit var calculator: Calculator
}
// or
class CalculatorTest {
@Rule @JvmField
val mockitoRule = MockitoJUnit.rule().strictness(Strictness.STRICT_STUBS)
@Mock
lateinit var calculator: Calculator
}
验证方法调用
Mockito 中可以很方便地验证 mock
对象或 spy
对象的方法调用,通过 verify
方法即可:
验证执行次数,参数也必须一致
class MockTest {
val mockList: MutableList<String> = mock(mutableListOf<String>()::class.java)
@Test
fun listAdd(){
mockList.add("once")
mockList.add("twice")
mockList.add("twice")
mockList.add("three times")
mockList.add("three times")
mockList.add("three times")
// 检查了 3 次 .add("three times)
// times(0) = never(),证明从来没发生
// atLeastOnce() -> 至少一次
// atLeast(x) -> 至少 x 次
// atMost(x) -> 至少 x 次
verify(mockList, times(0)).add(" times")
}
}
stubbing
指定方法的实现
Stub就是把需要测试的数据塞进对象中,使用基本为:
Mockito .when ( ... ) .thenReturn ( ... ) ;
使用 Stub
时,我们只关注于方法调用和返回结果。
class StubTest {
val mockedList = mock(mutableListOf<String>().javaClass)
@Test
fun subTest(){
`when`(mockedList[0]).thenReturn("first").thenReturn("second").thenThrow(NullPointerException())
`when`(mockedList[1]).thenThrow(RuntimeException())
`when`(mockedList.set(anyInt(), anyString())).thenAnswer { invocation ->
val args = invocation.arguments
println("set index ${args[0]} to ${args[1]}")
args[1]
}
doThrow(RuntimeException()).`when`(mockedList).clear()
// 两种写法
`when`(mockedList.clear()).thenThrow(RuntimeException())
doReturn("third").`when`(mockedList)[2]
println(mockedList[0]) // first
println(mockedList[0]) // second
println(mockedList.set(0,"first"))
// set index 0 to first first
// first
println(mockedList[2]) // third
println(mockedList.clear()) // java.lang.RuntimeException
}
}