Mở đầu — vì sao bài này quan trọng
Trong suốt các bài trước, chúng ta đã kiểm thử ứng dụng di động qua giao diện: chạm nút, cuộn màn hình, xác nhận màn hình hiển thị đúng. Nhưng có một sự thật mà bất kỳ QA mobile chuyên nghiệp nào cũng phải sớm nhận ra: phần lớn ứng dụng di động ngày nay chỉ là một "lớp vỏ mỏng" hiển thị dữ liệu, còn toàn bộ nghiệp vụ thật sự nằm ở các API phía sau. Một app đặt xe, một app ngân hàng, một sàn thương mại điện tử — mỗi thao tác của người dùng đều dịch thành một loạt lời gọi HTTP tới server. Nếu API sai, không có bao nhiêu test UI cứu được bạn.
Điều làm cho API testing trong bối cảnh mobile trở nên đặc biệt là ở chỗ: client di động có những ràng buộc mà web không có. Mạng chập chờn, thiết bị yếu, người dùng nâng cấp app không đồng đều, và quan trọng nhất — app đã cài trên máy người dùng thì bạn không thể "sửa nóng" như web. Khi backend đổi cấu trúc response mà app cũ không hiểu, hàng triệu người dùng có thể gặp crash cùng lúc. Đây chính là lý do contract test (kiểm thử hợp đồng giữa client và server) trở thành một kỹ năng sống còn của QA mobile hiện đại.
Bài này sẽ giúp bạn tách bạch hai góc nhìn kiểm thử API, hiểu các mối lo đặc thù của mobile, và biết cách dựng contract test để bắt lỗi tương thích trước khi nó ra tới tay người dùng.
Khái niệm cốt lõi
Hai góc nhìn (test view) khi kiểm thử API mobile
Khi nói "test API cho mobile", thực chất chúng ta đang đứng ở hai vị trí rất khác nhau, và cần tách bạch rõ ràng.
Góc nhìn 1 — Mobile app POV (client gọi API). Ở đây bạn không quan tâm API "đúng" theo nghĩa tuyệt đối, mà quan tâm: app có gọi đúng endpoint không? có gửi đúng header, đúng token, đúng payload không? khi API trả về lỗi thì app xử lý thế nào? Bạn kiểm thử hành vi của client. Ví dụ, khi token hết hạn và API trả về 401, app có tự động refresh token rồi thử lại, hay đá người dùng ra màn hình đăng nhập một cách thô bạo? Đây là góc nhìn "từ trong app nhìn ra".
Góc nhìn 2 — API POV (endpoint trả đúng). Ở đây bạn bỏ qua app, gọi thẳng vào endpoint bằng công cụ như Postman, curl, hoặc REST Assured, và kiểm tra: status code có đúng không? cấu trúc JSON có đủ field không? kiểu dữ liệu có chính xác không? nghiệp vụ có đúng không (ví dụ: đặt hàng trừ đúng tồn kho)? Đây là góc nhìn "từ ngoài nhìn vào endpoint".
Một QA mobile giỏi phải làm cả hai. Chỉ test API POV thì bạn có thể tự tin endpoint hoàn hảo nhưng app vẫn crash vì gọi sai. Chỉ test app POV thì bạn khó cô lập được nguyên nhân khi có lỗi — không biết lỗi ở client hay server. Kết hợp cả hai giúp bạn khoanh vùng lỗi cực nhanh: nếu gọi thẳng API OK mà app hỏng, lỗi ở client; nếu gọi thẳng API cũng hỏng, lỗi ở server.
Contract test — trái tim của bài học
Contract test là kiểm thử "hợp đồng" — tức là thỏa thuận ngầm giữa hai bên về hình dạng của dữ liệu trao đổi. Hợp đồng này bao gồm: endpoint nào, method gì, request cần những field nào và kiểu gì, response trả về cấu trúc ra sao, các field bắt buộc và tùy chọn, ý nghĩa của từng status code.
Ý tưởng cốt lõi: thay vì để client và server đoán ý nhau, ta viết ra một bộ test đóng vai trò "trọng tài". Consumer (app di động) khai báo nó kỳ vọng nhận được gì; provider (backend) phải chứng minh nó đáp ứng đúng kỳ vọng đó. Nếu backend lỡ tay đổi tên field full_name thành fullName, contract test sẽ đỏ ngay trong CI của backend, trước khi build được deploy — chứ không phải để app phát hiện lúc chạy thật.
Có hai kiểu contract test phổ biến. Consumer-driven contract (điển hình là Pact): app định nghĩa hợp đồng, sinh ra file mô tả kỳ vọng, và backend chạy lại file đó để verify. Schema-based contract: hai bên cùng thống nhất một schema chuẩn (OpenAPI/Swagger, hoặc JSON Schema), rồi mỗi bên validate response thực tế ngược lại schema đó.
Các mối lo đặc thù của API mobile
Ngoài những vấn đề API chung, mobile có vài điểm bạn phải để mắt riêng.
Pagination (phân trang). App di động gần như luôn hiển thị danh sách dài — feed, tin nhắn, lịch sử giao dịch — và tải theo trang khi cuộn (infinite scroll). Bạn phải test kỹ: trang cuối trả về gì? có field next_cursor hay has_more không? khi hết dữ liệu thì next_cursor là null hay chuỗi rỗng? App phân biệt hai trường hợp này thế nào? Một lỗi kinh điển là API trả has_more: true kèm danh sách rỗng, khiến app gọi vô hạn.
Kích thước payload. Trên 3G/4G chập chờn, mỗi kilobyte đều đáng giá. Response nặng, ảnh base64 nhồi trong JSON, field thừa — tất cả làm chậm app và tốn data người dùng. Test cả tính đúng lẫn tính "gọn" của payload.
Versioning và tương thích ngược. Vì app cũ vẫn tồn tại trên máy người dùng nhiều tháng, backend không được xóa field cũ một cách tùy tiện. Contract test giúp phát hiện "breaking change".
Xử lý lỗi và timeout. Test cách app phản ứng với 4xx, 5xx, và với mạng treo (không trả về gì). Đây là phần app native khác web rõ nhất.
Tình huống thực tế
Tình huống 1 — Sàn TMĐT đổi tên field, app cũ trắng màn hình
Một sàn thương mại điện tử lớn ở Việt Nam (giả định tên ShopViet) có team backend và team mobile làm việc tách biệt. Trong một sprint, backend refactor API danh sách sản phẩm, đổi field price (số nguyên VND) thành object pricing: { amount, currency } để chuẩn bị bán hàng đa tiền tệ. Họ test kỹ trên môi trường staging với bản app mới nhất — mọi thứ xanh.
Nhưng khi lên production, khoảng 40% người dùng vẫn đang dùng app phiên bản cũ hơn hai tuần. App cũ đọc response.price để hiển thị giá; giờ price biến mất, app hiểu là undefined, và màn hình danh sách sản phẩm trắng xóa hoặc crash. Trong 30 phút, tỷ lệ crash tăng vọt và tổng đài nhận hàng trăm cuộc gọi.
Bài học: vấn đề không nằm ở việc backend viết code sai — endpoint mới hoàn toàn "đúng" theo API POV. Vấn đề là hợp đồng với app cũ đã bị phá vỡ. Nếu ShopViet có contract test chạy trong CI của backend, kiểm tra rằng field price (kiểu số) vẫn phải tồn tại cho tới khi tất cả client cũ ngừng hỗ trợ, breaking change này đã bị chặn ngay lúc merge. Cách xử lý đúng là cộng thêm field pricing mới song song với price cũ, chỉ xóa price sau khi buộc nâng cấp app tối thiểu.
Tình huống 2 — App ví điện tử và cái bẫy pagination
Một team QA tại một ví điện tử Đông Nam Á (giả định PayNow) kiểm thử màn hình lịch sử giao dịch. Ở góc nhìn app POV, họ cuộn danh sách và thấy chạy mượt với tài khoản test có 50 giao dịch. Không ai để ý gì.
Đến khi một khách hàng doanh nghiệp có hơn 3.000 giao dịch mở màn hình này, app tải trang đầu 20 giao dịch, rồi tiếp tục gọi API với cursor để lấy trang sau. Vấn đề: API trả về next_cursor nhưng do lỗi backend, cursor này trỏ ngược lại đúng trang vừa lấy. App cứ gọi mãi cùng một trang, next_cursor không bao giờ đổi, và vòng lặp tải vô hạn ngốn pin lẫn data, cuối cùng làm treo app.
QA đã bỏ sót vì họ test bằng dữ liệu quá nhỏ, không đủ để kích hoạt trang thứ hai. Khi họ chuyển sang API POV — gọi thẳng endpoint bằng Postman với tài khoản nhiều giao dịch — họ thấy ngay next_cursor lặp lại giá trị cũ. Một assertion đơn giản trong contract test ("cursor của trang N+1 phải khác cursor của trang N") sẽ bắt được lỗi này tự động.
Bài học: pagination phải được test với dữ liệu lớn thật và test ở cả hai góc nhìn. UI trơn tru không có nghĩa hợp đồng phân trang đúng. Luôn có test khẳng định: khi hết dữ liệu, has_more phải là false và next_cursor phải null.
Tình huống 3 — Contract test cứu một app giao đồ ăn
Một startup giao đồ ăn (giả định FoodFast) áp dụng Pact cho luồng đặt món. Team mobile định nghĩa contract: khi gọi POST /orders, họ kỳ vọng response chứa order_id (string), status (một trong pending, confirmed, cancelled), và estimated_minutes (số nguyên). File Pact này được đẩy lên một Pact Broker chung.
Vài tuần sau, backend muốn đổi estimated_minutes thành estimated_delivery_at dạng ISO timestamp cho linh hoạt hơn. Ngay khi họ chạy verify contract trong pipeline, Pact báo đỏ: "consumer FoodFast-Mobile kỳ vọng field estimated_minutes nhưng response không còn". Backend lập tức biết mình sắp phá app, bèn giữ cả hai field trong giai đoạn chuyển tiếp và phối hợp với team mobile lên lịch bỏ field cũ.
Bài học: contract test biến một sự cố tiềm tàng ngoài production thành một dòng báo đỏ trong CI — rẻ hơn hàng trăm lần. Giá trị lớn nhất của nó là phát hiện breaking change sớm và ép hai team phải giao tiếp trước khi thay đổi hợp đồng.
Hướng dẫn từng bước
Dưới đây là quy trình thực hành để bạn triển khai kiểm thử API mobile, đi từ đơn giản tới bài bản.
Bước 1 — Chặn và quan sát traffic thật của app. Dùng proxy như Charles Proxy hoặc mitmproxy đặt giữa thiết bị và server. Thao tác trên app và ghi lại từng request/response. Đây là cách trung thực nhất để biết app thật sự gọi gì, gửi header nào, dùng phiên bản API nào. Đừng dựa vào tài liệu — hãy dựa vào traffic thật.
Bước 2 — Kiểm thử API POV độc lập. Lấy các endpoint quan sát được ở bước 1, dựng bộ test trong Postman hoặc REST Assured. Với mỗi endpoint, kiểm: status code, cấu trúc JSON, kiểu dữ liệu từng field, các trường bắt buộc, và cả trường hợp lỗi (token sai → 401, id không tồn tại → 404). Chạy được bộ này mà không cần mở app.
Bước 3 — Định nghĩa schema hợp đồng. Nếu backend có sẵn file OpenAPI/Swagger, dùng nó làm nguồn sự thật. Nếu chưa, tự viết JSON Schema cho các response quan trọng. Sau đó validate response thực tế ngược lại schema — nhiều thư viện (như ajv cho JavaScript, hay tính năng schema validation trong Postman) làm việc này chỉ với vài dòng.
Bước 4 — Viết consumer-driven contract nếu đội ngũ đủ chín. Với Pact: ở phía app, viết test khai báo kỳ vọng và sinh file Pact. Đẩy file lên Pact Broker. Ở phía backend, thêm bước verify contract vào pipeline để backend chạy lại toàn bộ kỳ vọng của app.
Bước 5 — Test các mối lo đặc thù mobile. Thêm test riêng cho pagination (trang cuối, cursor lặp, danh sách rỗng), cho versioning (gửi header phiên bản app cũ, xác nhận backend vẫn tương thích), và cho payload size (đặt ngưỡng cảnh báo nếu response vượt kích thước hợp lý).
Bước 6 — Tự động hóa và đưa vào CI. Bộ API test và contract test phải chạy tự động mỗi lần backend build. Contract đỏ là chặn merge. Đây mới là điểm mà công sức của bạn thực sự bảo vệ được người dùng.
Lỗi thường gặp & mẹo
Lỗi: chỉ test qua UI, tưởng thế là đủ. Rất nhiều lỗi API bị "che" bởi cache của app hoặc bởi dữ liệu test nhỏ. Luôn bổ sung tầng test gọi thẳng API.
Lỗi: test API bằng dữ liệu quá đẹp, quá nhỏ. Như tình huống PayNow, dữ liệu 50 dòng không lộ ra lỗi pagination. Hãy chuẩn bị sẵn tài khoản test với dữ liệu lớn, dữ liệu biên (0 phần tử, đúng 1 trang, hơn 1 trang một chút).
Lỗi: coi mọi thay đổi API là an toàn. Thêm field mới thường an toàn; đổi tên, xóa field, đổi kiểu, hoặc thu hẹp tập giá trị hợp lệ đều là breaking change với app cũ. Ghi nhớ nguyên tắc: với client mobile, chỉ được cộng thêm, hạn chế tối đa xóa hoặc đổi.
Mẹo — test cả trường hợp app cũ. Gửi header X-App-Version với số phiên bản cũ và xác nhận backend vẫn trả về hợp đồng tương thích. Đây là điều web ít cần nhưng mobile bắt buộc.
Mẹo — validate kiểu dữ liệu, đừng chỉ validate giá trị. Một lỗi âm thầm là backend trả "price": "150000" (string) thay vì 150000 (number). App có thể vẫn hiển thị đúng nhưng logic tính toán (sắp xếp, cộng tổng) sẽ sai. Schema validation bắt được ngay.
Mẹo — đừng quên field null và field vắng mặt. field: null khác với không có field. App xử lý hai trường hợp này rất khác nhau; contract test nên nói rõ field nào được phép null.
Bài tập thực hành
- Chặn traffic. Cài Charles Proxy hoặc mitmproxy, kết nối một app di động bất kỳ (kể cả app demo bạn tự dựng), thực hiện luồng đăng nhập và tải một danh sách. Ghi lại toàn bộ request/response, chỉ ra: app dùng phiên bản API nào, gửi những header gì, response phân trang có field cursor không.
- Viết bộ API POV. Với endpoint danh sách vừa quan sát, viết 5 test trong Postman: (a) status 200, (b) response là mảng, (c) mỗi phần tử có đủ field bắt buộc, (d) kiểu dữ liệu đúng, (e) token sai trả 401. Chạy chúng độc lập với app.
- Bắt lỗi pagination. Tạo dữ liệu đủ nhiều để có ít nhất 3 trang. Viết test khẳng định: cursor mỗi trang phải khác nhau, và trang cuối trả về
has_more: falsevớinext_cursornull. Cố tình làm hỏng một điều kiện và xác nhận test đỏ.
- Viết một JSON Schema cho response chi tiết một đối tượng (ví dụ chi tiết đơn hàng), rồi validate response thật ngược lại schema. Thử đổi kiểu một field trong response giả lập và xác nhận schema bắt được.
- Suy nghĩ tình huống. Backend muốn đổi
status: "OK"thànhstatus: 0. Hãy viết ra: đây có phải breaking change không, vì sao, và bạn đề xuất cách chuyển đổi an toàn nào để app cũ không hỏng.
Tóm tắt
Kiểm thử API cho mobile luôn phải nhìn từ hai góc: app POV (client gọi có đúng, xử lý lỗi có tốt) và API POV (endpoint trả có đúng cấu trúc, đúng nghiệp vụ). Thiếu một góc là bạn đang mù một mắt. Điểm đặc thù của mobile — app cũ tồn tại lâu trên máy người dùng, mạng yếu, phân trang dài — khiến contract test trở thành công cụ then chốt: nó biến những "breaking change" nguy hiểm thành một dòng báo đỏ trong CI, rẻ và sớm, thay vì thành làn sóng crash ngoài production. Hãy nhớ nguyên tắc vàng với hợp đồng API di động: chỉ cộng thêm, cẩn trọng khi đổi, và tuyệt đối chậm rãi khi xóa. Ba tình huống ShopViet, PayNow, FoodFast đều dạy cùng một điều — chi phí phát hiện lỗi càng dịch về phía trái (càng sớm trong pipeline) thì càng rẻ, và contract test chính là cách bạn kéo nó về trái.