Contents
Chào mừng bạn đến với Fx Studio. Chúng ta đã cùng nhau tìm hiểu về các khái niệm cơ bản trong iOS Testing & UnitTest rồi. Tuy nhiên, chúng ta vẫn còn một chủ đề cơ bản nữa. Đó là tương tác với API trong UnitTest, hay còn gọi là API Testing.
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!
Vấn đề
Đối với functional test ngoài việc test các hàm xử lí logic, hàm để setup data … thì chúng ta cũng phải test các functions liên quan đến server, response data … kiểm tra request đó thành công hay thất bại.
Tại sao lúc test không nên phụ thuộc vào server?
Test phải đảm bảo nó nhanh, độc lập, có thể nhân rộng ra, tự verify được. Nhưng việc phụ thuộc vào network đã vi phạm những quy tắc này.
-
Làm chậm việc test.
-
Khó xác định.
-
Có nghĩa là chúng ta sẽ bị phụ thuộc vào data trả về từ server. Bạn không thể đảm bảo data trả về sẽ đúng như những gì bạn kì vọng.
-
Test cũng có thể bị “time out” bởi vì network ở một lúc nào đó nó sẽ bị tắt nghẽn hoặc server sẽ trả về một data không đúng, do phía backend chạy migration trên một staging server khác mà không thông báo cho bạn.
-
-
Khó để test trường hợp error.
-
Khi thực sự truy cập vào “real server“, thì thật sự khó để nó có thể rơi trường hợp failure, nếu rơi vào team backend xịn xò. Dẫn đến reponse data luôn success và không thể nào test được case failure.
-
Vậy nên:
Quá phụ thuộc và truy cập trực tiếp đến network sẽ làm việc test thực sự rất khó.
Đó là lí do chúng ta phải tách biệt, độc lập (decouple) từ network trong unittest.
Đó là lí do mình sẽ giới thiệu API Testing (UnitTest) with OHHTTPStubs.
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.
OHHTTPStubs
Khái niệm
OHHTTPStubs là một thư viện được thiết kế để “stub” cái request của bạn một cách dễ dàng.
Vậy stub là gì?
Stub là một chương trình hoặc thành phần giả lập để kết nối với các công đoạn khác thành một khối hoàn chỉnh.
Ví dụ: Chúng ta có 4 công đoạn, mà công đoạn đầu tiên phía BE làm chưa xong, thì phía Mobile phải làm sao?
Dev mobile đành ngồi chơi vậy!!!
Cách giải quyết là chúng ta create tạm thời “một thành phần giả lập cho cái công đoạn chưa hoàn thành đó” để những công đoạn tiếp theo diễn ra trôi chảy.
Stub kiểu như anh trai mưa vậy, cô gái mới cãi nhau người yêu cần một người lấp đầy khoảng trống, thì stub sẽ xuất hiện. Khi 2 người đó làm lành, thì stub tiếp tục lặng yên nhìn 2 người đó yêu nhau :(.
Còn đối với stub trong API Testing, thì với những lí do chúng ra không thể phụ thuộc quá vào server. Vì những lí do đã nêu trên, thì cần một “stub” để thay thế cho việc request đến server đó.
Cài đặt
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 OHHTTPStubs, 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 OHHTTPStubs. Xem đoạn code sau nhóe!
pod 'OHHTTPStubs/Swift'
Các bạn nhớ để OHHTTPStubs ở 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.
import OHHTTPStubs
Mọi thứ vẫn làm như bài trước và mình chỉ bắt đầu hướng dẫn lúc bắt đầu test với những functions liên quan đến server nhé!
Usage example
Setup
Để thực hiện việc test, ta cần phải chuẩn bị một số thức liên quan trước nhóe. Đầu tiên, ta sẽ có 1 function liên quan ở ViewModel trước.
func getDataCovid(completion: @escaping APICompletion) { HomeService.getDataCovid { [weak self] result in guard let this = self else { return } switch result { case .success(let items): this.covids = items completion(.success) case .failure(let error): completion(.failure(error)) } } }
Nhìn vào func trên, rõ ràng có 2 case chúng ta cần test là case success và failure.
Tiếp theo, chúng ta cần chuẩn bị bộ dữ liệu giả lập cho dữ liệu nhận được từ server. Ta sẽ tạo một file JSON và tùy ý việc đặt tên cho nó. Nhưng nội dung của nó thì tương tự với cấu trúc dữ liệu nhận được từ server. Ví dụ như sau:
Trong file detail.json thì nó như thế này:
{ "length": 841, "maxPageLimit": 2500, "totalRecords": 841, "data": [ { "date": "2022-05-20", "areaName": "United Kingdom", "areaCode": "K02000001", "confirmedRate": 33151.9, "latestBy": 6338, "confirmed": 22238713, "deathNew": 87, "death": 177977, "deathRate": 265.3 }, { "date": "2022-05-19", "areaName": "United Kingdom", "areaCode": "K02000001", "confirmedRate": 33142.5, "latestBy": 12208, "confirmed": 22232377, "deathNew": 169, "death": 177890, "deathRate": 265.2 } ] }
Các bạn phải làm đúng cấu trúc và key của jsonObject để chúng ta có thể map data đúng nhé!
Create Stub
Bắt tay vào công việc chính thôi nào. Để nhanh gọn mình sẽ code example một “stub” đơn giản rồi phân tích từng dòng một nhé.
stub(condition: isHost("api.coronavirus.data.gov.uk")) { _ in let path: String! = OHPathForFile("detail.json", type(of: self)) return HTTPStubsResponse(fileAtPath: path, statusCode: 200, headers: nil) }
Thư viện OHHTTPStubs hỗ trợ chúng ta một hàm “stub” để giả lập một quá trình request.
Nó có parameter:
-
condition: so khớp điều kiện coi đúng không , nếu đúng thì thực hiện request.
-
Nó cung cấp cho ta các điều kiện như isMethodGET(), isMethodPOST(), isMethodPUT(), isHost, isPATH.
-
Ở ví dụ đây, ta sử dụng isHost để check condition.URL: https://api.coronavirus.data.gov.uk/v1/data ,thì: Host: api.coronavirus.data.gov.uk
-
Hàm stub này sẽ trả về một HTTPStubResponse với các params:
-
fileAtPath: chính là file json mà ta tự tạo ra xem như nó chính là data response từ server về.
Qua đó chúng ta thấy được rằng nhờ OHHTTPStubs mà chúng ta có thể control được response nó trả về như thế nào, dữ liệu ra sao để ta đạt được kết quả kì vọng đúng.
let path: String! = OHPathForFile("detail.json", type(of: self))
-
statusCode: Chúng ta có thể control được mã trạng thái bằng cách nhập code mà mình mong muốn.
-
200: Ok. Request đã được tiếp nhận và xử lý thành công.
-
400: Bad Request. Server sẽ không thể xử lý hoặc sẽ không xử lý các request lỗi về phía client (request có cú pháp sai, ..)
-
500: Internal Server Error: Lỗi server khi mà server gặp một sự cố nào đó.
-
Còn nhiều statusCode khác nữa, tuỳ yêu cầu test những gì thì mình thay đổi statusCode thôi.
-
Trong ví dụ này , thì chúng ta đang test 2 trường hợp cơ bản là success có data với statusCode là 200 và failure do bad request với status code là 400.
-
-
Header: Tuỳ request đó có yêu cầu hay không, cần thì thêm vào không cần thì cho nó nil.
Test
Okay! Vậy là chúng ta vừa tạo một stub để chuẩn bị cho việc test server. Tiếp tục nào:
waitUntil(timeout: DispatchTimeInterval.seconds(20)) { done in viewModel.getDataCovid { _ in expect(viewModel.covids.count) == 2 done() } }
Chúng ta tiếp tục sử dụng hàm waitUntil của thư viện Nimble ở bài trước với param là timeout để thực hiện việc “chờ bất đồng bộ cho đến khi quá trình done() hoàn tất hoặc đã vượt quá timeout mà chúng ta cho phép”.
Sau khi gọi hàm getDataCovid()
, chúng ta kiểm thử output có đúng như kì vọng không? Thì ở file json chúng ta thấy rõ ràng là có 2 object Covid nên ta kì vọng mảng covids bằng 2. Nếu không đúng, thì có 2 nguyên nhân:
- File json chúng ta tạo ra có vấn đề, cấu trúc nó không đúng hoặc key bị sai.
- Timeout.
Như trong ví dụ:
Nếu ta request lên server bằng cách gọi hàm getDataCovid
vì một lí do nào đó mà quá thời gian 20s
thì nó sẽ báo lỗi “timeout” như này.
Chúng ta phải gọi closure done()
để báo rằng quá trình đợi đã được hoàn thành. Nếu không gọi, thì nó sẽ chạy ở trong đó mãi và đến thời gian timeout, thì nó sẽ báo lỗi tương tự như trên.
Results
- case Success
expect(viewModel.covids.count) == 2
Ở đây chúng ta có dựa vào detail.json mà chúng ta đã tạo ra để dự đoán số item của mảng covids mà server trả về có đúng hay không.
- case Failure: Bạn chỉ cần đổi status code từ 200 -> 400 để request của chúng ta thành bad request và kì vọng mảng covids sẽ là rỗng.
expect(viewModel.covids.count) == 0
Đây là phần code tổng quan cho các bạn dễ hình dung hơn:
override func spec() { var viewModel: DetailViewModel! context("Test func getdataCovid") { beforeEach { viewModel = DetailViewModel() } it("Test api success") { stub(condition: isHost("api.coronavirus.data.gov.uk")) { _ in let path: String! = OHPathForFile("detail.json", type(of: self)) return HTTPStubsResponse(fileAtPath: path, statusCode: 200, headers: nil) } waitUntil(timeout: DispatchTimeInterval.seconds(20)) { done in viewModel.getDataCovid { _ in expect(viewModel.covids.count) == 2 done() } } } it("Test api failure") { stub(condition: isHost("api.coronavirus.data.gov.uk")) { _ in let path: String! = OHPathForFile("detail.json", type(of: self)) return HTTPStubsResponse(fileAtPath: path, statusCode: 400, headers: nil) } waitUntil(timeout: DispatchTimeInterval.seconds(20)) { done in viewModel.getDataCovid { _ in expect(viewModel.covids.count) == 0 done() } } } afterEach { viewModel = nil } } }
Từ đó, chúng ta test đủ cả 2 case success và failure để đảm bảo coverage toàn bộ. Chúng ta có thể control được được sự thay đổi server bằng một số thay đổi thôi qua trình giả lập stub. Còn nếu ta request trực tiếp thì đảm bảo với bạn nó vừa lâu, vừa gây spam và rất khó để handle để nó “được lỗi“.
Advanced
Vấn đề
Phần này không liên quan đến việc test, nhưng có liên quan đến OHHTTPStubs thì mình cũng nói thêm những kinh nghiệm mà mình biết.
Cũng vì đam mê!
Trong một dự án có nhiều lúc BE không thể chạy trước vì một số lí do nên phía FE hay Mobile không nên đợi BE deploy xong mới làm vì vô cùng mất thời gian, ảnh hưởng đến dự án chung nên chúng ta có thể tận dụng khả năng của “stub” để “chạy trước BE” để xem chúng ta kiểm tra:
- Parse data đúng hay chưa?
- UI khi hiển thị dữ liệu nó đúng chưa?
- kiểm thử nhiều chức năng khác liên quan đến server.
Chúng ta sẽ thực hiện API Testing với dữ liệu giả lập.
Giải pháp
Các bước thực hiện như sau:
Step1: Phía BE làm chưa xong nhưng chắc chắn có document json, xml example... Chúng ta hoàn toàn có thể copy đoạn json,xml mẫu đó xem như đó là response trả về và muốn response trả về như thế nào thì vào đó mà sửa (file tương tự như detail.json ở ví dụ trên).
{ "length": 841, "maxPageLimit": 2500, "totalRecords": 841, "data": [ { "date": "2022-05-20", "areaName": "TP Da Nang", "areaCode": "K02000001", "confirmedRate": 123, "latestBy": 0, "confirmed": 4, "deathNew": 87, "death": 3, "deathRate": 265.3 }, { "date": "2022-05-19", "areaName": "Monstar-lab", "areaCode": "K02000001", "confirmedRate": 3445, "latestBy": 12208, "confirmed": 555, "deathNew": 169, "death": 177890, "deathRate": 265.2 } ] }
Ở đây mình tạo file test.json và thay đổi key “areaName” theo yêu cầu của dự án hay để test một số case đặc biệt nào đó.
Step2: Ở ViewModel chúng ta viết một hàm handle stub như sau:
// handle stub func handleStub(completion: () -> Void) { stub(condition: isHost("api.coronavirus.data.gov.uk")) { _ in let path: String! = OHPathForFile("test.json", type(of: self)) return HTTPStubsResponse(fileAtPath: path, statusCode: 200, headers: nil) } completion() }
File json, xml example phía BE đưa cho thì bỏ vào OHPathForFile nhé.
Step3: Ở ViewController chúng ta thực thi như sau:
override func viewDidLoad() { super.viewDidLoad() configTableView() //getDataCovid() viewModel.handleStub { self.getDataCovid() } }
Chúng ta chạy hàm getDataCovid()
sau khi hàm handleStub()
hoàn thành xong để đảm bảo sẽ có reponse trả về như ta mong muốn. Còn đến khi nào phía BE họ deploy lên, thì không cần dùng stub nữa, gọi trực tiếp như cũ.
Hãy trân trọng stub đi, lúc chúng ta khó khăn thì chỉ có stub giúp chúng ta, thế thôi!!
Step4: Build xem thành quả của mình có đúng như kì vọng của mình đặt ra hay không?
Như vậy là chúng ta có thể hoàn toàn control được dữ liệu về như thế nào, mong muốn ra sao để đạt được múc đích của mình chỉ với stub.
Ví dụ trên request này đã có sẵn, đã được deploy. Còn khi các bạn làm một api mới hoàn toàn thì nhớ check document cho kĩ để thay đổi cái host hoặc check kĩ các json, xml của bạn đã đúng cấu trúc và các key đã đúng hay chưa. Nếu nó sai, thì debug sửa lại cho đúng nhé.
Tạm kết
- Các vấn đề liên quan tới API Testing
- Giới thiệu thư viện OHHTTPStubs cho việc API Testing
- Hướng dẫn tương tác và test một API cơ bản
- Giả lập dữ liệu cho API với các stub
Bài viết đã hướng dẫn bạn cách sử dụng thư viện OHHTTPStubs để test những functions liên quan đến API. Và từ đó, ta có thể kết hợp với bài trước và bài này để có thể test được toàn bộ một ViewModel cụ thể nào đó.
Okay! Tới đây, mình xin kết thúc bài viết về API Testing với OHHTTPStubs 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ạn có thể checkout source code tại đây.
- Bài viết tiếp theo: (đang cập nhật)
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
- 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
You may also like:
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)