Nimble trong 10 phút

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:

  • Hello Testing iOS
  • UnitTest with Quick & Nimble
  • API Testing (UnitTest) with OHHTTPStubs

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)

  • Cài đặt CocoaPods
  • Sử dụng Bundle cho iOS Project

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!

Xem thêm  Powerlifting là gì ? Những lưu ý cho người mới tập Powerlifting

# 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
Xem thêm  Gain trong âm thanh là gì? Cách dùng nút gain trên amply, mixer

// 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ủa aClass 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ãn aProtocol.

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ủa aClass.

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!

Xem thêm  Cá Bơn Đông Lạnh (250-300G) - Farmed Turbot Fillet (Box ~1.2KG)-Palamos

// 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!