Contents
Chào mừng bạn đến với Fx Studio. Chủ đề bài viết lần này là về UnitTest trong iOS. Tuy nhiên, chúng ta sẽ quan tâm tới một thư viện được sử dụng khá nhiều, đó là Nimble. Nó là gì & hoạt động như thế nào, thì ta sẽ khám phá tiếp bên dưới 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ì đã có một bài viết về cách viết UnitTest với Quick & Nimble rồi. Do đó, bài viết này chỉ tập trung những gì mà Nimble có và cách bạn sử dụng thư viện mà thôi.
Giới thiệu Nimble
Nimble là một framework giúp diễn tả kết quả mong đợi của testcase bằng cách sử dụng ngôn ngữ tự nhiên, dễ hiểu hơn so với framework XCTest mà Apple cung cấp.
Ví dụ với một dòng test bằng XCTest của Xcode.
// XCTest XCTAssertEqual(1 + 1, 2, "One plus one should be equal to two")
Ta sẽ viết lại nó bằng Nimble như sau:
// Nimble expect(1 + 1).to(equal(2), description: "One plus one should be equal to two")
Về cảm nhận đầu tiên, bạn sẽ thấy dễ hiểu test hơn khi được viết bằng Nimble. Các chú thích hay các tên tham số rất cụ thể.
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 đoạn code ở Podfile như sau nhóe!
# Podfile use_frameworks! target "MyApp" do # Normal libraries abstract_target 'Tests' do inherit! :search_paths target "MyAppTests" target "MyAppUITests" pod 'Quick' pod 'Nimble' end end
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.
expect() – diễn tả kết quả mong đợi
Keyword “expect” trong Nimble rất quan trọng. Nó diễn tả kết quả mong đợi bằng cách sử dụng các hàm cơ bản:
expect(<kết quả>).to(<mong đợi>)
expect(<kết quả>).toNot<mong đợi>)
expect(<kết quả>).notTo(<mong đợi>)
Ví dụ code nhóe!
import Nimble ... expect(username).to(equal("A Nguyen V.")) expect(username).toNot(equal("A Nguyen V.")) expect(username).notTo(equal("A Nguyen V."))
Trong đó
notTo
vàtoNot
giống nhau.
Bên cạnh đó, bạn còn có thể custom thông báo khi lỗi xảy ra:
expect(<kết quả>).to(<mong đợi>, description: <thông báo khi lỗi>)
Xem tiếp ví dụ code nhóe!
import Nimble ... expect(username).to(equal("A Nguyen V."), description: "User name should be equal to `A Nguyen V.`")
Những phương thức thường sử dụng
Phần tiếp theo, mình sẽ liệt kê các phương thức expect() mà bạn thường sử dụng với Nimble.
Kiểm tra khác kiểu
Nimble không cho phép biên dịch khi ta expect kết quả
với mong đợi
khác kiểu dữ liệu. Ví dụ như sau:
// Does not compile: expect(1 + 1).to(equal("Squee!"))
Trong đó:
- Kết quả của expect là một kiểu Int
- Mong đợi lại là một kiểu String
Đây cũng điều đầu tiên mà bạn cần lưu ý và cũng có thể lợi dụng để test. Biết đâu được nhĩ!
Bất đồng bộ
Về bất đồng bộ, bạn sẽ sử dụng nó khá nhiều. Nhất là trong các việc test với server hay các API. Chúng ta sẽ có các function liên quan như sau:
expect(<kết quả>).toEventually(<mong đợi>, description: <thông báo khi lỗi>)
expect(<kết quả>). toEventuallyNot(<mong đợi>, description: <thông báo khi lỗi>)
waitUntil { done in <thực thi expect> done()}
waitUntil(timeOut: <giá trị>) {done in <thực thi expect> done()}
Giải thích đơn giản:
- toEventually giúp bạn có thể dự đoán một thứ gì đó “trong tương lai”.
- waitUntil thì chờ đợi kết quả và so sánh với mong đợi
Ví dụ code nhóe!
- toEventually
var fishes: [String] = [] api.request { fish in fishes.append(fish) // value is whales } expect(fishes).toEventually(contain("whales"))
var fishes: [String] = [] api.request { fish in // value is whales fishes.append(fish) } expect(fishes).toEventually(contain("whales"), timeout: 3)
- waitUntil
// Call done() to finish expect() function, after timeout if done() is not executed, expect() func is fail waitUntil(timeout: 3.0) { done in api.request { fish in expect(fish) == "whales" done() } }
Chú ý done()
& giá trị timeout mặc định sẽ là 1.0
nhóe!
So sánh giá trị
Những giá trị được so sánh phải extension từ Equatable
, Comparable
, hoặc là các lớp con của NSObject
. Ta sẽ có những cách so sánh sau với việc sử dụng 2 kiểu viết code (dùng method & dùng toán tử).
// Equal expect(actual).to(equal(expected)) // Or expect(actual) == expected // Less than expect(actual).to(beLessThan(expected)) // Or expect(actual) < expected // Less than or equal expect(actual).to(beLessThanOrEqualTo(expected)) // Or expect(actual) <= expected // Greater than expect(actual).to(beGreaterThan(expected)) // Or expect(actual) > expected // Greater than or equal expect(actual).to(beGreaterThanOrEqualTo(expected)) // Or expect(actual) >= expected
Kiểm tra kiểu
beAKindOf(aClass)
: Passes khi kết quả cần kiểm tra có kiểu làaClass
, hoặc khi nó là một thể hiện củaaClass
hay là bất kỳ lớp con nào từaClass
.beAKindOf(aProtocol)
: Passes khi kết quả cần kiểm tra là một thể hiện thoả mãnaProtocol
.
protocol SomeProtocol {} class SomeClass {} class TestClass: SomeClass {} extension TestClass: SomeProtocol {} // The following tests passes expect(1).to(beAKindOf(Int.self)) expect("turtle").to(beAKindOf(String.self)) let testClass = TestClass() expect(testClass).to(beAKindOf(TestClass.self)) expect(testClass).to(beAKindOf(SomeClass.self)) expect(testClass).to(beAKindOf(SomeProtocol.self))
beAnInstanceOf(aClass)
: Passes khi là thể hiện chính xác củaaClass
.
protocol SomeProtocol {} class SomeClass {} class TestClass: SomeClass {} extension TestClass: SomeProtocol {} // The following tests passes expect(1).to(beAnInstanceOf(Int.self)) expect("turtle").to(beAnInstanceOf(String.self)) let testClass = TestClass() expect(testClass).to(beAnInstanceOf(TestClass.self)) expect(testClass).toNot(beAnInstanceOf(SomeClass.self)) expect(testClass).toNot(beAnInstanceOf(SomeProtocol.self))
Và bạn có thể dùng phương thức này để kiểm tra các kiểu dữ liệu của các CellViewModel con trong một ViewModel lớn.
Kiểm tra tham chiếu
Cũng ít khi sử dụng tới, nhưng mà cũng vì đam mê nên bỏ vào thôi.
// Passes if 'actual' has the same pointer address as 'expected': expect(actual).to(beIdenticalTo(expected)) expect(actual) === expected // Passes if 'actual' does not have the same pointer address as 'expected': expect(actual).toNot(beIdenticalTo(expected)) expect(actual) !== expected
Xử lí lỗi
Các function có xử lý lỗi thì đặc trưng sẽ là try catch
& throw
. Nên khi viết UnitTest thì ta cũng phải viết luôn việc xử lý các phần đó. Ta có ví dụ với 1 function như sau:
enum SomethingError: Error { case less case greater } func checkZeroNumber(_ number: Int) throws -> Int { if number < 0 { throw SomethingError.less } if number > 0 { throw SomethingError.greater } return number }
Bạn sẽ sử dụng expect tới try
& có thể kèm với closure để xử lý throw
. Xem tiếp ví dụ nhóe!
// Passes if 'checkZeroNumber(:_)' throws an 'Error' expect { try checkZeroNumber(-1)}.to(throwError()) expect { try checkZeroNumber(1)}.to(throwError()) // Passes if 'checkZeroNumber(:_)' is an expected value expect { try checkZeroNumber(0)}.to(equal(0)) // Passes if 'checkZeroNumber(:_)' throws an 'less' error expect { try checkZeroNumber(-1)}.to(throwError(SomethingError.less)) // Passes if 'checkZeroNumber(:_)' throws an 'greater' error expect { try checkZeroNumber(1)}.to(throwError(SomethingError.greater)) // Passes if 'checkZeroNumber(:_)' throws an error type expect { try checkZeroNumber(-1)}.to(throwError(errorType: SomethingError.self)) // Expected error by closure expect { try checkZeroNumber(-1)}.to(throwError { error in expect(error) == SomethingError.less })
Custom validation
Tiếp theo, với expect bạn có thể tùy chỉnh các mong đợi tương ứng với các kiểu Result. Ví dụ nhóe
enum Result { case success case failure(String) } // passes if .succeeded is returned from the closure let actual: Result = .success expect({ guard case Result.success = actual else { return .failed(reason: "Wrong enum case") } return .succeeded }).to(succeed()) // passes if .succeeded is returned from the closure let actual2: Result = .failure("a") expect({ guard case Result.failure("a") = actual2 else { return .failed(reason: "Wrong enum case") } return .succeeded }).to(succeed()) // passes if .failed is returned from the closure let actual3: Result = .failure("a") expect({ guard case Result.failure("b") = actual3 else { return .failed(reason: "Wrong enum case") } return .succeeded }).notTo(succeed())
Trong đó:
- Tại expect, bạn sẽ cung cấp 1 closure để kiểm tra các điều kiện
- Bạn sẽ return về các case phù hợp
- Cuối cùng, bạn sẽ so sánh với mong đời
.to(succeed())
Để có thể đơn giản hóa đi việc test như trên. Bạn sẽ thêm các extension của Result với các protocol có thể so sánh được, như là: Equatable & Comparable. Ví dụ:
enum Result { case success case failure(String) } extension Result: Equatable { public static func == (lhs: Result, rhs: Result) -> Bool { switch (lhs, rhs) { case (.success, .success): return true case let (.failure(lhsValue), .failure(rhsValue)): return lhsValue == rhsValue default: return false } } } let actual: Result = .success let actual2: Result = .faillure("a") // Equal expect(actual) == Result.success expect(actual).to(equal(Result.success)) // Equal expect(actual2) == Result.failure("a") expect(actual2).to(equal(Result.failure("a")))
Tạm kết
- Tìm hiểu về thư viện Nimble
- Diễn tả kết quả mong đợi với expect
- Những phương thức thường sử dụng trong các trường hợp cần test.
Okay! Tới đây, mình xin kết thúc bài viết về 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.
- Bài viết tiếp theo tại đây.
Cảm ơn bạn đã đọc bài viết này!
Related Posts:
Written by Tâm Kun
Leave a Reply Cancel reply
Fan page
Tags
Recent Posts
- Phù thủy phiên dịch ý tưởng
- XML Delimiters – Mở khóa thế giới prompt phức tạp
- Instructions – Cung cấp hướng dẫn cho các Gen AI
- SMART – Hướng dẫn dành tạo Prompt cho người mới bắt đầu
- Nhìn lại năm 2024
- CO-STAR – Công thức vàng để viết Prompt hiệu quả cho LLM
- Prompt Engineering trong 10 phút
- Một số ví dụ sử dụng Prompt cơ bản khi làm việc với AI
- Prompt trong 10 phút
- Charles Proxy – Phần 1 : Giới thiệu, cài đặt và cấu hình
You may also like:
Archives
- January 2025 (5)
- December 2024 (4)
- 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)