Automated API Testing: Mảnh ghép lớn trong bức tranh Automated End-to-End Testing

Automated API Testing: Mảnh ghép lớn trong bức tranh Automated End-to-End Testing

Xem nhanh

Các loại APIs như: REST, SOAP thường được xây dựng rất bài bản và quy mô cho những sản phẩm chủ lực của công ty. Việc đảm bảo endpoints của API hoạt động đúng, tin cậy sau mỗi lần source code thay đổi là rất tốn kém vì phải test hết endpoints/URI bằng manual testing bằng các công cụ hỗ trợ.

Chính vì điều này, rất nhiều công cụ ra đời Postman, Assured, v.v. để hỗ trợ tự động hóa phần nào. Trong bài viết này tôi xin chia sẻ tổng thể cách triển khai REST API cho sản phẩm Garoon của Cybozu

1. API là gì? Vì sao cần Automated API testing?

API là viết tắt của của cụm từ Application Programing Interface. Trước khi định nghĩa chúng ta làm rõ thêm ngữ nghĩa từ API ở bối cảnh computer science

  1. A: Viết tắt của từ Application tiếng Việt là ứng dụng. Ví dụ: Zalo ứng dụng, Zalo web-app
  2. P: Viết tắt của từ Programing tiếng Việt là lập trình
  3. I: Viết tắt của từ Interface. Từ “interface” không thể hiểu theo cách thông thường là "giao diện" mà muốn đề cập những điều khoản/URIs công bố ra bên ngoài.

Vậy theo tôi, API là một thuật ngữ trong lĩnh vực khoa học máy tính mà nó mô tả một hình thức lập trình ứng dụng sử dụng những điều khoản (URIs) đã công bố của bên cung cấp Endpoint mà không quan tâm Endpoints làm gì bên trong, dựa trên những tiêu chuẩn đã được công nhận trước

Còn theo định nghĩa tại wikipedia

Ví dụ: Chúng ta sử dụng Zing mp3 API (được cấp phép) để viết một ứng dụng nghe nhạc cho riêng mình. Như vậy tôi sử dụng Zing mp3 API cung cấp và công bố (URI) để phát triển ứng dụng mà chúng ta không bao giờ biết Zing mp3 APIs làm gì, làm như thế nào bên trong.

Hình 1.0: Tổng quan hoạt động API

Hình 1.0: Tổng quan hoạt động APIs

Automated API testing là chúng ta implement mã nguồn để kiểm thử giúp đảm bảo API Endpoints cung cấp ra bên ngoài hoạt động chính xác thay vì chúng ta sử dụng manual testing để thực hiện.

2. Vì sao phải automated API testing?

Một điểm thú vị là API testing thuộc về end-to-end testing (e2e) khi automated API testing chúng sẽ có 3 điểm nổi trội so với automated các loại test khác trong danh mục e2e: (1) Ổn định hơn hẳn UI testing, (2) thời gian thực thi testing ngắn, (3) triển khai đơn giản.
Ngoài lý do kể trên, theo sơ đồ Pyramid testing thì cũng khuyến cáo chúng ta gia tăng automated API testing test case càng nhiều càng tốt nói chung số test case đã implemented phải lớn hơn UI testing. Chúng ta nên ưu tiên triển khai API testing hơn so với UI testing.

Hơn nữa, thông thường một tính năng sản phẩm phần mềm: Phần UI (sử dụng bởi end-user) và URI của API cũng chung một logic (business/controller layer) xử lý. Nên 1 URI triển khai automated testing sẽ giúp giảm thiểu 80% test case cần automated của tính năng đó ở UI testing. Điều này giúp giảm cost trong quá trình implement acceptance testing rất nhiều.

Hình 2.0: Mô hình Pyramid testing

Hình 2.0: Mô hình Pyramid testing

3. Điểm khác biệt chính giữa UI testing và API testing

Ở phần này chúng ta điểm qua một số điểm khác biệt chính của UI testing và API testing để làm tiền đề cho lý do vì sao nên automated API testing. Và các điểm liệt kê ở đây cũng chỉ mang tính chất tương đối

  1. API testing có ưu điểm chạy nhanh, ổn định hơn UI testing. Thí dụ: 57 test case của Garoon REST API chạy chỉ mất 2 phút 28 giây trong khi một acceptance testing test case trung bình là 60 giây
  2. UI testing end-user tương tác với ứng dụng ở mức độ client (Cài đặt và sử dụng ứng dụng) thông qua GUI với một kịch bản được xác định trước.
    API testing end-user tương tác với ứng dụng ở mức độ server (Gửi request lên API server) thông qua công cụ hỗ trợ như Postman để gửi request hoặc test code.
  3. API testing ít phụ thuộc lẫn nhau giữa các thành phần. Vì mỗi URI xử lý đúng một việc duy nhất do đó không phụ thuộc với bất kỳ URI nào.
    UI testing phụ thuộc cao giữa các thành phần của ứng dụng. Vì UI test case hoạt động theo kịch bản end-user do đó trong kịch bản có sự tham gia của nhiều thành phần do đó nó phụ thuộc lẫn nhau.

    Ví dụ: Test case thêm mới một đối tượng appointment của module Schedule trong ứng dụng Garoon có hai thành phần: Facility, và appointment. --> chỉ cần facility không thêm thành công lúc test dẫn đến không thể thêm Appointment.
  4. API testing ít tốn cost hơn so với UI testing
    Ít tốn cost hơn bao nhiêu? Để có con số chính xác hơn 50%, 70%, v.v. Nó phụ thuộc vào mỗi tổ chức, cá nhân cũng như thời gian trải nghiệm trên project. Cá nhân tôi từng đo lường thông thường implemented API testing ít cost hơn 70% cost so với UI testing.
  5. API testing chạy ổn định hơn so với UI testing
    Lý do là gì? Chắc là bản chất của UI testing, xem thêm bài flaky test để có thể có nhiều thông tin hơn.

4. Cách tổ chức source code cho phù hợp với API testing

Trong bài này chúng ta sẽ mô tả automated testing Rest API với WebdriverIO testing framework. URI của REST API được cung cấp bởi Garoon’s REST API

Sơ đồ tổng quan E2E automated project

Hình 3.0: Tổ chức REST API trong e2e project

Hình 3.0: Tổ chức REST API trong e2e project

Nếu chúng ta một khi đã quyết tâm đeo đuổi automated E2E testing một cách nghiêm túc thì việc tìm hiểu và sử dụng một "right structure" source code phù hợp là điều xứng đáng đầu tư :)


Như Hình 3.0 ở trên, REST API sử dụng tài nguyên của E2E core lẫn tài nguyên của các loại test khác như Acceptance testing SOAP API, lẫn Cybozu's JS API . Tổ chức đúng và nguyên lý chia sẻ tài nguyên có thể giúp việc triển khai REST API trở nên đơn giản hơn, ổn định, giảm cost.

Lưu ý thêm: REST API không làm việc trực tiếp với các loại test khác mà phải thông qua một facade pattern

Cấu trúc E2E và tổ chức source code

Trong loạt bài trước cũng từng đề cập về E2E project. Trong bài này chúng ta tập trung mô tả REST API là chính. Bên dưới REST API testing tổng thể như sau:

Copy
e2e
|---e2e-core
|   |---src
|   |   |---scheduler
|   |   |---mail
|   |   |---//...
|   |---.babelrc
|   |---mobile-view
|   |   |---src
|---rest-api
|   |---src
|   |   |---rest-core
|   |   |---shared
|   |   |   |---external-service
|   |   |   |   |---index.js // This file import and re-export other service for the rest api uses
|   |   |---schedule
|   |   |   |---event
|   |   |   |   |---POST
|   |   |   |   |   |---typical-regular-event
|   |   |   |   |   |   |---typical-regular.request.data.js|.expected.data.js|.spec.js
|   |   |   |   |   |   |---helper.js
|   |   |   |   |   |---private-regular-event
|   |   |   |   |   |   |---private-regular.request.data.js|.expected.data.js|.spec.js|.helper.js
|   |   |   |   |   |   |---helper.js
|   |   |   |   |   |---// ...
|   |   |   |   |---GET
|   |   |   |   |   |---typical-regular-event
|   |   |   |   |   |   |---typical-regular.request.data.js|.expected.data.js|.spec.js|.helper.js
|   |   |   |   |   |---// ...
|   |   |   |   |---DELETE
|   |   |   |   |   |---typical-regular-event
|   |   |   |   |   |   |---typical-regular.request.data.js|.expected.data.js|.spec.js|.helper.js
|   |   |   |   |   |---// ...
|   |---.babelrc
|   |---wdio.conf.js|.prepared.conf.js
|   |---setting.global.js
|---acceptance
|   |---schedule
|   |   |---test-specs
|   |   |   |---added-new-appoiment
|   |   |   |   |---added-new-appointment.spec.js|.data.js
|---package.json
|---wdio.conf.js
|---webpack.config.js
|---.gitignore
// ...

Triển khai automation REST API testing

Chúng ta sử dụng một số kỹ thuật và thông tin yêu cầu như bên dưới:

Các bước tiến hành

Bước 1: Phát sinh folder structure với tên folder phản ánh nghiệp vụ cần test như bên dưới

Copy
// ...
|   |   |---schedule                           // module mà test case thuộc về
|   |   |   |---event                          // đối tượng mà endpoint cần test
|   |   |   |   |---POST                       // HTTP method sử dụng
|   |   |   |   |   |---typical-regular-event  // nghiệp vụ cần automated
// ...

Bước 2: Phát sinh các *.js file tham gia vào quá trình testing

Copy
|   |   |---schedule
|   |   |   |---event
|   |   |   |   |---POST
|   |   |   |   |   |---typical-regular-event
|   |   |   |   |   |   |---request.data.js       // file chứa các key/value tương ứng để gửi lên server
|   |   |   |   |   |   |---expected.data.js      // file chứa các key/value dùng để đối chiếu kết quả server trả về
|   |   |   |   |   |   |---test.spec.js          // file chứa js logic để thực hiện nghiệp vụ test
|   |   |   |   |   |   |---helper.js             // file chứa các js function giúp test.spec.js sử dụng, mục đích che đi sự phức tạp ở .spec.js

Bước 3: Triển khai logic nghiệp vụ tương ứng từng *.js files đã khai báo ở Bước 2

Tổng quan REST API cần 3 js file: 1 js file chứa data test request lên server; 1 js file để xác minh kết quả REST Server trả về và 1 js file chứa kịch bản test.

File request chứa data test: schedule/events/POST/typical-regular-event/request.data.js

Copy
export default {
    url: 'api/v1/schedule/events',
    httpMethod: 'POST',
    headers: {
        'X-Cybozu-Authorization': '%%login_user_password_base64%%',
        accept: '*',
    },
    body: {
        eventType: 'REGULAR',
        eventMenu: 'PLAN',
        subject: 'DETAIL',
        notes: 'DESCRIPTION',
        isAllDay: false,
        isStartOnly: false,
        attendees: [{id: '2', name: 'Organization10', type: 'ORGANIZATION', code: 'Organization10',
            },{
                id: '3',
                name: 'user03',
                type: 'USER',
                code: 'user03',
            },
        ],
        start: {
            dateTime: '2021-10-01T09:00:00+09:00',
            timeZone: 'Asia/Tokyo',
        },
        end: {
            dateTime: '2021-10-01T10:00:00+09:00',
            timeZone: 'Asia/Tokyo',
        },
        originalStartTimeZone: 'Asia/Tokyo',
        originalEndTimeZone: 'Asia/Tokyo',
        facilities: [
            {
                id: '2',
                name: 'Facility1-2',
                code: 'Facility1-2',
            },
        ],
        visibilityType: 'PUBLIC',
        useAttendanceCheck: false,
    },
    json: true,
};

File chứa data để xác minh kết quả trả về: rest-api/src/schedule/event/POST/typical-regular-event/expected.data.js

Copy
export default {
    statusCode: '201',
    body: {
        eventType: 'REGULAR',
        eventMenu: 'PLAN',
        subject: 'DETAIL',
        notes: 'DESCRIPTION',
        isAllDay: false,
        isStartOnly: false,
        attendees: [
            {
                id: '2',
                name: 'Organization10',
                type: 'ORGANIZATION',
                code: 'Organization10',
            },
            {
                id: '3',
                name: 'user03',
                type: 'USER',
                code: 'user03',
            },
        ],
        companyInfo: {
            name: '',
            zipCode: '',
            address: '',
            route: '',
            routeTime: '',
            routeFare: '',
            phone: '',
        },
        attachments: [],
        start: {
            dateTime: '2021-10-01T09:00:00+09:00',
            timeZone: 'Asia/Tokyo',
        },
        end: {
            dateTime: '2021-10-01T10:00:00+09:00',
            timeZone: 'Asia/Tokyo',
        },
        originalStartTimeZone: 'Asia/Tokyo',
        originalEndTimeZone: 'Asia/Tokyo',
        facilities: [
            {
                id: '2',
                name: 'Facility1-2',
                code: 'Facility1-2',
            },
        ],
        visibilityType: 'PUBLIC',
        additionalItems: {
            item: {
                value: '',
            },
        },
    },
};

File chứa kịch bản test: rest-api/src/schedule/event/POST/typical-regular-event/test.spec.js. Trong file này chúng ta imported hai đối tượng SoapApi( của UI-LESS), RestApi

Copy
import SoapApi from '#rest_api/rest-core/SoapApi';
import RestApi from '#rest_api/rest-core/RestApi';

describe('POST schedule/events', () => {
    let replaceMap = {'web.user': 'brown', 'web.password': 'brown',};
    let event_id;

    it('Post a regular event (typical)', () => {
        event_id = runTest(replaceMap, replaceMap);
        const requestReplaceMap = replaceMap;
        const responseReplaceMap = replaceMap;
        const request = require(`${__dirname}/request.data.js`);
        const response = require(`${__dirname}/expected.data.js`);

        if (parseInt(response.statusCode, 10) === 201) {
            const restApi = new RestApi(request, requestReplaceMap)
                .execute()
                .verifyInCaseOfStatusCode(response.statusCode)
                .verifyInCaseOfSuccess(response.body, responseReplaceMap);
            return restApi.getResponse().body.id;
        }

        new RestApi(request, requestReplaceMap)
            .execute()
            .verifyInCaseOfStatusCode(response.statusCode)
            .verifyInCaseOfFailed(response.body, responseReplaceMap);
    });

    after(() => {
        replaceMap = Object.assign(replaceMap, { event_id: event_id });
        new SoapApi(`${__dirname}/remove_event.xml`, replaceMap).execute(); // Sử dụng UI-less để xử lý một số nghiệp vụ phụ trợ
    });
});

Bước 4: Đăng ký test spec (test.spec.js) vừa implement xong vào test suite của WDIO

File: rest-api/conf/wdio.conf.js

Copy
const merge = require('deepmerge');
const mainConfig = require('./../wdio.conf.js');

const DONT_MERGE = (destination, source) => source;
exports.config = merge(
    mainConfig.config,
    {
        capabilities: [
            {
                browserName: 'chrome',
                'goog:chromeOptions': {
                    binary: process.env.CHROME_BIN,
                    args: ['--headless'],
                },
                maxInstances: 1,
            },
        ],
        specs: ['rest-api/src/**/test.spec.js'],
        suites: {
            schedule: ['rest-api/src/schedule/**/*.spec.js'], // Bổ sung test spec ở đây
        },
    },
    { arrayMerge: DONT_MERGE }
);

Bước 5: Chạy test script thông qua npm test đã viết và xem kết quả. Sử dụng terminal yêu thích và thực hiện lệnh như sau:

npm test rest-api/conf/wdio.conf.js -- --spec rest-api/src/schedule/events/POST/typical-regular-event/test.spec.js

  • Khi passed log message như bên dưới

    Copy
    [chrome 93.0.4577.63 linux #0-0] » /rest-api/src/schedule/events/POST/typical-regular-event/test.spec.js
    [chrome 93.0.4577.63 linux #0-0] POST schedule/events
    [chrome 93.0.4577.63 linux #0-0]    ✓ Post a regular event (typical)
    [chrome 93.0.4577.63 linux #0-0]
    [chrome 93.0.4577.63 linux #0-0] 1 passing (2.2s)
    
    Spec Files:      1 passed, 1 total (100% completed) in 00:00:14 
  • Như vậy source code hoàn thành các bước trên như sau.

Để đảm bảo độ dài của bài tech-blog cũng như trong phạm vi mô tả dạng prototype nên có thể bạn đọc còn nhiều chỗ thắc mắc, khó hiểu khi đọc bài này. Hãy gửi bất kỳ mối bận tâm nào của bạn cho tác giả để được giải thích thêm.

Service Dependencies

Xem kỹ chúng ta vẫn chưa thấy 2 dependencies đã import ởtest.spec.js có gì bên trong:

Copy
import SoapApi from '#rest_api/rest-core/SoapApi';
import RestApi from '#rest_api/rest-core/RestApi';

RestApi, chúng ta sẽ implement nghiệp vụ: thực thi requestverify kết quả REST Server trả về, bên dưới là prototype

Copy
export default class RestApi {
    constructor(request, replaceMap) {
        // ...
        this._isUsedProxy = global.setting.process.isUsedProxy;
    }
    execute(url = global.setting[product].web.url) {
        // ...
        const requestOptions = {
            url: request.url,
            headers: request.headers,
            json: true,
        };
        const requestBody = {
            body: request.body,
            json: true,
        };
        switch (httpMethod) {
            case 'GET':
                response = api.get(requestOptions);
                break;
            case 'POST':
                response = api.post(requestOptions, requestBody);
                break;
            case 'PUT':
                response = api.put(requestOptions, requestBody);
                break;
            case 'PATCH':
                response = api.patch(requestOptions, requestBody);
                break;
            case 'DELETE':
                response = api.delete(requestOptions);
                break;
            default:
                throw new Error(
                    `${logPrefix} [ Invalid request method: ${httpMethod} ]`
                );
        }
        // ...
    }
    verifyInCaseOfStatusCode(compareParams, replaceMap) {
        // ...
        return this;
    }
    verifyInCaseOfSuccess(compareParams, replaceMap) {
        // ...
        return this;
    }
    // ...
}

Kết bài

REST API là một kiểu kiến trúc phần mềm phổ biến và việc triển khai automated testing là cần thiết do đó nên cân nhắc để đầu tư.
Điều này cũng giống như chúng ta “đào một dòng kênh thay vì đi gánh từng thùng nước vậy”. Ban đầu, nó tốn chi phí lớn và chưa có kết quả tức thì như manual testing hoặc dùng tool như Postman.
Nhưng về lâu dài, automated REST API testing mang lại rất nhiều giá trị cho các bên, công ty, QA, Developer như: giải phóng sức người, giành thời gian để skill-up tech, dev triển khai automated giúp nâng cao kỹ năng automated testing, automated giúp đảm bảo URIs của công ty hoạt động chính xác, tăng độ tin cậy.

Triển khai automated REST API testing nói riêng và APIs nói chung có nhiều cách, nhiều kiến trúc lẫn library hỗ trợ. Nên nếu gọi là triển khai automated API hiệu quả thì theo tôi nó đảm bảo ba yếu tố: Chạy ổn định, dễ dàng maintain source code, mở rộng automated API REST testing mà không đập bỏ làm lại từ đầu

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