Product Management
Đăng nhập
ESC

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

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

Bài 9 — Appium — Viết test login flow đầu tiên

Đã đọc 0/13 mục · còn khoảng 10 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 ba bài trước là "học lý thuyết và dựng sân khấu", thì đây chính là buổi biểu diễn đầu tiên của bạn. Sau khi đã hiểu kiến trúc Appium (Bài 7) và nắm được các chiến lược tìm phần tử — locator strategies (Bài 8), bây giờ chúng ta ghép mọi thứ lại thành một test flow hoàn chỉnh, chạy được, thất bại được, và pass được. Và không có kịch bản nào kinh điển hơn để bắt đầu bằng login flow — luồng đăng nhập.

Tại sao lại là login? Vì gần như mọi ứng dụng mobile thương mại đều bắt đầu bằng nó: từ ví MoMo, ứng dụng ngân hàng Techcombank, đến app đặt xe Grab hay app học tiếng Anh ELSA. Login là "cửa ngõ" — nếu người dùng không đăng nhập được, họ không dùng được gì cả. Trong thực tế đội QA, login là test case đầu tiên được tự động hóa và cũng là smoke test (kiểm tra sức khỏe cơ bản) chạy trước mỗi lần build. Một khi bạn viết được test login từ đầu đến cuối, bạn đã nắm được toàn bộ vòng đời của một Appium test: khởi tạo session → tìm phần tử → nhập liệu → tương tác → xác nhận (assertion) → dọn dẹp. Mọi test phức tạp hơn chỉ là mở rộng của khung xương này.

Bài này tập trung viết một test login đầu tiên bằng JavaScript + WebdriverIO — bộ đôi phổ biến nhất trong cộng đồng Appium ngày nay. Chúng ta sẽ không lan sang phần xử lý chờ đợi phức tạp (waits/sync — dành cho Bài 10), mà chỉ dùng vừa đủ để test chạy ổn định.

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

Cấu trúc một test flow là gì?

Một test tự động, dù đơn giản đến đâu, luôn tuân theo mô hình AAA — Arrange, Act, Assert (Chuẩn bị, Hành động, Kiểm chứng):

  • Arrange (Chuẩn bị): Khởi tạo driver, mở app, đảm bảo màn hình đăng nhập đã sẵn sàng.
  • Act (Hành động): Nhập email, nhập mật khẩu, chạm nút Login.
  • Assert (Kiểm chứng): Xác nhận rằng màn hình Home đã hiện ra — nghĩa là đăng nhập thành công.
Nếu thiếu bước Assert, bạn không có test — bạn chỉ có một con robot bấm nút. Assert là linh hồn của test: nó biến "app không crash" thành "app hoạt động đúng như kỳ vọng".

WebdriverIO và Appium làm việc cùng nhau thế nào?

WebdriverIO (viết tắt là wdio) là một test framework đóng vai trò client. Nó gửi lệnh theo chuẩn W3C WebDriver đến Appium Server, Appium dịch các lệnh này thành thao tác thật trên thiết bị (thông qua XCUITest cho iOS hoặc UiAutomator2 cho Android). Bạn viết code JavaScript, wdio lo phần giao tiếp.

Điểm mạnh của WebdriverIO là API rất "người": $('~loginButton') để tìm phần tử, .setValue('...') để nhập, .click() để chạm. Dấu ~ (tilde) là cú pháp tắt của WebdriverIO để tìm theo accessibility id — cách tìm phần tử ổn định nhất mà chúng ta đã bàn ở Bài 8.

Capabilities — tấm "chứng minh thư" của session

Trước khi test chạy, WebdriverIO cần biết: chạy trên nền tảng nào, thiết bị nào, app nào. Thông tin này gọi là capabilities. Ví dụ cho Android:

const capabilities = {
  platformName: 'Android',
  'appium:automationName': 'UiAutomator2',
  'appium:deviceName': 'Pixel_6_API_33',
  'appium:app': '/Users/qa/app/momo-staging.apk',
  'appium:appPackage': 'com.mservice.momotransfer',
  'appium:appActivity': '.activity.SplashActivity',
  'appium:autoGrantPermissions': true,
};

Đây chính là điểm khác biệt lớn nhất so với web testing: bạn phải mô tả rõ môi trường thiết bị. autoGrantPermissions: true là mẹo nhỏ nhưng quý — nó tự động cấp quyền (thông báo, vị trí...) để hộp thoại xin quyền không chắn ngang test của bạn.

Tình huống thực tế

Ví dụ 1 — Đội QA của một fintech Việt Nam và smoke test 30 giây

Một công ty fintech ở TP.HCM (giả định tên Napas Pay, đội QA 6 người) phát hành app ví điện tử mỗi tuần hai lần. Trước khi bài này được viết, mỗi lần có build mới, một bạn tester phải cầm điện thoại đăng nhập thủ công để kiểm tra "app còn vào được không". Việc này tốn khoảng 5 phút mỗi lần, và tệ hơn: có lần build lỗi cấu hình API, login hỏng, nhưng không ai kiểm tra kịp nên bản lỗi lọt ra tận vòng UAT, gây trễ 1 ngày.

Sau khi đội viết test login đầu tiên bằng WebdriverIO, họ đưa nó vào pipeline như một smoke test chạy tự động sau mỗi build. Test chỉ mất khoảng 30 giây: mở app, nhập tài khoản test 0900000001 / Test@123, chạm "Đăng nhập", xác nhận số dư ví hiển thị trên Home. Nếu test đỏ, build bị chặn ngay.

Bài học rút ra: Test login đầu tiên không cần phức tạp. Giá trị của nó nằm ở chỗ chạy tự động và chạy sớm. Một test 30 giây thay thế cho 5 phút thao tác tay và loại bỏ được lỗi con người "quên kiểm tra".

Ví dụ 2 — Assertion sai chỗ khiến test "xanh giả"

Một bạn QA junior tại một startup edtech ở Hà Nội viết test login cho app học tiếng Anh. Test của bạn ấy như sau: nhập email, nhập mật khẩu, chạm Login, rồi kết thúc — không có assertion. Test luôn xanh (pass). Đội mừng rỡ đưa vào CI.

Hai tuần sau, có một bug: sau khi login, do lỗi điều hướng, app đứng ở màn hình loading vô tận. Nhưng test vẫn... xanh — vì nó chỉ chạm nút Login rồi dừng, chẳng kiểm tra gì cả. Đây gọi là false positive (xanh giả): test báo ổn trong khi app hỏng.

Cách sửa: thêm assertion kiểm chứng một phần tử chỉ xuất hiện khi login thành công — ví dụ nút "Bắt đầu học" trên Home, hoặc lời chào "Xin chào, [tên]". Sau khi thêm expect(await homeGreeting.isDisplayed()).toBe(true), test bắt đúng bug ngay lần chạy kế tiếp.

Bài học rút ra: Không có assertion thì không có test. Và assertion phải nhắm vào phần tử đặc trưng cho trạng thái thành công, không phải phần tử chung chung có thể tồn tại ở nhiều màn hình.

Ví dụ 3 — Hardcode tài khoản thật và sự cố rò rỉ

Tại một team QA thuê ngoài (outsourcing) làm cho khách hàng Singapore, một tester viết test login và... nhét thẳng email + mật khẩu tài khoản admin thật vào code, rồi commit lên GitHub. Repository là private, nhưng khi một nhân sự nghỉ việc, tài khoản đó vẫn còn quyền và mật khẩu vẫn nằm trong lịch sử git.

May mắn không có sự cố nghiêm trọng, nhưng đội đã phải đổi toàn bộ mật khẩu và dọn lịch sử git — một buổi chiều căng thẳng. Từ đó họ đặt ra quy tắc: thông tin đăng nhập test luôn đọc từ biến môi trường (environment variables) hoặc file .env không được commit, và chỉ dùng tài khoản test riêng trên môi trường staging, tuyệt đối không dùng tài khoản người dùng thật.

Bài học rút ra: Ngay từ test đầu tiên, hãy hình thành thói quen bảo mật. Dùng process.env.TEST_EMAIL thay vì viết thẳng chuỗi. Đây là chi tiết nhỏ nhưng phân biệt một QA nghiệp dư với một QA chuyên nghiệp.

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

Giả sử bạn đã cài Appium Server, WebdriverIO và có sẵn thiết bị/emulator từ Bài 7. Ta viết một test độc lập, dễ hiểu.

Bước 1 — Khởi tạo session và capabilities. Tạo file login.test.js:

const { remote } = require('webdriverio');

const capabilities = {
  platformName: 'Android',
  'appium:automationName': 'UiAutomator2',
  'appium:deviceName': 'Pixel_6_API_33',
  'appium:app': process.env.APP_PATH,
  'appium:autoGrantPermissions': true,
};

const wdOpts = {
  hostname: 'localhost',
  port: 4723,
  logLevel: 'info',
  capabilities,
};

Bước 2 — Viết luồng chính. Bọc mọi thứ trong một hàm async vì các lệnh Appium đều bất đồng bộ (asynchronous) — luôn dùng await:

async function runLoginTest() {
  const driver = await remote(wdOpts);
  try {
    // Arrange: chờ màn hình login sẵn sàng
    const emailField = await driver.$('~emailInput');
    await emailField.waitForDisplayed({ timeout: 10000 });

    // Act: nhập email + password
    await emailField.setValue(process.env.TEST_EMAIL);
    const passwordField = await driver.$('~passwordInput');
    await passwordField.setValue(process.env.TEST_PASSWORD);

    // Act: chạm nút Login
    const loginButton = await driver.$('~loginButton');
    await loginButton.click();

    // Assert: kiểm tra Home screen hiện ra
    const homeGreeting = await driver.$('~homeGreeting');
    await homeGreeting.waitForDisplayed({ timeout: 15000 });
    const isVisible = await homeGreeting.isDisplayed();

    if (isVisible) {
      console.log('PASS: Đăng nhập thành công, Home screen hiển thị.');
    } else {
      throw new Error('FAIL: Không tìm thấy Home screen sau khi login.');
    }
  } finally {
    // Cleanup: luôn đóng session dù pass hay fail
    await driver.deleteSession();
  }
}

runLoginTest();

Bước 3 — Chuẩn bị dữ liệu qua biến môi trường. Chạy test kèm biến:

APP_PATH=/Users/qa/app/staging.apk \
TEST_EMAIL=qa.tester@example.com \
TEST_PASSWORD='Staging@2026' \
node login.test.js

Bước 4 — Đảm bảo Appium Server đang chạy. Ở một terminal khác: appium. WebdriverIO sẽ kết nối tới localhost:4723.

Bước 5 — Chạy và quan sát. Nhìn màn hình emulator: bạn sẽ thấy email được điền, mật khẩu được điền, nút Login được chạm, và Home hiện ra. Console in ra PASS. Chúc mừng — bạn vừa hoàn thành test Appium đầu tiên!

Bước 6 (khuyến nghị) — Nâng lên dùng test framework. Trong dự án thật, người ta hiếm khi viết if/else thủ công. Ta dùng cấu trúc describe/it với thư viện assertion:

describe('Login flow', () => {
  it('nên đăng nhập thành công với tài khoản hợp lệ', async () => {
    await driver.$('~emailInput').setValue(process.env.TEST_EMAIL);
    await driver.$('~passwordInput').setValue(process.env.TEST_PASSWORD);
    await driver.$('~loginButton').click();
    const home = await driver.$('~homeGreeting');
    await expect(home).toBeDisplayed();
  });
});

Cách này cho báo cáo đẹp, gom nhiều test và tái sử dụng cấu hình — nhưng bản chất vẫn là AAA như trên.

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

"NoSuchElementError" ngay lệnh đầu tiên. Nguyên nhân phổ biến nhất: app chưa kịp vẽ xong màn hình, bạn đã đi tìm phần tử. Đừng vội đổ lỗi cho locator — hãy thêm waitForDisplayed như ở Bước 2. (Chi tiết về waits sẽ ở Bài 10, nhưng riêng test login thì waitForDisplayed là tối thiểu bắt buộc.)

Locator sai vì đoán mò. Đừng tự nghĩ ra ~loginButton. Hãy mở Appium Inspector để xem đúng accessibility id / resource-id mà lập trình viên đã gán. Nếu app chưa có accessibility id, hãy đề nghị dev thêm — đây là "hợp đồng" quan trọng giữa QA và dev, giúp test bền vững hơn nhiều so với bám vào XPath.

Bàn phím ảo che nút Login. Trên nhiều app, sau khi nhập mật khẩu, bàn phím vẫn hiện và che nút Login khiến .click() chạm nhầm. Mẹo: gọi await driver.hideKeyboard() trước khi chạm Login (lưu ý API này ổn định trên Android hơn iOS).

Quên await. Đây là lỗi kinh điển của người mới với JavaScript async. Thiếu await, lệnh chưa chạy xong đã sang lệnh sau, gây lỗi khó hiểu và test flaky (chập chờn). Quy tắc: mọi lệnh Appium đều phải có await.

Quên đóng session. Nếu test lỗi giữa chừng mà không deleteSession(), session Appium bị treo, lần chạy sau có thể xung đột. Luôn đặt deleteSession() trong khối finally.

Hardcode mật khẩu. Như Ví dụ 3 — đừng bao giờ. Dùng process.env.

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

  • Viết test login pass: Với app mẫu bất kỳ (hoặc app staging của công ty bạn), viết một test login hoàn chỉnh theo mô hình AAA. Dùng accessibility id lấy từ Appium Inspector, đọc email/password từ biến môi trường, và assert vào một phần tử đặc trưng của Home screen.
  • Viết test login fail (negative test): Tạo test thứ hai nhập mật khẩu SAI. Assert rằng thông báo lỗi ("Sai mật khẩu") xuất hiện và Home screen không hiện. Đây là bước tập nghĩ như một tester thực thụ: kiểm chứng cả trường hợp thất bại.
  • Refactor bảo mật: Rà lại code test của bạn, đảm bảo không có chuỗi email/mật khẩu nào bị viết thẳng. Chuyển hết sang process.env và tạo file .env.example (không chứa giá trị thật) để đồng đội biết cần biến gì.
  • Suy ngẫm: Nếu chỉ được viết MỘT assertion cho test login, bạn sẽ chọn kiểm phần tử nào của Home screen? Vì sao phần tử đó "đáng tin" hơn các phần tử khác? Viết 3–4 câu giải thích.

Tóm tắt

Test login đầu tiên là cột mốc quan trọng vì nó dạy bạn toàn bộ khung xương của một Appium test: khởi tạo session với capabilities → tìm phần tử bằng accessibility id → nhập liệu và tương tác → assert vào trạng thái thành công → dọn dẹp session. Mọi test phức tạp về sau chỉ là mở rộng khung này. Ba nguyên tắc cần khắc cốt ghi tâm: (1) không có assertion thì không có test — và assertion phải nhắm vào phần tử đặc trưng cho login thành công; (2) luôn await mọi lệnh và luôn deleteSession() trong finally; (3) không bao giờ hardcode thông tin đăng nhập — dùng biến môi trường và tài khoản test staging. Với bộ đôi JavaScript + WebdriverIO, bạn đã có công cụ đủ mạnh để tự động hóa luồng quan trọng nhất của mọi app. Ở Bài 10, chúng ta sẽ đào sâu vào waits và sync để test của bạn hết chập chờn — nhưng nền móng thì bạn đã đặt xong ngay từ hôm nay.

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