Android功能性测试实践入门

Android面试技术要点汇总
2021-05-17 14:29 · 阅读时长12分钟
小课

软件测试是评估和验证软件产品和应用程序是否达到预期效果的重要环节,通过测试能够避免一些bug,提升稳定性,验证软件功能是否完整,为用户提供更好的产品服务。

软件测试可分为:

  • 功能性测试
  • 非功能性测试
什么是功能性测试?

功能性测试是根据产品需求来验证软件系统是否正常的一种软件测试,它的目的是测试软件中每一个功能是否达到预期效果。主要通过输入参数,根据功能需求来验证输出是否正确,它是一种黑盒测试,因为在测试中不涉及到源代码。

为什么需要功能性测试?

功能性测试对于验证软件系统的功能和质量至关重要,QA团队根据产品的需求,通过功能性测试来验证软件功能是否工作正常。

功能性测试 vs 非功能性测试
  • 功能性测试用于验证功能和特性是否达到预期,而非功能性测试主要关注性能、可用性和稳定性等方面。
  • 功能性测试易于手动运行,而非功能性测试一般难以手动运行。
功能性测试的优点
  • 它能保证客户或用户的产品体验。
  • 它可以减少潜在的bug。
  • 它能确保所有的功能需要都正常可用,并且达到预期效果。
  • 它可以提升安全性和产品质量。
功能性测试的步骤
  1. 确定产品需要测试的功能需求。
  2. 创建符合需求规范的输入数据和预期的输出结果。
  3. 运行测试用例。
  4. 对比实际输出结果和预期输出结果。
功能性测试的类型
  • 单元测试,用于测试软件系统中每个独立的单元或组件,确保每个单元或组件按照预期执行。
  • 冒烟测试,用于测试软件系统是否稳定。
  • 系统测试,它是从完整软件系统的角度进行测试,主要测试软件系统是否符合需求。
  • 用户验收测试,在软件系统部署到生产环境之前进行测试,主要关注端到端的业务流程是否正常。
  • 回归测试,主要验证最近的改动是否影响到现有的功能,当有新功能添加或者bug修复提交时,回归测试是十分必要的。
  • 集成测试,软件系统一般分为多个端或模块,集成测试主要测试这些端或者模块集成之后是否有问题。
功能性测试技术
  • 正面测试,使用有效的输入数据作为输入,验证软件系统的输出是否和预期一致。
  • 负面测试,使用无效或者不正确的数据作为输入,验证软件系统对于这些输入数据的反应是否符合预期,确保软件系统不会因此崩溃或者异常,保证稳定性。
功能性测试的工具
  • JUnit
  • Mockito

JUnit是在Java中使用最广泛的测试框架,可以通过以下方式引入

testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
testImplementation 'com.google.truth:truth:1.1.3'

Truth是由Guava团队维护的一个代码库,Google内部的代码库也用它来进行测试。

val result = ValidatorUtils.validateRegistrationFields("", "123456", "123456")
assertThat(result).isFalse()

使用它可以让代码更加简洁,易于理解。下面写一个简单的单元测试来验证邮箱和密码是否合法。

在Android Studio项目app/src/test/java目录中创建ValidatorUtilsTest.kt

1class ValidatorUtilsTest {
2
3    @Test
4    fun `empty username returns false`() {
5        val result = ValidatorUtils.validateRegistrationFields("", "123456", "123456")
6        assertThat(result).isFalse()
7    }
8
9    @Test
10    fun `valid username and correctly repeated password returns true`() {
11        val result = ValidatorUtils.validateRegistrationFields("Eric", "123456", "123456")
12        assertThat(result).isTrue()
13    }
14
15    @Test
16    fun `username already exists returns false`() {
17        val result = ValidatorUtils.validateRegistrationFields("John", "123456", "123456")
18        assertThat(result).isFalse()
19    }
20
21    @Test
22    fun `empty password returns false`() {
23        val result = ValidatorUtils.validateRegistrationFields("John", "", "")
24        assertThat(result).isFalse()
25    }
26
27    @Test
28    fun `incorrectly repeated password returns false`() {
29        val result = ValidatorUtils.validateRegistrationFields("John", "123456", "abcdef")
30        assertThat(result).isFalse()
31    }
32
33    @Test
34    fun `less than two digits password returns false`() {
35        val result = ValidatorUtils.validateRegistrationFields("John", "abcde1", "abcde1")
36        assertThat(result).isFalse()
37    }
38}
除了在单元测试中,请不要使用‘xxxx’定义方法名
  • 在需要测试的方法上面加上注解@Test。
  • 根据需求为需要测试的方法创建输入数据。
  • assertThat方法用来确保测试返回的结果是true或者false。
  • 测试用例都通过后,开始编写实际的代码逻辑。
1object ValidatorUtils {
2
3    val existingUsernames = listOf("John", "david", "nick")
4
5    fun validateRegistrationFields(username: String, password: String, confirmedPassword: String): Boolean {
6        if (username.isEmpty() || password.isEmpty()) {
7            return false
8        }
9        if (username in existingUsernames) {
10            return false
11        }
12        if (password != confirmedPassword) {
13            return false
14        }
15        if (password.count { it.isDigit() } < 2) {
16            return false
17        }
18        return true
19    }
20}

Mockito是一个非常流行的开源软件测试框架,它极大的简化了对具有外部依赖的类的测试,它能够模拟依赖的类或接口。

使用Mockito的API

通过mock静态方法或@Mock注解来创建模拟对象。when(…​.).thenReturn(…​.) 链式调用可以根据指定的参数来返回不同的值。下面写一个针对firebase方法的测试用例。

首先在build.gradle中添加Mockito的依赖

implementation "org.mockito:mockito-core:3.5.13"
implementation "org.mockito:mockito-inline:3.5.13"

然后编写测试类

1@RunWith(MockitoJUnitRunner::class)
2class MainActivityViewModelTest {
3  
4    private var firebaseAuth = mock(FirebaseAuth::class.java)
5    
6    @Mock
7    private lateinit var taskMock: Task<AuthResult>
8    
9    private val viewModel: MainActivityViewModel by lazy {
10        MainActivityViewModel(
11            UserRepositoryImpl(
12                firebaseFirestore,
13                firebaseAuth)
14        )
15    }
16  
17    @Test
18    fun testUserLogin() {
19        val email = "[email protected]"
20        val password = "123456"
21
22        `when`(firebaseAuth.signInWithEmailAndPassword(email, password)).thenReturn(taskMock)
23        viewModel.userLogin(email, password)
24
25        verify(firebaseAuth).signInWithEmailAndPassword(email, password)
26        verify(taskMock).addOnCompleteListener(any(MyOnCompleteListener::class.java))
27    }
28}
29
30interface UserRepository {
31    fun login(email: String, password: String)
32}
33
34class UserRepositoryImpl @Inject constructor(
35   private val firebaseAuth: FirebaseAuth
36): UserRepository {
37    
38    override fun login(email: String, password: String) {
39        firebaseAuth.signInWithEmailAndPassword(email, password)
40            .addOnCompleteListener(object : MyOnCompleteListener(this) {
41                override fun onComplete(p0: Task<AuthResult>) {
42                    println("!@# onComplete called")
43                }
44            })
45    }
46}
47
48@HiltViewModel
49class MainActivityViewModel @Inject constructor(
50    private val userRepository: UserRepository
51) : BaseViewModel() {
52    
53    fun userLogin(email: String, password: String) {
54        userRepository.login(email, password)
55    }
56}
57
58open class MyOnCompleteListener(): OnCompleteListener<AuthResult> {
59    private lateinit var callBackListener: CallBackListener
60
61    constructor(
62        callBackListener: CallBackListener
63    ): this() {
64        this.callBackListener = callBackListener
65    }
66
67    override fun onComplete(p0: Task<AuthResult>) {
68        callBackListener.doSomething()
69    }
70}
移动软件测试单元测试unit test