Flaky test thử thách lớn của test engineer

Flaky test thử thách lớn của test engineer

Xem nhanh

Flaky test là thách thức khó khăn mà developer hoặc test engineer cần vượt qua để gia tăng bộ kỹ năng cần có của mình trong lĩnh vực automated testing. Ngoài ra chính flaky test cũng làm giảm độ tin cậy của automated testing project.
Dù muốn hay không chúng ta cũng phải đối diện và xử lý nó, còn không, nó sẽ "knockout" project chúng ta bằng sự bỏ cuộc. Theo tôi, flaky test là nỗi ám ảnh của developer

1. Flaky test là gì?

Flaky test là một thuật ngữ trong lĩnh vực testing dùng để mô tả trường hợp cùng một test code nhưng kết quả chạy không ổn định: Thỉnh thoảng cho kết quả chạy failed, nhưng khi chạy lại kết quả chạy lại là passed.

Ví dụ: Tiến hành chạy test spec /address/address-book-shared/address-book-shared.spec.js nhiều lần, sẽ cho ra kết quả chạy khác nhau:

  • Passed:
[chrome 94.0.4606.71 Mac OS X #0-0] »/address/address-book-shared/address-book-shared.spec.js
[chrome 94.0.4606.71 Mac OS X #0-0] AddressBookShared
[chrome 94.0.4606.71 Mac OS X #0-0]    ✓ GRacceptance85_AddSharedAddressBook_001
[chrome 94.0.4606.71 Mac OS X #0-0]    ✓ GRacceptance86_AddSharedAddressEntry_002
[chrome 94.0.4606.71 Mac OS X #0-0]
[chrome 94.0.4606.71 Mac OS X #0-0] 2 passing (47.4s)


Spec Files:      1 passed, 1 total (100% completed) in 00:01:20 
  • Failed: ERR_PAGE_INTERACTION
[chrome 94.0.4606.71 Mac OS X #0-0] 
                ErrorCode: ERR_PAGE_INTERACTION
                Error: BaseElement->element: Error: element ("//a[contains(.,"Book Name - 565f-e699")]") still not displayed after 30000ms
                Cause: 
                Try: Try to check the https://onlinedemo2.cybozu.info/scripts/garoon/grn.exe/ testing site provided is working normally
                Selector: //a[contains(.,"Book Name - 565f-e699")]

Spec Files:      0 passed, 1 failed, 1 total (100% completed) in 00:01:53 

2. Những nguyên nhân có thể dẫn đến flaky test

Theo kinh nghiệm cá nhân, flaky test thường xuất hiện khi: Webdriver tương tác với WebElement không tồn tại ở thời điểm tương tác.
Điều này xảy ra bởi nhiều lý do, chẳng hạn như: thời gian phản hồi của server (web-server, app-server) chậm hơn mong đợi của chờ ngầm định của webdriver mà automated project sử dụng.

Bên dưới trình bày chi tiết hơn về một số nguyên nhân dẫn đến flaky test:

Nguyên nhân đến từ yếu tố bên ngoài

Là những yếu tố chi phối/ảnh hưởng đến kết quả automated testing mà chúng ta không thể kiểm soát hoặc can thiệp vào được. Trong đó, chúng ta thường thấy nhất là: Mạng máy tính chập chờn, loại máy tính khác nhau sử dụng trong việc xây dựng test runner (client types), và hạ tầng triển khai automated testing (infrastructure).

  • Network: Với tác yếu tố “đường truyền” thì developer/test engineer không thể can thiệp được. Dấu hiệu nhận biết cho network không ổn định là Element not found(ElementNotVisibleException) xuất hiện ở log message. Để giảm thiểu vấn đề này, chúng nên lựa chọn nhà cung cấp dịch vụ tin cậy và ổn định.
  • Client types: Để hạn chế flaky test bị tác động từ client types, chúng ta nên đưa ra thông số yêu cầu tin cậy để developer tham khảo triển khai test system.
    Ví dụ: Client dùng để triển khai Selenium Grid Chrome node phải có Disk 100 GB, Memory 10GB, Processor Quad-Core Intel Core i7, v.v.
  • Infrastructure: Chúng ta cố gắng sử dụng tối ưu hạ tầng đang có để tránh quá tải, leak memory, v.v.
    Chẳng hạn như: Gỡ bỏ hoàn toàn application đã cài đặt lên server sau khi đã hoàn thành testing. Xóa các files vật lý phát sinh trong quá trình testing như upload/download file giúp giải phóng bộ nhớ.
    Tổng quát hơn nên áp dụng tinh thần của: The boy scout rule

Nguyên nhân đến từ yếu tố bên trong

Là những yếu tố ảnh hưởng đến kết quả testing mà chúng thuộc về nội tại công ty hoặc đội nhóm phát triển. Hay nói cách khác, những yếu tố mà developer có thể xem xét để xử lý được. Thường có hai yếu tố như sau:

  • Web server: tốc độ phản hồi chậm, server quá tải, hoặc modern web-app sử dụng kỹ thuật bất đồng bộ (như ajax), v.v. Thậm chí dữ liệu bùng nổ trong quá trình testing có khả năng làm ứng dụng suy yếu, từ đó dẫn đến server phản hồi chập chờn.
  • Test data: có thể làm test case trở nên flaky test.
    Ví dụ: Garoon’s Schedule không cho phép tạo hai appointment cùng thời gian với một đối tượng facility Room A. Do đó lần chạy đầu tiên passed nhưng lần chạy thứ 2 sẽ cho kết quả failed (vì facility trùng thời gian sử dụng). Nên chúng ta sẽ xóa dữ liệu sau khi test xong để kết quả chạy những lần tiếp theo ổn định.

Nguyên nhân đến từ yếu tố khác

Kỹ năng viết code nói chung và test code nói riêng của developer cũng có thể là yếu tố tạo ra flaky test.

Để automated testing project hạn chế flaky test thì developer ưu tiên:
1. Ưu tiên sử dụng những thuật toán đơn giản trong implement (đơn giản là: developer nào cũng có thể đọc hiểu nó làm gì)
2. Ưu tiên sử dụng thư viện bên ngoài hỗ trợ nếu có (tốt nhất hỏi google trước khi quyết định implement)
Source code nhờ vậy cũng hướng đến dễ bảo trì như từng đề cập trong bài: test code trước tiên cho con người
3. Khai thác triệt để các built-in methods của mỗi framework hỗ trợ. Ví dụ: Các built-in trong JavaScript

Cách xác định flaky test case

Sau khi implement test code, chúng ta thực hiện chạy test với hai tiêu chí:

  1. Số lần chạy: Chạy nhiều lần trên cùng source code. Tốt nhất chúng ta sử dụng CI với crontab hoặc schedule của OS để tự động khởi chạy nhiều lần thay vì chúng ta chạy manual
  2. Thời điểm chạy: Chạy vào nhiều thời điểm khác nhau trong ngày. Thực tế cho thấy bug được phát hiện trong những thời điểm đặc biệt như 23:59, ngày cuối tháng, ngày cuối năm, timezone khác nhau, v.v. Nên chạy tại các thời điểm khác nhau giúp xác định flaky test

3. Giải pháp khắc phục flaky test

Mỗi một testing frameworks có cách hỗ trợ riêng để khắc phục flaky test. Do đó chúng ta đang sử dụng framework nào thì cố gắng áp dụng theo hướng dẫn của testing framework đó. Bên dưới là các cách mà tôi áp dụng cho Garoon automated testing xây dựng trên WebdriverIO frameworks.

Chờ ngầm định (Implicit wait)

Đây là một kỹ thuật đã hỗ trợ mặc định bởi WebDriver protocol. Nhưng mặc định giá trị này là 0 giây, tức là không chờ. Do đó nếu mỗi action method trong page object thực thi và tương tác với WebElement. WebElement này không tồn tại sẽ báo lỗi và kết thúc.

Như vậy chúng ta gia tăng thêm thời gian chờ ngầm định lên để gia tăng cơ hội WebElement xuất hiện trước khi tương tác. Liệu có ổn khi tăng lên không? tăng lên bao nhiêu? Rất khó khăn vì:\

  1. Phạm vi áp dụng là global (tức là giá trị này sẽ áp dụng cho tất cả WebElement chứ không riêng gì một WebElement nào)\
  2. Vô tình nó sẽ làm tổng thời gian testing tăng lên. Điều này trái ngược với sự mong đợt của automation là cho kết quả nhanh.

Vậy chúng ta thật sự cân nhắc khi thay đổi giá trị implicit wait này, tốt nhất không xài

Thông thường chúng ta sẽ wrapper lại testing frameworks mà chúng ta sử dụng thay vì chúng ta sử dụng trực tiếp.

e2e
|---core
|---src
|   |---wrapper
|   |   |---element
|   |   |   |---index.js
|   |   |   |---// ...

Path: e2e/core/src/wrapper/element/index.js

export const element = (selector, timeout = 30000) => {
    try {
        if (isWaitForDisplayed) {
            $(selector).waitForDisplayed({ timeout }); // Wrapper 
        }
        return $(selector);
    } catch (ex) {
        throw new OperationPageException({Status: 'Error', Class: 'BaseElement', Method: 'element', Exception: ex, Selector: selector,
        });
    }
};
// ...

Chờ rõ ràng (Explicit Waits)

Chỉ định chờ ở những logic/command mà chúng ta biết nó cần thiết lúc implement. Trong WDIO nó đã built-in những phương thức khá nhiều đó là:

waitForExists, waitForClickable, waitForDisplayed, waitForEnabled, v.v. Nói tổng quát là những phương thức bắt đầu với waitFor*

Như vậy tùy vào bối cảnh của logic mà chúng ta rõ ràng để để dụng một phương thức “waitFor*” phù hợp.

Bên dưới là một ví dụ chờ rõ ràng waitForExists mà WDIO đã built-in

it('sẽ xuất hiện một notification sau khi gửi message thành công', () => {
    const $form = element('message-form');
    const $notification = element('.notification');
    $form.element(".send").click();
    
    $notification.waitForExist({ timeout: 5000 }); // Chờ WebElement này xuất hiện tối đa là 5 giây, sau đó sẽ lỗi element not found
    
    expect($notification.getText()).to.be.equal('Data transmitted successfully!')
});

Với những command được hỗ trợ mặc định trong WDIO command này chúng ta sử dụng phù hợp lúc implement sẽ giúp giảm thiểu flaky test mà không bị hạn chế chỗ DƯ chỗ thiếu

Chờ linh hoạt (Fluent Waits)

Chờ cho tới khi thỏa điều kiện xảy ra (expected conditions) hoặc hết thời gian chờ hết thời gian tối đa chỉ định trước

browser.waitUntil(condition, { timeout, timeoutMsg, interval })

Sử dụng tính năng khởi chạy lại (Use Retry)

Sau khi đã áp dụng các cơ chế chờ ngầm định, chờ rõ ràng và chờ linh hoạt nhưng vẫn không thể giải quyết được flaky test. Có thể yếu tố bên ngoài (đường truyền) thì giải pháp retry có vẻ là giải pháp phù hợp. Trong đối sách cuối cùng này chúng ta hết sức cẩn thận và lưu ý dùng nó ở môi trường CI.

End-to-end(e2e) Cybozu Garoon đang sử dụng WDIO với test runner là mocha do đó tính năng retry được hỗ trợ khá linh hoạt như: từng hook riêng như before, beforeEach, , thậm chí từng it riêng và thậm chí là cả test suite.

  • Path: e2e/e2e.setting.global.js
export default {
// ...
    wdioConfig: {
    retryNum:
        process.env.NUMBER_RETRIES || 4,
        specFileNumberRetries: process.env.SPEC_FILE_NUMBER_RETRIES || 2,
  }
  // ...
}
  • Test spec: Bên dưới là ví dụ chúng ta chỉ định ở từng hook

    describe('Adding comment suite', () => {
        before('Add an appointment', () => {
            // ...
            }, WdioConfig.retryNum());
    
        it('Should be added a comment successul', () => {
            //
            }, WdioConfig.retryNum());
    
        after('Delete an appointment', () => {
            // ...
        })
    });

Chi tiết về tính năng retry vui lòng đọc bài: Khi sử dụng Retry, các hooks của Mocha hoạt động ra sao?

Áp dụng retry mức nào (suite, test case, v.v.) là phù hợp? hoặc chỉ định con số bao nhiêu là đủ? không ai có thể trả lời cho bạn một cách chính xác mà phụ thuộc vào mỗi project. ở Cybozu Vietnam thì chọn phương pháp “thử và sai” sau đó đưa ra NUMBER_OF_PROCESS là 4.
Như đã đề cập ở trên, chỉ nên bật ON tính năng retry ở thời điểm chạy trên CI, còn môi trường development tốt nhất là OFF tính năng này để phát hiện càng sớm càng tốt source code không ổn định. Retry có thể che dấu những test code vốn dĩ không ổn định.

Stop lại tức là Không implement test case đó

Cách đơn giản nhất đối diện với test case, test-spec, hay test suite flaky test là stop lại. Sau đó tùy vào tình hình mà mình có thể quyết định: Điều tra và tiếp tục theo đuổi khắc phục. Hoặc nó bị xóa ra khỏi github repository của automated test project

4. Giải pháp Cybozu’s UI-Less

Cybozu’s UI-Less là gì?

Là một thuật ngữ trong Cybozu Vietnam dùng để chỉ một phương pháp thực hiện việc implement test case mà: Chỉ tập trung tương tác UI vào những phần chính của test case; còn những phần phụ trợ; những phần khởi tạo data, v.v. Chúng ta dùng những giải pháp thay thế khác như web api, v.v.

Nguyên lý triển khai

Đối với e2e test case, chúng ta chỉ implement test code cho những bước chính cần xác định trên UI để đảm bảo implement đúng với testing mong đợi. Những bước khác trong kịch bản test, nếu xét thấy nó chỉ là những bước phụ trợ chúng ta ưu tiên dùng UI-Less

Độ ưu tiên sử dụng UI-Less như bên dưới:

  1. REST-API
  2. SOAP-API
  3. HTTP-Request

Ví dụ áp dụng thực tế thêm một một facility theo UI-Less với độ ưu tiên 3 (HTTP-request):

Giao diện thêm mới một facility của Garoon

Hình 2.0: Giao diện thêm mới một facility của Garoon

Phân bổ

e2e
|---core
|---src
|   |---schedule
|   |   |---adding-facility
|   |   |   |---index.js
|   |   |   |---add-facility.json
|   |   |   |---facility_add.json

Path: adding-facility/index.js

export const addingFacility = (credential, facility) => {
    uiLessAddFacility(`${__dirname}/add-facility.json`, `${__dirname}/facility_add.json`, {
            name: facility.name, code: facility.code, memo: facility.notes, fagid: facility.fagid,'web.user': credential.username,'web.password': credential.password,}, garoonUrl);

    return facility.name;
};

Path: adding-facility/add-facility.json

{
  "url": "schedule/system/command_facility_add",
  "httpMethod": "POST",
  "headers": {
  },
  "body": {
    "facilityName-def": "%%name%%",
    "facility_code": "%%code%%",
    "memo": "%%memo%%",
    "fagid": "%%fagid%%"
  }
}

Path: adding-facility/add-facility.json

{
  "url": "schedule/system/command_facility_add",
  "httpMethod": "POST",
  "headers": {
  },
  "body": {
    "facilityName-def": "%%name%%",
    "facility_code": "%%code%%",
    "memo": "%%memo%%",
    "fagid": "%%fagid%%"
  }
}

Path: adding-facility/facility_add.json

{
  "url": "schedule/system/facility_add",
  "httpMethod": "GET",
  "headers": {
  }
}

Tại nơi test spec sử dụng:

describe('Adding a repeating appointment', () => {
    before(() => {
        login(adminAccount);
        addFacility(adminAccount, facilityInfo);
    });
// ...
})

Ưu điểm

  • Giảm số lượng test case flaky test (giảm bao nhiêu % phụ thuộc quy mô, và độ phủ UI-Less).
  • Tốc độ thực thi nhanh, ít nhất là nhanh gấp đôi so với tương tác ui
  • Tốc độ implement nhanh, vì chỉ tập trung vào các bước chính của test case

Nhược điểm

  • Tốn cost để viết các ui-less component để có thể sử dụng.
  • Khó để tái hiện nếu component đó bị bug

Kết bài

E2E testing sớm muộn gì cũng xuất hiện flaky test. Hay nói cách khác flaky test là built-in của e2e project. Do đó developer cần trang bị mindset và khắc phục vì flaky test càng sớm càng tốt.
Flaky test có thể là nguyên nhân chính làm suy yếu độ tin cậy của mọi người đối với E2E testing project. Failed/unstable là kết quả người dùng cuối nhận được và đâu phải ai developer cũng có cơ hội giải thích.

Để hạn chế test case xuất hiện trong danh sách flaky test. Chúng ta KHÔNG nên sử dụng tính năng Retry của WDIO trong môi trường phát triển.

Các bài viết cùng chủ đề