[Android]单元测试和模块测试

时间:2024-05-21 08:33:35

在 Kotlin 开发中,单元测试和模块测试(有时也称为组件测试或服务测试)是两种关键的测试方法,它们帮助开发者确保代码的各个部分独立和整体上都按预期工作。

1.单元测试

单元测试是测试软件应用中最小单元(通常是方法或函数)的过程。它的目标是验证这些单元在隔离的环境中的行为是否符合预期。

优点

  • 快速执行。
  • 帮助开发者识别和修复问题的精确位置。
  • 通过测试用例作为文档,可以增加代码的可读性和可维护性。

常用工具

  • JUnit: Java和Kotlin中最流行的测试框架。
  • MockK: 为Kotlin特别设计的模拟库,支持协程和扩展函数的模拟。
  • Spek: 使用Kotlin DSL的基于规格的测试框架。

示例 1: 简单的单元测试

假设我们有一个简单的 Calculator 类:

class Calculator {
    fun add(a: Int, b: Int): Int = a + b
    fun multiply(a: Int, b: Int): Int = a * b
}

为这个类编写单元测试:

import org.junit.jupiter.api.Test
import org.junit.jupiter.api.Assertions.assertEquals

class CalculatorTest {
    private val calculator = Calculator()

    @Test
    fun `add should correctly add two numbers`() {
        assertEquals(5, calculator.add(2, 3), "Adding 2 and 3 should equal 5")
    }

    @Test
    fun `multiply should correctly multiply two numbers`() {
        assertEquals(6, calculator.multiply(2, 3), "Multiplying 2 by 3 should equal 6")
    }
}

示例 2: 使用 MockK 模拟依赖

假设我们有一个依赖外部服务的 UserService 类:

class UserService(private val userApi: UserApi) {
    fun getUserEmail(userId: Int): String? {
        val user = userApi.fetchUserById(userId)
        return user?.email
    }
}

data class User(val id: Int, val name: String, val email: String)
interface UserApi {
    fun fetchUserById(userId: Int): User?
}

为 UserService 编写单元测试,使用 MockK 模拟外部 API:

import io.mockk.every
import io.mockk.mockk
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.Assertions.assertEquals

class UserServiceTest {
    private val userApi = mockk<UserApi>()
    private val userService = UserService(userApi)

    @Test
    fun `getUserEmail returns correct email`() {
        val user = User(1, "John Doe", "john@example.com")
        every { userApi.fetchUserById(1) } returns user

        val email = userService.getUserEmail(1)

        assertEquals("john@example.com", email)
    }

    @Test
    fun `getUserEmail returns null if user not found`() {
        every { userApi.fetchUserById(any()) } returns null

        val email = userService.getUserEmail(99)

        assertEquals(null, email)
    }
}

2.模块测试

模块测试或组件测试通常涉及一个模块或多个紧密相关的模块(如一个类及其依赖)。这种测试可能涉及数据库、文件系统、网络或其他外部依赖的模拟。

优点

  • 验证模块间的集成是否正确。
  • 检查模块与外部系统的交互。
  • 找到可能在单元测试中被忽略的问题。

常用工具

  • JUnit 或 TestNG: 用于编写和执行测试。
  • MockK: 模拟外部依赖。
  • Testcontainers: 提供真实的外部服务测试环境,如使用Docker容器的数据库。

示例: 模块测试

假设我们有一个 OrderService 类,它依赖于 OrderRepository 和 PaymentService

class OrderService(private val orderRepository: OrderRepository, private val paymentService: PaymentService) {
    fun processOrder(orderId: Int, paymentDetails: PaymentDetails): Boolean {
        val order = orderRepository.findById(orderId) ?: return false
        return paymentService.processPayment(order, paymentDetails)
    }
}

interface OrderRepository {
    fun findById(orderId: Int): Order?
}

interface PaymentService {
    fun processPayment(order: Order, paymentDetails: PaymentDetails): Boolean
}

data class Order(val id: Int, val amount: Double)
data class PaymentDetails(val method: String, val amount: Double)

为 OrderService 编写模块测试:

import io.mockk.every
import io.mockk.mockk
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Assertions.assertFalse

class OrderServiceTest {
    private val orderRepository = mockk<OrderRepository>()
    private val paymentService = mockk<PaymentService>()
    private val orderService = OrderService(orderRepository, paymentService)

    @Test
    fun `processOrder returns true when payment is successful`() {
        val order = Order(1, 100.0)
        val paymentDetails = PaymentDetails("credit",100.0)
        every { orderRepository.findById(1) } returns order
        every { paymentService.processPayment(order, paymentDetails) } returns true

        val result = orderService.processOrder(1, paymentDetails)

        assertTrue(result, "Order processing should succeed when payment is successful")
    }

    @Test
    fun `processOrder returns false when order not found`() {
        every { orderRepository.findById(any()) } returns null

        val result = orderService.processOrder(99, PaymentDetails("credit", 50.0))

        assertFalse(result, "Order processing should fail when order is not found")
    }

    @Test
    fun `processOrder returns false when payment fails`() {
        val order = Order(1, 100.0)
        val paymentDetails = PaymentDetails("debit", 100.0)
        every { orderRepository.findById(1) } returns order
        every { paymentService.processPayment(order, paymentDetails) } returns false

        val result = orderService.processOrder(1, paymentDetails)

        assertFalse(result, "Order processing should fail when payment fails")
    }
}

3.断言方法

在 Kotlin 开发中,单元测试和模块测试通常使用测试框架,如 JUnit,Kotlin Test,或 Spek 等。这些框架提供了一系列的断言方法,这些方法是用来验证代码的行为是否符合预期的关键组件。我将重点介绍 JUnit 和 Kotlin Test 中常用的断言方法。

(1).JUnit 断言方法

JUnit 是 Java 和 Kotlin 中最常用的测试框架之一。JUnit 5 提供了一个 Assertions 类,其中包含多种静态方法用于断言测试结果。

基本断言
  • assertEquals(expected, actual, message): 验证两个值是否相等。
  • assertNotEquals(unexpected, actual, message): 验证两个值是否不等。
  • assertTrue(condition, message): 验证条件是否为真。
  • assertFalse(condition, message): 验证条件是否为假。
  • assertNull(value, message): 验证对象是否为 null
  • assertNotNull(value, message): 验证对象是否非 null
对象和类断言
  • assertSame(expected, actual, message): 验证两个引用是否指向同一个对象。
  • assertNotSame(unexpected, actual, message): 验证两个引用是否不指向同一个对象。
数组和集合断言
  • assertArrayEquals(expected, actual, message): 验证两个数组是否相等。
异常断言
  • assertThrows(exceptionType, executable, message): 验证预期的异常是否被抛出。
组合断言
  • assertAll(executables): 同时执行多个断言,只有当所有断言都通过时,测试才会通过。
示例
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Test

class AssertionsExample {
    @Test
    fun testAssertions() {
        assertEquals(4, 2 + 2, "Addition should work")
        assertTrue(5 > 2, "5 is greater than 2")
        assertAll("multiple",
            { assertNotNull("Hello", "Should not be null") },
            { assertThrows(ArithmeticException::class.java, { val result = 2 / 0 }, "Should throw ArithmeticException") }
        )
    }
}

(2).Kotlin Test 断言方法

Kotlin Test 是另一种专为 Kotlin 设计的测试库,它提供了一些额外的断言功能。

基本断言
  • shouldBe: 用于任何类型,比较是否相等。
  • shouldNotBe: 用于任何类型,比较是否不等。
  • shouldBeNull: 检查对象是否为 null
  • shouldNotBeNull: 检查对象是否非 null
  • shouldThrow: 检查是否抛出异常。
集合断言
  • shouldContain: 检查集合是否包含元素。
  • shouldNotContain: 检查集合是否不包含元素。
字符串断言
  • shouldStartWith: 检查字符串是否以特定前缀开始。
  • shouldEndWith: 检查字符串是否以特定后缀结束。
示例
import io.kotest.matchers.shouldBe
import io.kotest.matchers.nulls.shouldBeNull
import io.kotest.matchers.string.startWith
import io.kotest.core.spec.style.StringSpec

class KotlinTestExample : StringSpec({
    "addition works" {
        (2 + 2) shouldBe 4
    }
    "null checks" {
        val str: String? = null
        str.shouldBeNull()
    }
    "string should start with" {
        "hello world".shouldStartWith("hello")
    }
})