为什么要使用单元测试
使用单元测试我们可以很容易的发现代码的缺陷同时在你重构代码的时候可以很方便的帮你验证重构是否成功。在实际的开发过程中我经常会遇到面对前辈留下的一大坨过时的代码,维护起来很吃力,想重构又不敢,因为你刚接手这个业务肯定不熟悉,一些复杂的业务还设计到不同模块之间的交叉调用(就算是原作者也不敢拍胸脯保证的),万一改出问题来,造成生产事故那肯定是吃不了兜着走。如果业务模块配有完善的单元测,那这个问题就很好解决了,放手大胆的去重构,重构完成后运行一下单元测试就ok了,所以写单元测试是为自己也为别人。
基础知识
在Android中单元测试有一下几种类型:
- Local tests
直接在你本地电脑JVM环境下运行,运行过程中不依赖Android framework或者可以用mocking framework解除依赖,运行速度快。
-
Instrumented tests
运行在真机或模拟器,运行过程中需要访问真机(或模拟器)数据,这些数据不能或者不容易被mocking framework模拟,例如Context等。
配置环境
在编写我们的测试用例之前我们还需要配置下我们的测试环境,我们需要将*JUnit4 framework*
添加到我们的工程,在项目的build.gradle
中添加依赖 testImplementation 'junit:junit:4.12'
。
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'com.android.support:appcompat-v7:28.0.0'
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
//JUnit4 framework
testImplementation 'junit:junit:4.12'
}
在Android Studio 中写单元测试是很方便的,标准的android工程已经帮我们把目录结构规划好,我们要做的只需要在指定的目录写我们的测试用例就可以了。如下图所示
[图片上传失败...(image-c769c2-1539222083949)]
- test
我们单元测试相关的代码就应该写在这里,当我们调用gradle命令*./gradlew test*
的时候该目录下的所有的单元测试用例会打包运行并生成测试报告存放在../build/reports/
目录下。*test*
目录所对应的路径为*project_name/src/test/java/*
默认情况下这个目录是存在的,如果不存在我们可以手动创建就可以了。
- androidTest
这个目录对应的是Instrumented tests,也就是说Instrumented tests测试用例相关的代码应该写在这里。
在编写测试用例的时候需要注意,我们应该保证测试用例的目录结构和源码的目录结构保持一致,当然这不是一个强制要求,不过这样做可以让你的测试用例结构清晰,方便后期的维护和查阅。
编写单元测试用例
[图片上传失败...(image-8b8aa6-1539222083949)]
为了方便说明问题这里我创建了一个Demo模拟一个很常见的功能“登陆”,界面如图很简单,两个输入框分别用来输入用户名和密码,一个登陆按钮用于执行登陆验证逻辑,代码已上传github。
- 用户名
允许输入字母,如果输入其他非法字符会提示“User name is not valid.”
- 密码
只允许输入数字,没有做合法校验。
- 登陆成功
输入用户名“admin”,密码"123456"会跳转到登陆成功页面,其他输入会显示登陆失败。
核心业务逻辑代码如下:
/**
*
* @author 王强 on 2018/10/10 249346528@qq.com
*/
class MainModel {
private val charRegex: Regex = Regex("[a-zA-Z]")
/**
* 合法校验,只允许输入小写字母
* @name 用户名
* @return true 验证通过
*/
private fun checkUserName(name: String): Boolean = name.toCharArray().map {
it.toString()
}.filter {
it.matches(charRegex)
}.toList().isEmpty()
/**
* 模拟登陆,
* 用户名:admin
* 密码:123456
*/
fun doLogin(name: String, psw: String): LoginResult {
return if (checkUserName(name)) {
if (name == "admin" && psw == "123456") {
LoginResult(true, "Success")
} else {
LoginResult(false, "Fail")
}
} else {
return LoginResult(false, "User name is not valid.")
}
}
}
MainModel
类里面实现了我们登陆相关的所有业务逻辑,并对外暴露了doLogin(...)
方法,下面我们为“登陆”逻辑编写我们的测试用例代码。单元测试的主要工作就是针对我们要测试的业务逻辑去设计测试用例,测试用例设计的好坏直接影响我们单元测试的质量,概括来说就是测试用例要覆盖所有的业务场景和边界。这里我们主要从以下几个方面去设计我们的测试用例:
- 合法校验
检测用户名合法校验功能的实现是否符合预期,这里我们需要设计N组合法数据和N组非法数据。
- 登陆结果
检测登陆功能的实现是否符合预期,这里我们需要设计N组非法数据和一组合法数据(因为只有admin 123456能登陆,所以一组就行了,正常情况下应该是N组的)。
当然在生产环境下对登陆模块的测试要比这个复杂的多,这里我们只是为了说明问题,不对其他情况进行过多的考虑。
设计完成的测试用例如下:
/**
*
* @author 王强 on 2018/10/10 249346528@qq.com
*/
class MainModelTest {
private lateinit var mainModel: MainModel
//用于验证用户名的合法校验
private lateinit var errorCheckAccounts: List<String>
//用于验证非法登陆账户
private lateinit var errorLoginAccounts: List<String>
//用于验证合法登陆账户
private lateinit var rightLoginAccounts: List<String>
@Before
fun setUp() {
errorCheckAccounts = listOf(
"!@#", "123",
"123", "123",
"admin2", "admin",
"用户", "123456"
)
errorLoginAccounts = listOf(
"123", "123",
"admin", "1234",
"用户", "123456")
rightLoginAccounts = listOf(
"admin", "123456"
)
mainModel = MainModel()
}
@Test
fun doLoginTest() {
for (i in errorCheckAccounts.indices step 2) {
mainModel.doLogin(errorCheckAccounts[i], errorCheckAccounts[i + 1]).apply {
Assert.assertFalse(this.state)
}
}
for (i in errorLoginAccounts.indices step 2) {
mainModel.doLogin(errorLoginAccounts[i], errorLoginAccounts[i + 1]).apply {
Assert.assertFalse(this.state)
}
}
for (i in rightLoginAccounts.indices step 2) {
mainModel.doLogin(rightLoginAccounts[i], rightLoginAccounts[i + 1]).apply {
Assert.assertTrue(this.state)
}
}
}
}
这里我们用到了@Test 和 @Before注解,其实还有@After @BeforeClass @AfterClass @Ignore等只不过这里我们没有用到,这里我们来解释下这些注解的作用
@Test 目标测试方法,我们的测试用例方法都应该使用该注解标注
@Before 初始化方法,在调用每一个目标测试方法前都要执行一次,在这里我们可以进行资源初始化等操作
@After 释放资源,在每一个目标测试方法执行完成后调用
@BeforeClass 针对所有测试,只执行一次,且必须为static void
@AfterClass 针对所有测试,只执行一次,且必须为static void
@Ignore:忽略的测试方法,这个不常用
它们的执行顺序是 @BeforeClass -> @Before -> @Test --> @After --> @AfterClass
运行测试用例
我们可以通过以下几种途径运行我们的测试用例:
- GUI(图形界面)
如图我们直接在我们要运行的测试方法上鼠标右键选择“Run doLoginTest()
”就可以了,运行结果如下:
- Command line(命令行)
直接在命令行中输入./gradlew test
就可以了,执行结果在项目工程的../build/reports/
目录下,结果如图:
参考:
[1] https://developer.android.com/training/testing/unit-testing/local-unit-tests
[2] https://junit.org/junit4/