Bootstrap

Kotlin学习之旅(D10)- Unit Test with Kotlin

Kotlin学习之旅第十天

今天的主题是 - - Unit Test with Kotlin

前言

Kotlin学习之旅(D1)-学习计划&基本语法

Kotlin学习之旅(D2)-基本语法

Kotlin学习之旅(D3)-类与继承

Kotlin学习之旅(D4)-函数与Lambda表达式

Kotlin学习之旅(D5)-高级语法

Kotlin学习之旅(D6)-Kotlin Idioms part 1

Kotlin学习之旅(D7)-Kotlin Idioms part 2

Kotlin学习之旅(D8)-From Java to Kotlin

Kotlin学习之旅(D9)-Android Extensions

添加JUnit依赖

Kotlin里面的Unit Test 和 java里面大同小异,因此我们直接上手写一个测试就能掌握了。

说到Unit Test,就不得不提一下JUnit了,这是一个第三方库,用途就是跑测试用例。因此天才第一步,我们需要在build.gradle里面添加依赖

dependencies {
    ...
     testImplementation "junit:junit:4.12"
}

记得依赖方式是testImplementation,这样在打正式包的时候这个依赖被不会编译,可以减少APK的体积

实际上,在我们新建一个Android项目的时候,Android Studio会自动帮我们生成三个文件夹,分别是

  • code
  • test
  • androidTest

test 和 androidTest 文件夹就是我们编写Unit Test的地方

Snip20181018_3.png

Unit Test 例子

让我们在 test 文件夹下创建一个名为MyTest的文件:

class MyTest {

    @Test
    fun proof_of_concept() {
        assertEquals(4 , 2 + 2)
    }
}

这个时候看到左边有个绿色的箭头,点一下就能运行Unit Test

运行结果如下:

Snip20181018_3.png

可以看到我们写的这个Unit Test通过了,这就是一个最简单的例子,那么代码里面写的每一句是什么意思呢?

  • @Test - 这个注解表示接下来的方法是一个Unit Test 方法,通过上面的import信息也能看出来,这个注解是属于JUnit的
  • assertEquals(4, 2+2) - 这行代码表示 判断 4 是否等于 2+2,assertEquals方法同样也是JUnit里面的方法

所以这段代码的执行结果很明显是相等的,因此测试通过。

除了assertEquals,JUnit还提供了其他许多的判断方法

Snip20181018_3.png

关于JUnit的一切详情都可以从这里找到:JUnit 5 User Guide

另外推荐一个学习JUnit的网站:极客学院JUnit测试框架

Unit Test in Android

经过简单的介绍,我们已经知道了什么是Unit Test,如果还不太清楚的话,可以看一下以下几篇文章:

首先是关于TDD (Test-driven development)- 测试驱动开发

简单介绍就是:先写测试程序,然后编码实现功能,使得测试程序能够顺利运行的一种开发方法。

  1. 测试驱动开发-维基百科
  2. 浅谈测试驱动开发(TDD)
  3. Why Use Test-Driven Development?

然后是关于Unit Testing的:

  1. Unit testing - Wikipedia
  2. Unit Testing - Fundamentals

虽然在实际开发过程中,我们很少会使用TDD的方式进行项目开发,但是编写Unit Test还是有它的好处的,因此在Android开发里面,我们也需要掌握这项技能。 Let’s do it.

在Android里面进行测试,经常需要用到另外一个依赖库,叫做Mockito,那么下面我们就使用这个库来做个最简单的Unit Test

为什么要用Mockito

在JUnit中,我们可以验证有返回值方法是否正确,但是如果一个方法的返回值为void,也就是没有返回值,特别是这个方法是为了调用另外一个方法,那么这个时候就需要验证这个方法有没有被调用。我们就可以通过Mockito这个框架来验证。

怎么使用Mockito

在build.gradle文件中加入:

    testImplementation "org.mockito:mockito-core:2.18.3"

在我们在MyTest文件中加入以下代码:

    @Test
    fun mockito_test() {
        val mockedList = mock(mutableListOf<String>().javaClass)
        //using mock object
        mockedList.add("one")
        mockedList.clear()
        //verification
        verify(mockedList).add("one")
        verify(mockedList).clear()
    }

运行结果如下:

Snip20181018_6.png

这个例子是不是特别简单,它来自Mockito的官方,其实学习这些第三方框架,最好的教程就是他们的官方文档,因此这里我把几个不错的学习Mockito的资源列一下:

  1. Mockito 官网
  2. Mockito Github 地址
  3. Mocking-and-verifying
  4. Unit tests with Mockito - Tutorial

上面这个例子首先通过mock()创建List类,然后在mockedList中添加元素one,最后通过verify来验证mockedList有没有成功调用List里面的对应方法。我们一个个来看:

验证方法调用

通过verify 来验证方法是否被调用,例如:

mockedList.add("once")
mockedList.add("twice")
mockedList.add("twice")
mockedList.add("three times")
mockedList.add("three times")
mockedList.add("three times")

//following two verifications work exactly the same - times(1) is used by default
verify(mockedList).add("once");
verify(mockedList, times(1)).add("once")

//exact number of invocations verification
verify(mockedList, times(2)).add("twice")
verify(mockedList, times(3)).add("three times")

//verification using never(). never() is an alias to times(0)
verify(mockedList, never()).add("never happened")

//verification using atLeast()/atMost()
verify(mockedList, atLeastOnce()).add("three times")
verify(mockedList, atLeast(2)).add("three times")
verify(mockedList, atMost(5)).add("three times")

verify 方法中,我们可以指定方法调用的次数,通过times(1) 的形式,大家有兴趣的话可以跑一下上面的代码,看一下测试能不能通过。

stubbing指定方法的实现

除了空方法,我们有时候还需要检验方法的返回值或者实现。这个时候就需要用到when方法

看一个例子:

   //You can mock concrete classes, not just interfaces
        val mockedList = mock(LinkedList::class.java)

        //stubbing
        `when`(mockedList.get(0)).thenReturn("first")
        `when`(mockedList.get(1)).thenThrow(RuntimeException())

        //following prints "first"
        System.out.println(mockedList.get(0))

        //following throws runtime exception
        System.out.println(mockedList.get(1))

        //following prints "null" because get(999) was not stubbed
        System.out.println(mockedList.get(999))

        //Although it is possible to verify a stubbed invocation, usually it's just redundant
        //If your code cares what get(0) returns, then something else breaks (often even before verify() gets executed).
        //If your code doesn't care what get(0) returns, then it should not be stubbed. Not convinced? See here.
        verify(mockedList).get(0)

这个例子的运行结果是:

Snip20181018_7.png

为什么会失败呢? 因为有这么一行代码

`when`(mockedList.get(1)).thenThrow(RuntimeException())

当mockedList.get(1)的时候,我们让他抛出异常,因此运行的结果就是我们写的RuntimeException。

所以when 方法是不是很有用呢,有点像if…else 和 switch…case 的体系,可以通过不同的参数来控制测试方法对应的结果。

除了方法的运行结果可以控制以外,我们还可以测试方法的参数

参数匹配

例子:

val mockedList = mock(LinkedList::class.java)
//stubbing using built-in anyInt() argument matcher
`when`(mockedList[anyInt()]).thenReturn("element")

//following prints "element"
System.out.println(mockedList[999])

//you can also verify using an argument matcher
verify(mockedList)[anyInt()]

这个例子的运行结果是pass,因为我们需要的参数是anyInt(),verify的时候参数也是anyInt()。

注意:当你使用参数匹配的时候,所有的参数都必须是可匹配的,什么意思呢?看例子:


   verify(mock).someMethod(anyInt(), anyString(), eq("third argument"));
   //above is correct - eq() is also an argument matcher

   verify(mock).someMethod(anyInt(), anyString(), "third argument");
   //above is incorrect - exception will be thrown because third argument is given without an argument matcher.

第一行代码是正确的,因为eq()也是一种匹配模式,但是第二行代码直接传入一个String就不是匹配模式,在这里会抛出异常。

最后,由于Mockito的功能十分强大,因此在这里我也只是简单介绍一些常见的用法,对Mockito有一个初步的认识,其他的在需要的时候再去官方文档学习就可以了。

Mockito官方文档:Mockito Docs

总结

除了这篇文章里面说到的JUnit,Mockito这两个框架,我们还有可能需要用到一个叫做Robolectric 的框架,这个框架是用来测试运行在JVM上的Android代码的,不过由于暂时我们还接触不到,所以这里就不讲了,在后面进行项目实战的时候如果有需要,我们再去学习~

到目前为止,使用Kotlin开发Android项目所需要的基本知识都已经学完了,那么接下来就是实战阶段!加油!

Day 10 - Learn Kotlin Trip, Completed.


;