Product Management
Đăng nhập
ESC

Nhập từ khóa để tìm kiếm

↑↓ Di chuyển
Enter Mở
ESC Đóng

Bài 6 — Mobile Testing Pyramid

Đã đọc 0/13 mục · còn khoảng 13 phút
Cách chúng tôi ghi nhận

Một mục được tính là đã đọc khi nó nằm giữa màn hình đủ lâu so với độ dài của nó (bảng và sơ đồ tính một nửa), bạn vẫn đang cuộn hoặc bấm phím, và bạn đã thấy tới cuối mục. Lướt nhanh hoặc để tab mở không được tính. Chúng tôi chỉ lưu thời gian đọc từng mục — không lưu gì khác.

Mở đầu — vì sao bài này quan trọng

Nếu bạn từng nhìn một đội QA mobile chạy 400 test UI mỗi lần build và mất 3 tiếng chờ kết quả, chỉ để phát hiện 30 test đỏ vì một cái nút bị dịch 5 pixel, thì bạn đã thấy tận mắt cái giá của việc không có chiến lược phân tầng test. Testing Pyramid (kim tự tháp kiểm thử) chính là bản đồ giúp bạn trả lời câu hỏi quan trọng nhất trong đời một QA automation: "Loại test nào nên viết nhiều, loại nào viết ít, và tại sao?"

Bài này quan trọng vì nó là bài "kiến trúc tư duy" trước khi bạn lao vào code Appium ở các bài sau. Nhiều bạn học Appium xong là viết ngay 200 test E2E, tưởng rằng "càng nhiều test end-to-end càng an toàn". Sáu tháng sau, bộ test đó trở thành gánh nặng: chậm, dễ vỡ (flaky), tốn tiền chạy device farm, và cả team sợ mỗi khi phải bảo trì. Hiểu đúng pyramid từ đầu giúp bạn tránh cái bẫy đó.

Điểm mấu chốt của bài: kim tự tháp cổ điển của Mike Cohn — Unit / Service / E2E — không đủ và bị "xoay méo" khi áp dụng cho mobile, vì app mobile có thêm nhiều tầng đặc thù (native UI, bridge JavaScript, tương tác hệ điều hành, snapshot UI). Chúng ta sẽ học pyramid gốc, hiểu vì sao mobile phá vỡ nó, rồi dựng lại một mô hình phân tầng thực dụng cho QA mobile.

Khái niệm cốt lõi

Kim tự tháp cổ điển của Mike Cohn

Năm 2009, Mike Cohn trong cuốn Succeeding with Agile vẽ ra một kim tự tháp ba tầng đã trở thành kinh điển:

  • Đáy — Unit test (nhiều nhất): kiểm thử một hàm/lớp riêng lẻ, không phụ thuộc mạng, database hay UI. Chạy trong mili-giây. Rẻ, nhanh, ổn định.
  • Giữa — Service/Integration test (vừa phải): kiểm thử sự phối hợp giữa các thành phần — ví dụ tầng logic gọi API, đọc/ghi database. Chậm hơn unit nhưng vẫn không cần giao diện.
  • Đỉnh — UI/End-to-End test (ít nhất): giả lập một người dùng thật bấm qua toàn bộ luồng. Chậm, đắt, dễ vỡ, nhưng cho niềm tin cao nhất rằng "hệ thống thật sự chạy được".
Triết lý cốt lõi: càng lên cao càng ít test, vì chi phí viết và bảo trì tăng, tốc độ giảm, độ ổn định giảm. Một chiếc bug được bắt bởi unit test rẻ hơn gấp trăm lần so với bị bắt bởi E2E test — vì unit test chỉ ra chính xác dòng code hỏng, còn E2E chỉ nói "luồng thanh toán fail" và bạn phải đi dò.

Vì sao pyramid cổ điển "không đủ" cho mobile

Vấn đề là Mike Cohn nghĩ về ứng dụng web/server truyền thống. App mobile có ba đặc điểm phá vỡ giả định đó:

1. UI là "phần thịt" của app mobile. Với một app ngân hàng trên web, phần lớn logic nằm ở backend. Nhưng với app mobile, rất nhiều logic sống ngay trong client: xử lý offline, cache cục bộ, animation, state của màn hình, điều hướng. Nếu bạn chỉ test unit ở tầng backend, bạn bỏ sót đúng nơi rủi ro nhất. Tầng đỉnh của pyramid vì thế bị "phình to" một cách tự nhiên.

2. Nhiều tầng chạy (runtime) chồng lên nhau. Một app React Native có: code JS của bạn → bridge → native module Android/iOS → hệ điều hành. Một bug có thể nằm ở bất kỳ tầng nào. Pyramid ba tầng đơn giản không diễn tả được sự phức tạp này.

3. Sự phân mảnh thiết bị và hệ điều hành. Test "chạy đúng logic" (unit) không đảm bảo "chạy đúng trên Samsung Galaxy A05 chạy Android 13 với màn hình notch". Có một chiều rủi ro hoàn toàn mới — tương thích thiết bị — mà pyramid gốc không hề đề cập.

Kết quả: nhiều team mobile trên thực tế không có "kim tự tháp" mà có "chiếc kem ốc quế ngược" (ice-cream cone) — nhiều E2E test ở đỉnh, ít unit test ở đáy. Đây chính là anti-pattern (mẫu sai) mà bài này muốn bạn nhận diện và tránh.

Mô hình phân tầng thực dụng cho QA mobile

Thay vì ba tầng cứng nhắc, cộng đồng mobile testing (đặc biệt là các tài liệu của Google và Thoughtworks) đề xuất một pyramid mở rộng. Với vai trò QA, bạn nên hình dung năm tầng sau, từ đáy lên đỉnh:

  • Unit test (đáy, ~60-70%): logic thuần — hàm format tiền tệ, validate số điện thoại, parse JSON. Dev viết là chính, nhưng QA nên biết chúng tồn tại và tin tưởng chúng.
  • Integration/Component test (~15-20%): test một màn hình hoặc một component cô lập với dữ liệu giả (mock). Ví dụ: render màn hình giỏ hàng với 3 sản phẩm mock, kiểm tra tổng tiền hiển thị đúng. Nhanh hơn E2E nhiều vì không cần backend thật, không cần đi qua nhiều màn hình.
  • UI/E2E test trên emulator (~10-15%): Appium/Espresso/XCUITest chạy các luồng quan trọng (đăng nhập, thanh toán) trên máy ảo. Đây là tầng QA automation "sở hữu" nhiều nhất.
  • E2E trên thiết bị thật / device farm (~5%): chỉ chạy một tập nhỏ "smoke test" trên thiết bị thật để bắt lỗi phần cứng, cảm biến, hiệu năng thực.
  • Manual & Exploratory (không nằm trong % tự động, nhưng luôn tồn tại): kiểm thử khám phá, cảm nhận trải nghiệm — thứ mà máy không thay được.
Con số phần trăm không phải luật thánh; điều quan trọng là hình dạng: đáy rộng, đỉnh hẹp. Nếu bộ test của bạn ngược lại, bạn đang gánh nợ kỹ thuật.

Tình huống thực tế

Tình huống 1 — Sàn TMĐT "chiếc kem ốc quế ngược"

Một sàn thương mại điện tử tại TP.HCM (giả định tên "ShopViet") có đội QA 6 người. Khi mình vào rà soát, họ tự hào khoe 380 test Appium E2E, "phủ gần như mọi màn hình". Nghe rất ấn tượng — cho đến khi nhìn số liệu vận hành:

  • Mỗi lần chạy full suite mất 2 giờ 40 phút.
  • Tỷ lệ flaky (test đỏ nhưng chạy lại thì xanh) là ~18% — nghĩa là cứ mỗi lần chạy có khoảng 68 test đỏ giả.
  • Chi phí BrowserStack cho device farm khoảng 1.100 USD/tháng, phần lớn đốt vào việc chạy đi chạy lại các E2E dài dòng.
  • Đội có đúng 0 unit test và 0 component test.
Họ đang sống trong ice-cream cone điển hình. Vì mọi test đều là E2E, một thay đổi nhỏ ở màn hình đăng nhập làm đỏ 40 test — không phải vì logic sai, mà vì locator (bộ chọn phần tử) bị lệch.

Diễn giải & bài học: Đội tái cấu trúc trong 2 sprint. Họ chuyển phần lớn kiểm tra "tính đúng của tổng tiền, thuế, mã giảm giá" xuống tầng component test (render màn hình giỏ hàng với data mock, không đi qua login/tìm kiếm/thêm giỏ). Từ 380 E2E, họ giữ lại 45 E2E cho các luồng doanh thu cốt lõi, còn lại đẩy xuống dưới. Kết quả sau 2 tháng: full suite còn 28 phút, flaky rate còn 4%, hóa đơn device farm giảm còn ~450 USD. Bài học: đừng đo sức khỏe bộ test bằng số lượng E2E, hãy đo bằng hình dạng của kim tự tháp.

Tình huống 2 — Fintech và cái bẫy "test hết trên thiết bị thật"

Một startup ví điện tử ở Đông Nam Á (giả định "PayNow") sợ bug thanh toán đến mức ra chính sách: mọi test tự động phải chạy trên thiết bị thật để "giống người dùng nhất". Nghe có lý về mặt niềm tin, nhưng thảm họa về mặt vận hành.

Họ có một device farm nội bộ 12 điện thoại thật. Mỗi đêm CI phải xếp hàng chạy 220 test trên 12 máy đó. Đến sáng, kết quả thường xuyên không đầy đủ vì máy hết pin, mất Wi-Fi, hoặc dính popup cập nhật hệ điều hành. Đội QA mất trung bình 1.5 giờ mỗi sáng chỉ để phân loại "test này đỏ vì bug hay vì thiết bị dở chứng".

Diễn giải & bài học: Vấn đề là họ nhầm lẫn giữa "cần niềm tin cao" và "phải chạy TẤT CẢ ở tầng đỉnh". Giải pháp đúng theo pyramid: đẩy 90% test xuống emulator (nhanh, ổn định, chạy song song hàng chục instance trên cloud), và chỉ giữ một tập smoke ~15 test chạy trên thiết bị thật cho những gì chỉ thiết bị thật mới bắt được — như xử lý NFC, cảm biến vân tay, hoặc hiệu năng cuộn danh sách trên máy giá rẻ. Sau khi tách, thời gian chạy giảm 70% và độ tin cậy tăng vọt. Bài học: thiết bị thật là tài nguyên quý và hiếm — dùng nó ở đỉnh cho đúng loại rủi ro, đừng lãng phí nó cho những gì emulator làm tốt hơn.

Tình huống 3 — App tin tức và tầng bị bỏ quên

Một tòa soạn số ở Hà Nội (giả định "TinNhanh") có app đọc báo React Native. Đội QA nhỏ, 2 người, viết được ~60 E2E Appium ổn. Nhưng họ liên tục để lọt một loại bug: hàm định dạng thời gian ("3 phút trước", "hôm qua", "2 tuần trước") sai khi mốc thời gian rơi vào các biên như đúng 60 giây, hoặc qua nửa đêm.

Loại bug này gần như không thể bắt hiệu quả bằng E2E — bạn phải giả lập đủ mốc thời gian, đi qua nhiều màn hình, rất tốn công. Nhưng ở tầng unit, nó là chuyện của 10 dòng test: đưa vào một timestamp, kỳ vọng một chuỗi ra.

Diễn giải & bài học: QA ngồi lại với dev, chỉ ra rằng hàm formatRelativeTime cần một bộ unit test biên. Dev viết 14 unit test trong nửa buổi, bắt được 3 bug biên ngay lập tức. Bài học: QA giỏi không chỉ tự viết test, mà biết loại bug nào thuộc về tầng nào, và biết đẩy trách nhiệm về đúng tầng — kể cả khi tầng đó do dev sở hữu. Pyramid là ngôn ngữ chung để QA và dev thương lượng "bug này nên bị bắt ở đâu".

Hướng dẫn từng bước

Đây là quy trình để bạn đánh giá và định hình pyramid cho một dự án mobile thực tế.

Bước 1 — Kiểm kê bộ test hiện có. Đếm số test theo từng tầng: bao nhiêu unit, bao nhiêu component/integration, bao nhiêu E2E emulator, bao nhiêu E2E thiết bị thật. Vẽ ra thành biểu đồ. Nếu bạn thấy hình quả trám hoặc kem ốc quế ngược, đó là tín hiệu đỏ.

Bước 2 — Đo ba chỉ số sức khỏe. Với bộ E2E hiện tại, ghi lại: (a) thời gian chạy full suite, (b) tỷ lệ flaky (chạy lại 5 lần, đếm số lần đổi màu), (c) chi phí hạ tầng/tháng. Đây là bằng chứng để thuyết phục cả team thay đổi.

Bước 3 — Phân loại từng test theo câu hỏi "bug này thuộc tầng nào?". Với mỗi E2E, hỏi: "Nếu test này chỉ để kiểm tra logic/tính toán, nó có thể sống ở tầng component hoặc unit không?" Nếu có, đánh dấu để hạ tầng. Chỉ giữ ở E2E những test thật sự cần "đi hết luồng người dùng".

Bước 4 — Xác định danh sách luồng cốt lõi (critical path). Liệt kê 5-10 luồng mà nếu hỏng thì mất tiền hoặc mất khách: đăng nhập, thêm giỏ, thanh toán, đăng ký. Đây là những luồng xứng đáng có E2E. Mọi thứ khác nên bị nghi ngờ.

Bước 5 — Tách tập smoke cho thiết bị thật. Từ danh sách E2E cốt lõi, chọn ra tập nhỏ nhất chỉ chạy trên thiết bị thật — ưu tiên những gì liên quan phần cứng, cảm biến, hiệu năng. Phần còn lại chạy emulator.

Bước 6 — Thiết lập cơ chế chạy phân tầng trong CI. Cấu hình pipeline: unit + component chạy mỗi commit (phải xong dưới 5 phút), E2E emulator chạy mỗi merge request, E2E thiết bị thật chạy 1 lần/đêm hoặc trước release. Đừng bắt mọi tầng chạy cùng lúc mọi lúc.

Bước 7 — Theo dõi và điều chỉnh định kỳ. Mỗi cuối sprint, xem lại hình dạng pyramid và ba chỉ số sức khỏe. Pyramid là thứ sống động, không phải bản vẽ một lần.

Lỗi thường gặp & mẹo

Lỗi 1 — Coi số lượng E2E là thước đo chất lượng. Rất nhiều báo cáo QA khoe "đã có 500 test tự động" mà không nói tầng nào. 500 E2E là gánh nợ; 500 test phân bổ đúng pyramid là tài sản. Mẹo: trong báo cáo, luôn tách số liệu theo tầng.

Lỗi 2 — Đẩy mọi thứ lên E2E vì "thấy an tâm hơn". E2E cho cảm giác an toàn giả. Một bug tính toán tiền tệ bị bắt bởi E2E nghĩa là bạn đã để nó đi qua 4 màn hình mới lộ ra. Mẹo: mỗi khi định viết một E2E mới, tự hỏi "phần này có thể test ở tầng thấp hơn không?".

Lỗi 3 — Nhầm "chạy trên thiết bị thật" với "test tốt hơn". Thiết bị thật chỉ hơn ở đúng vài loại rủi ro (phần cứng, hiệu năng thật). Với logic UI, emulator ổn định hơn nhiều. Mẹo: dùng thiết bị thật như thuốc đặc trị, không phải vitamin uống hằng ngày.

Lỗi 4 — Bỏ quên hoàn toàn tầng component/integration. Đây là "tầng giữa mất tích" phổ biến nhất ở mobile. Nó là điểm ngọt: nhanh gần như unit, nhưng test được cả UI render. Mẹo: học công cụ component test của nền tảng (Espresso với Robolectric cho Android, XCTest cho iOS, React Native Testing Library cho RN) — đây là nơi bạn thu lời lớn nhất.

Lỗi 5 — Không tính đến "test tương thích thiết bị" như một chiều riêng. Pyramid nói về loại test, không nói về ma trận thiết bị. Bạn cần cả hai. Mẹo: hình dung pyramid như trục dọc (loại test) và ma trận thiết bị như trục ngang (chạy trên bao nhiêu cấu hình).

Mẹo tổng: khi tranh luận với team, đừng nói "chúng ta viết sai test". Hãy vẽ pyramid hiện tại lên bảng, để cả team tự nhìn thấy hình dạng méo mó. Hình ảnh thuyết phục hơn lời nói.

Bài tập thực hành

  • Kiểm kê nhanh: Lấy một dự án mobile bất kỳ (của bạn, hoặc một open-source như một app React Native trên GitHub). Đếm và phân loại test theo 4 tầng. Vẽ ra hình dạng thực tế. Nó giống kim tự tháp, quả trám, hay kem ốc quế ngược?
  • Bài toán phân tầng: Cho 6 kịch bản sau, hãy quyết định mỗi kịch bản nên test ở tầng nào (unit / component / E2E emulator / E2E thiết bị thật) và giải thích ngắn:
- (a) Hàm chuyển "1000000" thành "1.000.000 ₫". - (b) Kiểm tra người dùng đăng nhập bằng OTP thành công rồi vào được trang chủ. - (c) Màn hình danh sách sản phẩm hiển thị đúng badge "Hết hàng" khi tồn kho = 0. - (d) Quét vân tay để xác nhận thanh toán. - (e) Nút "Thêm vào giỏ" đổi màu khi được bấm. - (f) App cuộn mượt (60fps) danh sách 500 sản phẩm trên máy Android giá rẻ.

  • Đề xuất cải tổ: Với tình huống ShopViet ở trên (380 E2E), viết một bản đề xuất 5 gạch đầu dòng gửi trưởng nhóm, giải thích bạn sẽ hạ tầng những nhóm test nào và giữ lại E2E cho những luồng nào.
(Gợi ý đáp án bài 2: a→unit; b→E2E emulator; c→component; d→E2E thiết bị thật; e→component; f→E2E thiết bị thật/performance.)

Tóm tắt

  • Kim tự tháp Mike Cohn (Unit / Service / E2E) dạy nguyên tắc vàng: đáy rộng, đỉnh hẹp — nhiều test rẻ-nhanh-ổn định ở dưới, ít test đắt-chậm-dễ vỡ ở trên.
  • Mobile phá vỡ pyramid cổ điển vì UI là "phần thịt" của app, có nhiều tầng runtime chồng lên nhau, và thêm chiều rủi ro phân mảnh thiết bị.
  • Anti-pattern nguy hiểm nhất là ice-cream cone — quá nhiều E2E, quá ít unit — dẫn tới suite chậm, flaky và tốn kém, như trường hợp ShopViet.
  • Mô hình thực dụng cho QA mobile có 5 tầng: unit → component/integration → E2E emulator → E2E thiết bị thật → manual/exploratory. Tầng component thường bị bỏ quên nhưng là điểm ngọt lớn nhất.
  • Thiết bị thật là tài nguyên quý, chỉ dùng ở đỉnh cho rủi ro phần cứng/hiệu năng — đừng như PayNow bắt mọi test chạy trên máy thật.
  • QA giỏi dùng pyramid như ngôn ngữ chung để quyết định "bug này nên bị bắt ở tầng nào" và thương lượng trách nhiệm với dev — như bài học của TinNhanh.
  • Đánh giá sức khỏe bộ test bằng hình dạng pyramid và ba chỉ số (thời gian chạy, tỷ lệ flaky, chi phí hạ tầng), không bằng số lượng E2E.
Học xong bài này rồi? Tạo tài khoản miễn phí để lưu lại — lần sau vào là biết ngay đang dở ở đâu, và học hết khóa thì có chứng chỉ. Lưu tiến độ của tôi