top of page
Tìm kiếm
Ảnh của tác giảthanh pham

Demo Unit Test trong Android

Mặc dù bây giờ đã rất rõ ràng, kiểm thử là một phần rất quan trọng trong quá trình phát triển phần mềm. Theo một cách nào đó, các thử nghiệm là một cấu trúc xung quanh phần mềm, cung cấp sự tự tin cho việc thay đổi cấu trúc của nó (như việc phát triển tính năng, maintain) trong khi đảm bảo không ảnh hưởng gì đến logic cũ.


Với mục đích của bài viết này, mình đã tạo một bản demo đơn giản giúp chúng ta có cái nhìn tổng quát về cấu trúc cũng nhưng những yêu cầu cơ bản khi bắt đầu làm việc với UnitTest trên Android. Bản demo là màn hình đăng nhập. Sẽ hiển thị lỗi trong trường hợp tên người dùng hoặc mật khẩu trống, hoặc xác thực không chính xác, còn nếu đúng nó sẽ mở ra màn hình chính . Để làm cho nó đơn giản nhất có thể, mình quyết định chỉ sử dụng thư viện espresso và mockK.


1. Sự khác nhau giữa Local Unit Test và Instrumented Test

Chúng ta có thể để ý thấy là có 2 thư mục test trong ứng dụng android như vầy

Local Unit Test (Kiểm thử đơn vị) Thư mục src/androidTest dành cho các bài kiểm tra đơn vị liên quan đến thiết bị Android.


Instrumented Test (Kiểm thử thiết bị) Thư mục src/test dành cho thử nghiệm đơn vị thuần túy không liên quan đến khung Android. Bạn có thể chạy thử nghiệm ở đây mà không cần chạy trên thiết bị thực hoặc trên trình giả lập.


Sự khác biệt giữa Kiểm tra Đơn vị và Kiểm tra Thiết bị là gì?

Thông thường Local Unit Test được gọi là “bài kiểm thử cục bộ” hoặc “bài kiểm thử đơn vị cục bộ”. Lý do chính cho điều này dường như là để xác minh rằng logic nghiệp vụ đang hoạt động bình thường mà không cần thiết bị hoặc trình giả lập đi kèm.


Instrumented Test chạy trên thiết bị hoặc trình giả lập. Trong nền, ứng dụng của bạn sẽ được cài đặt và sau đó một ứng dụng thử nghiệm cũng sẽ được cài đặt, ứng dụng này sẽ kiểm soát ứng dụng của bạn, dùng bữa trưa và chạy thử nghiệm giao diện người dùng nếu cần. Các bài kiểm tra công cụ cũng có thể được sử dụng để kiểm tra UI và không chứa logic. Chúng đặc biệt hữu ích khi bạn cần kiểm tra mã có sự phụ thuộc vào ngữ cảnh.

Bạn có thể sử dụng cả hai cùng lúc. Sử dụng Instrumented Test để kiểm thử UI và sử dụng Local Unit Test để kiểm tra các logic. Các phương pháp để viết bài kiểm thử gần như giống nhau.

Bây giờ, hãy cùng taọ một ứng dụng mẫu. Mình sẽ tạo một ứng dụng đơn giản với mục đích duy nhất là đăng nhập. Mọi người có thể tham khảo qua source code tại đây.


2. Instrumented Test


Với ứng dụng demo chúng ta có 2 màn hình là Login và Main. Ở đây mình sẽ tập trung vào test UI của màn hình Login còn màn hình Main các bạn có thể xây dựng thêm giao diện mong muốn và thực hành dựa vào demo này của mình.

Nào giờ cùng nhau hình dung với giao diện này chúng ta có thể liệt kê ra một số test case cơ bản như nếu tên người dùng hoặc mật khẩu trống, màn hình sẽ hiển thị lỗi. Ngoài ra, chúng ta cần có các bài kiểm tra để đảm bảo rằng đối với xác thực không chính xác sẽ xuất hiện lỗi và đối với xác thực đúng, màn hình Main sẽ mở.


Đây là các test case mình sẽ sử dụng để test cho phần UI màn hình Login:

  • Login title có hiển thị

  • Text login tittle hiển thị chính xác

  • UsernameInput có hiển thị

  • Text hint của UsernameInput chính xác

  • PasswordInput có hiển thị

  • Text hint của PasswordInput chính xác

  • Button login có hiển thị

  • Text của Button login chính xác

  • Text “Username and Password cannot be blank or have spaces” hiển thị đúng khi username chứa khoảng trắng

  • Text “Username and Password cannot be blank or have spaces” hiển thị đúng khi password chứa khoảng trắng

  • Text “Login fail!” hiển thị khi đăng nhập thất bại

  • Di chuyển đến màn hình MainActivity khi đăng nhập thành công











LoginActivityTest

@RunWith(AndroidJUnit4::class)
class LoginActivityTest {
    @Rule
    @JvmField
    val rule = ActivityTestRule(LoginActivity::class.java)

    @Test
    fun shouldContainLoginTitle() {
        loginScreen {
            isDisplayedLoginTitle()
        }
    }

    @Test
    fun loginTitleShouldApplyCorrectText() {
        loginScreen {
            loginTitle {
                hasCorrectTitle()
            }
        }
    }

    @Test
    fun shouldContainUsernameInput() {
        loginScreen {
            isDisplayedUsernameInput()
        }
    }

    @Test
    fun usernameInputShouldApplyCorrectHint() {
        loginScreen {
            usernameEditor {
                hasCorrectHint()
            }
        }
    }

    @Test
    fun shouldContainPasswordInput() {
        loginScreen {
            isDisplayedPasswordInput()
        }
    }

    @Test
    fun passwordInputShouldApplyCorrectHint() {
        loginScreen {
            passwordEditor {
                hasCorrectHint()
            }
        }
    }

    @Test
    fun shouldContainLoginButton() {
        loginScreen {
            isDisplayedLoginButton()
        }
    }

    @Test
    fun loginButtonShouldApplyCorrectText() {
        loginScreen {
            loginButton {
                hasCorrectTitle()
            }
        }
    }

    @Test
    fun loginWithUsernameHaveBlankShouldDisplayError() {
        loginScreen {
            typeUsername("  ")
            typePassword("Pass@12")
        } submit {
            displayBlankUsernameOrPasswordError()
        }
    }

    @Test
    fun loginWithPasswordHaveBlankShouldDisplayError() {
        loginScreen {
            typeUsername("larn203")
            typePassword("  ")
        } submit {
            displayBlankUsernameOrPasswordError()
        }
    }

    @Test
    fun loginFailShouldDisplayError() {
        loginScreen {
            typeUsername("123456")
            typePassword("123456")
        } submit {
            displayLoginFail()
        }
    }

    @Test
    fun successfulLoginShouldOpenMainScreen() {
        loginScreen {
            typeUsername("larn203")
            typePassword("Pass@12")
        } submit {
            openMainActivity()
        }
    }
}

Wow cũng không khó như chúng ta nghĩ phải không. Để hiểu hơn về cách sử dụng Espresso các bạn có thể tham khảo thêm về bài viết này.


3. Local Unit Test


Bây giờ, mình sẽ kiểm tra đơn vị các lớp sau: ValidateUtils, LoginViewModel

Tạo một tệp java mới và đặt tên là ValidateUtilsTtest

Đầu tiên, chúng tôi sẽ kiểm tra lớp ValidateUtils. Lớp ValidateUtils có nhiệm vụ kiểm tra giá trị của username hoặc password có đúng cấu trúc mong muốn của nhà phát triển không. Nên các quy tắc kiểm tra cũng tuỳ thuộc vào từng ứng dụng. Ở đây mình đưa ra một số ví dụ tổng quan nhất nếu chưa đủ các bạn có thể thêm theo mong muốn nó cúng như một cách thực hành. Còn bây giờ chúng ta phải đưa ra tất cả các trường hợp đầu vào mà chúng tôi có thể nghĩ ra.


Test case cho phương thức isWhitespace

  • Đầu vào có khoảng trắng -> expect: return true

  • Đầu vào không có khoảng trắng -> expect: return false

Test case cho phương thức validateUsername và validatePassword

  • Đầu vào: rỗng -> Expect: return false

  • Đầu vào: trống -> Expect: return false

  • Đầu vào: có khoảng trắng -> Expect: return false

  • Đầu vào: có khoảng trắng ở đầu -> Expect: return false

  • Đầu vào: có khoảng trắng ở giữa -> Expect: return false

  • Đầu vào: có khoảng trắng ở cuối -> Expect: return false

  • Đầu vào: chính xác -> Expect: return true

ValidateUtilsTtest

class ValidateUtilsTest {
    @MockK
    private lateinit var validateUtils: ValidateUtils

    @Before
    fun setUp() {
        validateUtils = ValidateUtils()
    }

    @Test
    fun validateIsWhiteSpaceReturnTrue() {
        assertTrue(validateUtils.isWhitespace(" "))
    }

    @Test
    fun validateIsWhiteSpaceReturnFalse() {
        assertFalse(validateUtils.isWhitespace("123456"))
    }

    @Test
    fun validateUsernameNullReturnFalse() {
        assertFalse(validateUtils.validateUsername(null))
    }

    @Test
    fun validatorUserNameEmptyReturnFalse() {
        assertFalse(validateUtils.validateUsername(""))
    }

    @Test
    fun validatorUserNameBlankReturnFalse() {
        assertFalse(validateUtils.validateUsername("   "))
    }

    @Test
    fun validatorUserNameBlankAtStartReturnFalse() {
        assertFalse(validateUtils.validateUsername(" 123456"))
    }

    @Test
    fun validatorUserNameBlankAtBetweenReturnFalse() {
        assertFalse(validateUtils.validateUsername("123 456"))
    }

    @Test
    fun validatorUserNameBlankAtEndReturnFalse() {
        assertFalse(validateUtils.validateUsername("123456 "))
    }

    @Test
    fun validateUsernameCorrect() {
        assertTrue(validateUtils.validateUsername("123456"))
    }

    @Test
    fun validatePasswordNullReturnFalse() {
        assertFalse(validateUtils.validatePassword(null))
    }

    @Test
    fun validatorPasswordEmptyReturnFalse() {
        assertFalse(validateUtils.validatePassword(""))
    }

    @Test
    fun validatorPasswordBlankReturnFalse() {
        assertFalse(validateUtils.validatePassword("   "))
    }

    @Test
    fun validatorPasswordBlankAtStartReturnFalse() {
        assertFalse(validateUtils.validatePassword(" 123456"))
    }

    @Test
    fun validatorPasswordBlankAtBetweenReturnFalse() {
        assertFalse(validateUtils.validatePassword("123 456"))
    }

    @Test
    fun validatorPasswordBlankAtEndReturnFalse() {
        assertFalse(validateUtils.validatePassword("123456 "))
    }

    @Test
    fun validatorPasswordLessThreeCharacterReturnFalse() {
        assertFalse(validateUtils.validatePassword("12"))
    }

    @Test
    fun validatePasswordCorrect() {
        assertTrue(validateUtils.validatePassword("123456"))
    }
}

Mặc dù hầu hết các mã đều có thể tự giải thích, nhưng đây là một số điều bạn có thể không biết :

  • @Test: Là một chú thích được cung cấp bởi JUnit Framework để đánh dấu một phương pháp là một trường hợp thử nghiệm. Như bạn có thể thấy ở đây, mỗi phương pháp là một trường hợp thử nghiệm kiểm tra trường đầu vào để tìm đầu vào khả thi. Điều này hướng dẫn trình biên dịch coi phương pháp như một trường hợp thử nghiệm trong bộ thử nghiệm.

  • assertTrue(): Là một phương thức được cung cấp bởi JUnit Framework để xác nhận (buộc) giá trị bên trong dấu ngoặc đơn của nó là TRUE. Nếu giá trị bên trong dấu ngoặc đơn đánh giá là sai, trường hợp kiểm tra không thành công.

  • assertFalse(): Giống như phương thức khẳng định, ngoại trừ việc nó khẳng định đối số bên trong dấu ngoặc đơn là sai thay vì đúng. Nếu tham số truyền vào là true, trường hợp thử nghiệm không thành công.

Ở đây mình có sử dụng thư viện MockK, cung cấp hỗ trợ cho các cấu trúc và tính năng của ngôn ngữ Kotlin. MockK xây dựng proxy cho các lớp mock. Điều này gây ra một số suy giảm hiệu suất, nhưng những lợi ích tổng thể mà MockK mang lại cho chúng ta là xứng đáng. Việc mock các object cho phép bạn test trên các đối tượng độc lập. Bất kỳ sự phụ thuộc vào object nào khi test đều có thể được mock để cung cấp các điều kiện cố định, do đó, đảm bảo các test luôn ổn định và rõ ràng.


Mockito là một framework phổ biến được dùng bởi các Java developer và vô cùng mạnh mẽ. Nhưng nó lại có một vài khó chịu khi được áp dụng cho Kotlin. MockK, được thiết kế đặc biệt cho Kotlin, sẽ mang lại cho chúng ta một trải nghiệm thoải mái hơn nhiều. Hãy xem chúng ta sử dụng mockK như thế nào trong việc kiểm thử LoginViewModel.


Test case cho LoginViewModel:

  • Validate username return false -> Expect: checkValidate return false

  • Validate password return false -> Expect: checkValidate return true

  • Validate username và password return true -> Expect: checkValidate return true

  • LoginFail

  • LoginSuccess

LoginViewModelTest

class LoginViewModelTest : BaseTestViewModel<LoginViewModel>() {
    @Rule
    @JvmField
    val instantTaskExecutorRule: 
        TestRule = InstantTaskExecutorRule()

    @MockK
    private lateinit var loginRepository: LoginRepository

    @MockK
    private lateinit var validateUtils: ValidateUtils

    private val loginTest = LoginResponse(1, "larn203", "Pass@12")

    override fun setUp() {
        super.setUp()
        viewModel = 
        spyk(LoginViewModel(loginRepository, validateUtils))
    }

    @Test
    fun checkValidateUserNameFail() {
        //Given
        every { validateUtils.validateUsername(any()) } 
            returns false
        every { validateUtils.validatePassword(any()) } 
            returns true

        //Then
        assertFalse(viewModel.checkValidate())
    }

    @Test
    fun checkValidatePasswordFail() {
        //Given
        every { validateUtils.validateUsername(any()) } 
            returns true
        every { validateUtils.validatePassword(any()) } 
            returns false

        //Then
        assertFalse(viewModel.checkValidate())
    }

    @Test
    fun checkValidateSuccess() {
        //Given
        every { validateUtils.validateUsername(any()) } 
            returns true
        every { validateUtils.validatePassword(any()) } 
            returns true

        //Then
        assertTrue(viewModel.checkValidate())
    }

    @Test
    fun loginFail() {
        //Given
        every { viewModel.checkValidate() } returns true

        every { loginRepository.userLogin(any()) } answers {
            Single.error(Throwable("error"))
        }

        //When
        viewModel.onLogin()

        //Then
        verify(exactly = 1) {
            viewModel.checkValidate()
            viewModel.onLoadFail()
        }
    }

    @Test
    fun loginSuccess() {
        //Given
        every { viewModel.checkValidate() } returns true

        every { loginRepository.userLogin(any()) } answers {
            Single.just(loginTest)
        }

        //When
        viewModel.onLogin()

        //Then
        verify(exactly = 1) {
            viewModel.checkValidate()
            viewModel.onLoadSuccess(any())
        }
    }
}

Ở đây bạn gặp các chú thích mới:

  • @MockK: Như đã giải thích ở trên, nó tạo ra một đối tượng giả được lớp cần kiểm tra để hoạt động bình thường. Có 2 cách tạo đối tượng Mock là sử dụng anotation @MockK hoặc sử dụng phương thức tĩnh mockk().

  • @Before: Dùng Anotation này cho function mà chúng ta muốn nó chạy trước mọi function test khác. Thường thì chúng ta sẽ dùng cho function init. Như ở ví dụ trên mình đã gọi. Ở đây chúng tôi khởi tạo viewModel

  • @After: Dùng Anotatio này cho function mà chúng ta muốn nó chạy cuối cùng sau mọi function khác.

  • @Test: Anotation dùng cho function có nhiệm vụ test. Chúng ta sẽ viết test code trong funcction này.

  • @SpyK: được dùng để tạo một đối tượng spy. Đối tượng spy là đối tượng bán ảo, hay nói cách khác nó vừa là đối tượng thực, vừa là đối tượng ảo. Vừa là đối tượng thực vì nó hoàn toàn có thể thực hiện các method của một đối tượng thực một cách chính xác, không cần stub trước giá trị để trả về như đối tượng mock. Vừa là đối tượng ảo vì nó có thể thực hiện các câu lệnh của một đối tượng mock. Có 2 cách tạo đối tượng Spy là sử dụng anotation @SpyK hoặc sử dụng phương thức tĩnh spyk(). Trong ví dụ, mình đã sử dụng phương pháp spyk để tạo một đối tượng gián điệp.


Làm thế nào mock một Exception với every?

Khá đơn giản, chúng ta có thể ném một ngoại lệ bên trong every{} và dùng verify{} để xác minh onLoadFail() đã được gọi hay chưa như đoạn code dưới đây:

@Test
fun loginFail() {
    //Given
    every { viewModel.checkValidate() } returns true

    every { loginRepository.userLogin(any()) } answers {
        Single.error(Throwable("error"))
    }

    //When
    viewModel.onLogin()

    //Then
    verify(exactly = 1) {
        viewModel.checkValidate()
        viewModel.onLoadFail()
    }
}

Để kiểm thử luồng Login thành công chúng ta phải đảm bảo ứng dụng phải gọi được đến server và từ server phải trả về kết quả như mong muốn. Trong test case này chúng ta sẽ:

  • Mô phỏng việc gọi đến Server

  • Mô phỏng phản hồi với một response

  • Xác minh onLoadSuccess() được gọi

@Test
fun loginSuccess() {
    //Given
    every { viewModel.checkValidate() } returns true

    every { loginRepository.userLogin(any()) } answers {
        Single.just(loginTest)
    }

    //When
    viewModel.onLogin()

    //Then
    verify(exactly = 1) {
        viewModel.checkValidate()
        viewModel.onLoadSuccess(any())
    }
}



4. Tổng kết


Tổng kết đây là các bước để tạo một bài kiểm thử đơn vị (nó có thể phức tạp hơn, nhưng đối với người mới bắt đầu, mình nghĩ điều này là đủ):

  1. Hãy nghĩ về tất cả các trường hợp thử nghiệm có thể xảy ra.

  2. Tạo một phương thức cho mỗi trường hợp thử nghiệm và chú thích nó bằng @Test

  3. Tạo phương thức @Before và khởi tạo thư viện MockK

  4. Viết các trường hợp thử nghiệm của bạn bằng các phương pháp như when, is, assertThat / False / True, v.v.

  5. Luôn luôn kết thúc các phương thức mock tại @After

334 lượt xem0 bình luận

Bài đăng gần đây

Xem tất cả

Comentarios


bottom of page