Contents
Chào mừng bạn đến với Fx Studio. Chúng ta đã khám phá UnitTest hay Testing trong bài viết trước là gì rồi. Thì chủ đề lần này sẽ là việc thực hiện các UnitTest. Tuy nhiên, thay vì sử dụng các tools mặc định của Xcode, thì ta sẽ sử dụng 2 thư viện đình đám là Quick & Nimble. Nó là gì & hoạt động ra sao? … chúng ta cùng nhau tìm hiểu nhóe!
Tác giả bài viết là bạn Tâm Kun. Mọi người có thể theo dõi thêm các bài viết từ GitHub chính chủ của bạn nhóe!
Nếu mọi việc đã ổn rồi, thì …
Bắt đầu thôi!
Chuẩn bị
Về mặt kiến thức, bạn cần hiểu qua các thuật ngữ cơ bản đầu tiền của iOS Testing trước nhóe. Nếu bạn chưa biết nó là gì, bạn có thể tham khảo bài viết sau:
Về mặt công cụ, bạn chỉ cần có Xcode là xong nhóe. Bạn không cần quan tâm tới version Xcode, hay iOS … hầu như bạn sẽ sử dụng với các phiên bản mới nhất rồi.
Về mặt demo, Source code các ví dụ trong seri này các bạn có thể tham khảo ở đây. Trong đó có 3 file test chính:
- TutorialViewModelTest: Basic test.
- HomeViewModelTest: Test with Nimble/Quick.
- DetailViewModelTest: Test with OHHTTPStubs liên quan đến case server.
Quick & Nimble là gì?
Cài đặt thư viện
Chúng ta sẽ bắt đầu việc cài đặt thư viện trước nhóe. Vì bạn có thể vài đường Google để biết chúng nó là gì rồi. Và chúng ta sẽ không quan tâm quá nhiều về mặt lý thuyết dài dòng kia.
Để cài đặt Quick & Nimble, mình sử dụng CocoaPods. Cách này cũng khá phổ thông và nếu bạn chưa biết cách sử dụng CocoaPod thì có thể đọc vài viết này nhóe! (theo 2 cách)
Tiếp theo, bạn cập nhật lại PodFile để thêm các pod
cho Quick & Nimble. Xem hình sau nhóe!
Các bạn nhớ để Quick & Nimble ở trong target Tests nha. Cụ thể ở đây là “FinalProjectTests”. Nếu bỏ nhầm chổ khác, thì lúc import
nó ở file test sẽ không nhận đâu.
Quick
Khái niệm
Ta sẽ bắt đầu tìm hiểu khái niệm đầu tiên. Đó là Quick.
Quick là một testing framework, là nơi cung cấp những methods thuận tiện cho công việc viết test.
Nó sử dụng function spec()
để định nghĩa toàn bộ code test. Function spec()
hỗ trợ cho việc chia nhiều sections.
Ví dụ: Ở ViewModel sẽ có những hàm xử lí logic và hàm xử lí api, thì ta sẽ chia nó thành 2 section khác nhau để cho nó clear và những người khác lúc vào maintain test cũng có thể dễ dàng tìm được.
Một số cú pháp có trong Quick
Ta có 3 cú pháp quan trọng, đó là it, context & describle.
- it: được dùng để định nghĩa kết quả kì vọng cụ thể nhất.
Hiểu nôm na sử dụng it để test một case cụ thể nào đó trong một source code. Có bao nhiêu case thì có bấy nhiêu it.
enum Gender { case male case female } func identify(gender: Gender) -> String { switch gender { case .male { return "have bird" } case .female { return "dont have bird :("} } }
Như bạn thấy hàm này nó trả về kiểu string và có 2 case để nó trả về giá trị khác nhau. Nên khi test sử dụng 2 câu lệnh it để có thể “bao phủ” cả hàm trên.
- context: được đùng để định nghĩa các “specific context” của một tác vụ nào đó mà bạn phải test.
Các case khác nhau tạo ra một hàm xử lý, từ đó suy ra được nhiều “it” nó tạo ra một context.
Như ví dụ trên: 2 cái “it” tạo ra một context. Context này để giúp xác định giới tính của một người nào đó trong lớp học.
Tóm lại trong code, những case nào mà cùng liên quan đến một tác vụ, một chức năng nào đó thì gom lại thành 1 context.
- describle: được dùng để định nghĩa nhưng tác vụ lớn hoặc hành vi mà bạn phải test
Cũng tương tự như vậy, nhiều context sẽ tạo ra describle.
Cũng với ví dụ trên, trong một lớp học cần xác định giới tính (context) , xác định học lực (context), kiểm tra hành kiểm (context)… Nhiều cái thì chúng ta gom chúng vào một describle.
Vậy buộc phải dùng tất cả hả ? Trả lời : Không.
Vậy dùng toàn “it” được không? Trả lời: Được.Nhưng…
Ví dụ:
it("Test case rank bad") { expect(viewModel.rankStudent(point: 3)) == .bad expect(viewModel.rankStudent(point: 3)).to(equal(.bad)) } it("Test case gender male") { expect(viewModel.identify(gender: .male)) == "have bird" } it("Test case rank middle") { expect(viewModel.rankStudent(point: 6)) == .middle expect(viewModel.rankStudent(point: 6)).toNot(equal(.bad)) } it("Test case rank good") { expect(viewModel.rankStudent(point: 8.2)) == .good expect(viewModel.rankStudent(point: 8.2)).toNot(equal(.middle)) } it("Test case rank verygood") { expect(viewModel.rankStudent(point: 8.6)) == .verygood expect(viewModel.rankStudent(point: 8.2)).toNot(equal(.middle)) } it("Test case gender female") { expect(viewModel.identify(gender: .female)) == "dont have bird :(" } it("Test case rank error") { expect(viewModel.rankStudent(point: 11)) == .error }
context("Test rank") { it("Test case rank bad") { expect(viewModel.rankStudent(point: 3)) == .bad expect(viewModel.rankStudent(point: 3)).to(equal(.bad)) } it("Test case rank middle") { expect(viewModel.rankStudent(point: 6)) == .middle expect(viewModel.rankStudent(point: 6)).toNot(equal(.bad)) } it("Test case rank good") { expect(viewModel.rankStudent(point: 8.2)) == .good expect(viewModel.rankStudent(point: 8.2)).toNot(equal(.middle)) } it("Test case rank verygood") { expect(viewModel.rankStudent(point: 8.6)) == .verygood expect(viewModel.rankStudent(point: 8.2)).toNot(equal(.middle)) } it("Test case rank error") { expect(viewModel.rankStudent(point: 11)) == .error } } context("Test gender") { it("Test case gender male") { expect(viewModel.identify(gender: .male)) == "have bird" } it("Test case gender female") { expect(viewModel.identify(gender: .female)) == "dont have bird :(" } }
Tóm tắt
Qua 2 cách viết trên ta thấy được cách nào cũng mang lại kết quả tối ưu là test pass.
Nhưng khi người sau khi vào maintain hay lỡ có bị fail, thì dev trố mắt nhìn để tìm kiếm đến đến case nào bị lỗi, function nào bị fail và nhìn vào chả có cảm tình gì cả.
Theo bản thân mình nhận thấy, mô hình MVC, MVVM hay bất kì mô hình gì, thì chúng ta cũng chia code ra để quản lý và maintain dễ hơn mà thôi. Thì viết test case cũng vậy, viết sao cho người sau vào đọc nói “dễ chịu”, cấu trúc rõ ràng. Không phải test case function này rồi thích nhảy qua test case của function kia.
Tâm-Kun yêu cái đẹp!!!
- beforeEach: tương tự như setup vậy, chúng ta chuẩn bị dữ liệu test ở beforeEach.
- afterEach: context nó chạy xong thì nó sẽ vào afterEach để config dữ liệu lại theo như mong muốn.
Nimble
Khái niệm
Tiếp theo, ta sẽ tìm hiểu khái niệm Nimble nhóe!
Nimble cũng là một framework cung cấp rất nhiều các options để giúp thoả mãn được các “kì vọng” test.
Keyword “expect” trong Nimble rất quan trọng. Nó thay thế cho XCTAssertion của hàng chính hãng XCTest.
Expect là “kì vọng”.
Có nghĩa là chúng ta kì vọng trường hợp đó output ra như ta mong đợi. Thư viện Nimble hỗ trợ ta rất nhiều để có thể “expect” được những giá trị, kiểu dữ liệu hay so sánh 2 đối tượng nào đó ở nhiều trường hợp. Rất tiện lợi đúng không nào!
Chúng ta tiếp tục xem Nimble hỗ trợ cho việc test về những gì nhé!
Hỗ trợ kiểm thử trong quá trình đồng bộ
expect(1 + 1).to(equal(2)) // so sánh bằng expect(1.2).to(beCloseTo(1.1, within: 0.1)) // xấp xỉ trong giới hạn là bao nhiêu expect(3) > 2 // so sánh hơn, kém expect("seahorse").to(contain("sea")) // kiểm thử có chứa phần tử hay không? expect(["Atlantic", "Pacific"]).toNot(contain("Mississippi")) // kiểm thử không chứa phần tử hay không? expect(1 + 1).to(equal(3), description: "Make sure 1+1 = 2") // Bạn muốn add thêm thông tin đến khi test case đó bị sai thì thêm argument description.
Xem qua đoạn code ví dụ trên, mình đã liệt kê ra kha khá các function mà bạn có thể dùng hoặc sẽ sử dụng khá nhiều sau này nhóe. Xoay quanh 2 kiểu chính:
to
toNot
Còn nội dung ở trong như thế nào, thì bạn sẽ theo logic mà bạn muốn test nhóe!
Hỗ trợ kiểm thử trong quá trình bất đồng bộ
Nimble cung cấp ta 2 cách để nhận biết rằng code chúng ta đang chạy là bất đồng bộ. Đó là toEventually và waitUntil.
- toEventually: Có một điều mình rất thích ở Nimble nói chung và toEventually nói riêng là nó cho phép ta viết kì vọng test nhưng đang đọc tiếng anh.
“Expect value to eventually be this“.
Giá trị mong đợi cuối cùng là …
Có nghĩa là toEventually giúp bạn có thể dự đoán một thứ gì đó “trong tương lai”.
DispatchQueue.main.async { ocean.add("dolphins") ocean.add("whales") } expect(ocean).toEventually(contain("dolphins", "whales"))
Ví dụ trên có ý nghĩa như sau:
- Đối tượng ocean nó sẽ được đánh giá liên tục.
- Nếu nó đã từng chứa dolphins và whales, thì kì vọng sẽ pass.
- Ngược lại, nếu nó không chứa trong bất kì khoảng thời điểm nào bất chấp ocean được đánh giá liên tục, thì kì vọng của chúng ta sẽ fail.
Tiếp theo với,
-
waitUntil: nó là một function được cung cấp bởi Nimble và hỗ trợ cho quá trình test bất đồng bộ.
Phần này, mình chỉ nói sơ qua để mọi người hiểu một phần nào đó. Bài tiếp theo, test bất đồng bộ liên quan đến server sẽ được nói rõ hơn.
Nimble lo tất cả & chúng ta chỉ cần dùng mà thôi 😀
Các bạn muốn biết nhiều hơn về cách dùng thì vào đây xem nhé. Thôi lí thuyết rứa đủ rồi, qua làm cái ví dụ là hiểu liền.
Example
Giới thiệu qua ViewModel chúng ta cần test một chút. Nó sẽ lấy data từ api về và fill data vào cái tableView, nên sẽ có 2 tác vụ chính.
- Các hàm xử lí api.
- Các hàm xử lí logic, cung cấp dữ liệu đổ vào tableView.
Step 1 : Import
import Nimble import Quick @testable import FinalProject class HomeViewModelTest: QuickSpec { }
Đầu tiên, tất nhiên là phải import thư viện cần dùng. Chú ý tiếp theo, các bạn sẽ thấy được cú pháp @testable import, thì nó để làm gì?
@testable import
giúp chúng ta khai báo thêm, tức là khi dùng nó ta sẽ add được file khác target vào target test, để phục vụ cho việc test.
Tiếp theo, đảm bảo class test phải kế thừa “QuickSpec”. Việc kết thừa QuickSpec, chúng ta có thể override function spec()
, thì công dụng của nó thì như ta đã nói phía trên.
Cuối cùng là khởi tạo một instance để dùng và bỏ ở trong hàm spec()
.
var viewModel: HomeViewModel!
Step 2: describle
Ta sử dụng describle” để mô tả mục đích lớn nhất của ta trong việc test ViewModel này là gì?
Ở đây, ta đang test các chức năng của màn hình Home nên ta có thể viết như sau:
describe("Test funcs homeScreen") {...}
Và nếu file hay class của bạn muốn test có quá nhiều logic trong đó, thì hãy cứ thêm các describle vào nhóe. Mục tiêu tối thượng là: người khác đọc hiểu một cách nhanh chóng!
Step 3: context
Trong describle thì sẽ có nhiều context.
Như chúng ta đã phân tích, thì có 2 tác vụ chính thì ta sẽ có 2 “context“.
context("Test some funcs related to tableView") { ... } context("Test func related to api") { // Phần này chúng ta phân tích kĩ hơn ở bài sau }
Tạm thời, bạn sẽ sử dụng 2 context như trên để test TableView. Chúng ta sẽ triển khai dần dần thêm các cấu hình phục vụ việc test nhóe.
Step 4: beforeEach
Trong cái context đầu tiên, chúng ta sẽ bắt đầu test các function liên quan đến TableView. Sử dụng beforeEach, để chuẩn bị dữ liệu test.
beforeEach { viewModel = HomeViewModel() viewModel.musics = DummyData.dummyMusics }
Hiện tại, chúng ta chưa có dữ liệu, nên sẽ fake để test. Ta sẽ viết extension để tạo ra một DummyData.
extension HomeViewModelTest { struct DummyData { static var dummyMusics: [Music] { var items: [Music] = [] // dummy let item1 = Music() item1.name = "tam" let item2 = Music() item2.name = "tien" items.append(item1) items.append(item2) return items } } }
Step 5: test case
Bây giờ, đây mới chính là công việc chính của chúng ta. Bắt đầu test từng case nào!
Số lượng section trong một TableView.
- HomeViewModel
func numberOfSections() -> Int { return 1 }
- HomeViewModelTest
it("Test func numberOfSection") { expect(viewModel.numberOfSections()) == 1 // cach khac expect(viewModel.numberOfSections()).to(equal(1)) }
Số lượng items trong một section.
- HomeViewModel
func numberOfItems(inSection section: Int) -> Int { musics.count }
- HomeViewModelTest
it("Test func numberOfItem in section 0") { expect(viewModel.numberOfItems(inSection: 0)) == 2 }
Dừng lại khoảng chừng 2 giây và nghĩ một chút nhóe!
Chúng ta dummyData
với cái mảng musics có 2 items nên chúng ta “expect” là 2
.
Vậy nếu chúng ta expect là 1
thì điều gì xảy ra?
Nó sẽ test fail case này và còn gợi ý kết quả “expect” đúng.
Tiếp tục, việc test từng item cho cell
- HomeViewModel
func viewModelForItem(at indexPath: IndexPath) -> HomeCellViewModel { return HomeCellViewModel(item: musics[indexPath.row], index: indexPath.row) }
- HomeViewModelTest
it("Test func viewModelForItem with row = 0, section = 0") { expect(viewModel.viewModelForItem(at: IndexPath(row: 0, section: 0))).to(beAnInstanceOf(HomeCellViewModel.self)) // cach khac expect(viewModel.viewModelForItem(at: IndexPath(row: 0, section: 0)).item?.name) == "tam" }
Sau khi “run” test, thì ta được kết quả như sau:
Okay! bạn đã đi dạo 1 vòng viết UnitTest với Quick & Nimble rồi đây. Và đây là toàn bộ phần test cho các bạn có cái nhìn tổng quan hơn.
override func spec() { var viewModel: HomeViewModel! describe("Test funcs homeScreen") { context("Test some funcs related to tableView") { beforeEach { viewModel = HomeViewModel() viewModel.musics = DummyData.dummyMusics } it("Test func numberOfSection") { expect(viewModel.numberOfSections()) == 1 // cach khac expect(viewModel.numberOfSections()).to(equal(1)) } it("Test func numberOfItem in section 0") { expect(viewModel.numberOfItems(inSection: 0)) == 2 } it("Test func viewModelForItem with row = 0, section = 0") { expect(viewModel.viewModelForItem(at: IndexPath(row: 0, section: 0))).to(beAnInstanceOf(HomeCellViewModel.self)) // cach khac expect(viewModel.viewModelForItem(at: IndexPath(row: 0, section: 0)).item?.name) == "tam" } afterEach { viewModel = nil } } context("Test func related to api") { } } }
Qua trên, là một số ví dụ cơ bản khi sử dụng “expect“. Thư viện Nimble hổ trợ ta rất nhiều để giúp ta có thể đạt được mục đích test.
Cứ làm nhiều vào, khó có stackoverflow. Ahihi!
Tạm kết
- Tìm hiểu Quick & Nimble.
- Các thành phần cơ bản sử dụng trong test.
- Giới thiệu các bạn cách test cơ bản sử dụng thư viện Quick & Nimble.
- Đưa ra một ví dụ cơ bản để sử dụng.
Thực sự khi làm dự án thật, thì một ViewModel có rất nhiều functions cần được test. Và một app hoàn chỉnh, thì lại có rất nhiều ViewModel nữa.
Đối với một newbie hoặc thế thệ devs cũ … thì đây là một điều kinh khủng. Nó ăn mòn vào lối suy nghĩ của từng thế hệ.
Bài viết tiếp theo, mình sẽ hướng dẫn các bạn viết test đối với những case có liên quan đến api.
Okay! Tới đây, mình xin kết thúc bài viết về Quick & Nimble trong UnitTest . Nếu có gì thắc mắc hay góp ý cho mình thì bạn có thể để lại bình luận hoặc gởi email theo trang Contact.
Cảm ơn bạn đã đọc bài viết này!
Written by Tâm Kun
Leave a Reply Cancel reply
Fan page
Tags
Recent Posts
- Charles Proxy – Phần 1 : Giới thiệu, cài đặt và cấu hình
- Complete Concurrency với Swift 6
- 300 Bài code thiếu nhi bằng Python – Ebook
- Builder Pattern trong 10 phút
- Observer Pattern trong 10 phút
- Memento Pattern trong 10 phút
- Strategy Pattern trong 10 phút
- Automatic Reference Counting (ARC) trong 10 phút
- Autoresizing Masks trong 10 phút
- Regular Expression (Regex) trong Swift
Archives
- September 2024 (1)
- July 2024 (1)
- June 2024 (1)
- May 2024 (4)
- April 2024 (2)
- March 2024 (5)
- January 2024 (4)
- February 2023 (1)
- January 2023 (2)
- November 2022 (2)
- October 2022 (1)
- September 2022 (5)
- August 2022 (6)
- July 2022 (7)
- June 2022 (8)
- May 2022 (5)
- April 2022 (1)
- March 2022 (3)
- February 2022 (5)
- January 2022 (4)
- December 2021 (6)
- November 2021 (8)
- October 2021 (8)
- September 2021 (8)
- August 2021 (8)
- July 2021 (9)
- June 2021 (8)
- May 2021 (7)
- April 2021 (11)
- March 2021 (12)
- February 2021 (3)
- January 2021 (3)
- December 2020 (3)
- November 2020 (9)
- October 2020 (7)
- September 2020 (17)
- August 2020 (1)
- July 2020 (3)
- June 2020 (1)
- May 2020 (2)
- April 2020 (3)
- March 2020 (20)
- February 2020 (5)
- January 2020 (2)
- December 2019 (12)
- November 2019 (12)
- October 2019 (19)
- September 2019 (17)
- August 2019 (10)