Android单元测试-Kotlin

单元测试基本概念 及 动机

单元测试基本步骤

  1. 初始化——准备一些测试前提条件。例如新建需要测试的类的实例
  2. 调用被测试的方法
  3. 验证结果——测试结果是否与预期一致
  4. 释放资源或删除文件(optional)

单元测试重点

单元测试的测试重点是测试类的 public 方法,对于其中的 private 方法的具体实现不关心。

单元测试方法

  1. 对于直接返回结果方法:可以用 Junit4 测试框架的 assert 语句
  2. 对于 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 中自带的 RuleErrorCollectorExpectedExceptionExternalResourceTemporaryFolderTestNameTestWatcherTimeoutVerifier,其中 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 库提供一些全局函数,如 assertEqualsexpect ,更多详细内容请看 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 框架生效的基础,有两种方式 mockspy
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
   }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,607评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,047评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,496评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,405评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,400评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,479评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,883评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,535评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,743评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,544评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,612评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,309评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,881评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,891评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,136评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,783评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,316评论 2 342

推荐阅读更多精彩内容