Upload file: Làm thế nào để thực hiện tự động hóa ?

Upload file: Làm thế nào để thực hiện tự động hóa ?

Xem nhanh

Upload file là một tính năng quan trọng phổ biến trong các ứng dụng web hay di động hiện nay. Vì vậy khi thực hiện tự động hóa (automation testing) cho các sản phẩm này, việc hiện thực cho các test case upload file cũng được quan tâm.

Bài viết này chúng ta sẽ tìm hiểu về một số đặc điểm của file upload testing cũng như cách để thực hiện automation testing cho các test case upload file.

Để bài viết được cụ thể, các ví dụ về upload file trong phần nội dung sẽ được viết dựa trên sản phẩm Garoon, là sản phẩm hỗ trợ làm việc nhóm của Cybozu.

Đặc điểm của file upload testing

Như đã đề cập ở phần mở đầu, hiện nay tính năng upload file rất phổ biến trong các ứng dụng phần mềm. Và Garoon cũng không ngoại lệ. Vì là sản phẩm hỗ trợ làm việc nhóm nên nhu cầu chia sẻ dữ liệu rất cao. Trên Garoon có rất nhiều tính năng có liên quan đến upload file. Hình ảnh dưới đây là một ví dụ điển hình: người dùng có thể upload file lên Garoon thông qua ứng dụng quản lý file.

Chức năng quản lý file upload trên Garoon, một sản phẩm của Cybozu

Hình 1.0: Chức năng quản lý file upload trên Garoon

Nhìn chung với upload file của Garoon, manual testing phải thực hiện khá nhiều test case để đảm bảo chức năng này hoạt động đúng. Tuỳ theo yêu cầu của sản phẩm mà kịch bản test upload file sẽ khác nhau. Một số kịch bản test thông thường cho file upload testing:

  • Kiểm tra upload file cho các định dạng file khác nhau: .doc, .txt, .png, .pdf, .csv, v.v

  • Kiểm tra upload file không cho phép với dung lượng file vượt quá giới hạn tối đa.
    Ví dụ: Không cho phép upload file vượt quá 2 MB

  • Kiểm tra tính năng upload nhiều file

  • Kiểm tra chọn file upload bằng drag and drop

  • Kiểm tra UI của upload file hoạt động đúng hay không
    Ví dụ:

    • Khi click vào upload button cửa sổ chọn file mở ra không?
    • Progress bar có hoạt động sau khi chọn file không?

Đối với automation testing, chúng ta cần nắm rõ các đặc điểm của file upload testing để hiện thực đúng và phù hợp với hành vi manual testing thực hiện. Phần tiếp theo trong bài viết, chúng ta sẽ tìm hiểu cụ thể hơn về cách hiện thực cho upload file trong automation testing.

Hiện thực automation testing cho upload file

Công nghệ sử dụng: hiện tại chúng tôi đang sử dụng WebdriverIO để thực hiện automation testing cho Garoon. Do đó, phần source code dưới đây được viết dựa trên framework này.

Mô tả test case: Hình 1.0 có để cập đến chức năng quản lý file upload trên Garoon. Phần hiện thực này tôi sẽ viết 1 test case đơn giản liên quan đến chức năng này. Cụ thể, tôi sẽ thực hiện kịch bản test như sau:

  • Đăng nhập vào Garoon với thông tin username “brown“
  • Thực hiện upload 1 file hình ảnh
  • Download file về để kiểm tra nội dung file upload lên đúng hay không

Kịch bản thực hiện test case upload file

Hình 2.0: Kịch bản thực hiện test case upload file

Source code:

Cấu trúc file cho test case

Hình 3.0: Cấu trúc file cho test case

Để thực hiện test case này một cách đơn giản, tôi tạo ra folder upload-file bao gồm các thành phần:

  • helper.js: chứa một số các hàm xử lý việc download file, verify nội dung file
  • Photo1.jpg: hình dùng để upload file
  • test.data.js: khai báo thông tin liên quan đến file như file name, file path
  • test.spec.js: chứa các logic liên quan đến toàn bộ kịch bản test, các bước thực hiện cụ thể

Bạn có thể tham khảo và thay đổi cấu trúc file sao cho phù hợp với dự án của mình. Ở đây, tôi hướng đến sự đơn giản để người đọc có thể nắm bắt dễ dàng.

Để dễ theo dõi, tôi sẽ trình bày từ test.spec.js, nơi triệu gọi đến các thành phần còn lại trong folder upload-file.

test.spec.js

import fileInfo from "./test.data";
import { waitForDownloadFile, verifyFileContent } from "./helper";

describe("file upload testing", () => {
  before("Đăng nhập vào Garoon", () => {
    // đi đến địa chỉ trang Garoon
    browser.url("https://onlinedemo2.cybozu.info/scripts/garoon/grn.exe/index");
    
    // nhập username account
    $('[name="_account"]').addValue("brown");
    
    // nhập username password
    $('[name="_password"]').addValue("brown");
    
    // click vào Login để đăng nhập
    $('[name="login-submit"]').click();
  });

  it("Nên thực hiện upload file thành công", () => {
    //========upload file=========
    // click vào biểu tượng của quản lý file Cabinet trên Garoon
    $("=Cabinet").click();
    
    // click vào Add file để upload file lên
    $("=Add file").click();
    
    // thực hiện chọn file để upload lên, chỉ đến file path của Photo01.jpg
    $("#file_upload_").setValue(fileInfo.filePath);
    
    // điền subject cho file upload lên Cabinet
    $('//input[contains(@name,"title")]').addValue(fileInfo.subject);
    
    // chờ cho việc upload lên hoàn thành
    $('[name="upload_fileids[]"]').waitForDisplayed({
      timeout: 20000
    });
    
    // click vào Add để hoàn thành thêm file vào Cabinet
    $("=Add").click();
    
    // chờ cho việc thực hiện Add thành công, redirect qua trang Cabinet index
    $('td[style="white-space: nowrap;"]').waitForDisplayed({
      timeout: 20000,
      reverse: true
    });
    
    //========kiểm tra việc thực hiện upload file thành công không=========
    // click vào subject file vừa thêm vào Cabinet ở bước trước đó để đi vào trang chi tiết
    $(`=${fileInfo.subject}`).click();
    
    // chỉ định directory lưu file download về
    const directory = "/Users/{YourName}/Downloads/";
    
    // xác định file path của file download về
    const destinationFilePath = directory + fileInfo.fileName;
    
    // click vào link file để download file về
    $(`//a[contains(@href, "${fileInfo.fileName}")]`).click();
    
    //thực hiện chờ cho download file thành công
    waitForDownloadFile(destinationFilePath);
    
    // thực hiện verify nội dung file download về
    verifyFileContent(destinationFilePath, fileInfo.filePath);
  });
});

test.data.js

const path = require("path");

const fileInfo = {
  fileName: "Photo01.jpg",
  filePath: path.join(__dirname, "Photo01.jpg"),
  subject: "test upload image"
};

export { fileInfo as default };

helper.js

const fs = require("fs");

const isFileEqual = (firstFilePath, secondFilePath) => {
  const firstBuf = fs.readFileSync(firstFilePath);
  const secondBuf = fs.readFileSync(secondFilePath);
  return firstBuf.equals(secondBuf);
};

const isFileExisting = filePath => {
  return fs.existsSync(filePath);
};

export const waitForDownloadFile = (downloadFilePath, timeout = 60000) => {
  const delayTime = 1000;
  const maxCount = timeout / delayTime;
  let iCount = 0;
  while (!isFileExisting(downloadFilePath) && iCount < maxCount) {
    browser.pause(delayTime);
    iCount++;
  }
};

export const verifyFileContent = (destinationFilePath, sourceFilePath) => {
  const actualResult = isFileEqual(destinationFilePath, sourceFilePath);

  assert.isTrue(actualResult, "FAILED: The upload file does not correctly");
};

Một số điểm cần lưu ý trong phần hiện thực bên trên:

file path Đối với upload file:

\\thực hiện chọn file để upload lên, chỉ đến file path của Photo01.jpg
<$("#file\\_upload\\_").setValue(fileInfo.filePath);

Ở đây, tôi đang sử dụng absolute path, và test case trên local machine.
Trong trường hợp bạn chạy trên remote machine, vui lòng tham khảo cách chỉ định path tại đây

Đối với download file:
\\chỉ định directory lưu file download về
const directory = "/Users/{YourName}/Downloads/";

Thông thường với local machine, path sẽ phụ thuộc vào default directory mà browser được chỉ định để lưu file được download về. Đối với máy Mac, thường có định dạng như trên, bạn cần thay {YourName} bằng user đang login vào máy.
Máy Window sẽ có định dạng path khác, bạn cần lưu ý điểm này, để thay đổi đường dẫn cho phù hợp.

xử lý chờ \\chờ cho việc upload lên hoàn thành
$('[name="upload_fileids[]"]').waitForDisplayed({ timeout: 20000 });

\\chờ cho việc thực hiện Add thành công, redirect qua trang Cabinet index
$('td[style="white-space: nowrap;"]').waitForDisplayed({ timeout: 20000, reverse: true });
Bạn cần phải tự xử lý chờ để đảm bảo rằng file được upload lên hoàn thành rồi mới thực hiện các tác vụ tiếp theo. Việc này rất quan trọng, nó giúp chương trình của bạn ổn định hơn.
Việc lựa chọn chờ element nào phụ thuộc vào sản phẩm cụ thể mà bạn thực hiện test.

download file

Có rất nhiều cách để xử lý việc download file về, hiện tại tôi đang sử dụng UI để tải file về. Bạn hoàn toàn có thể sử dụng các thư viện để hỗ trợ việc lấy file về mà không cần UI như axios. Tuy nhiên bạn nên cân nhắc sửdụng như thế nào cho phù hợp.
Thực hiện download bằng UI sẽ phù hợp khi bạn test trải nghiệm thực tế của người dùng
Thực hiện download bằng thư viện khi bạn không thực sự quan tâm đến hành vi người dùng, cần nhanh chóng có file download cho một mục đích test khác.

verify verifyFileContent function
Việc so sánh file để xác nhận nội dung file upload lên có chính xác hay chưa. Đối với file hình ảnh sẽ có nhiều kỹ thuật để so sánh nội dung file, bạn có thể so sánh dạng binary hay base64.
Bảng 1.0: Một số lưu ý trong phần source code

Điểm lưu ý khi triển khai trên CI

Trên thực tế khi triển khai các test case upload file, chúng ta còn phải quan tâm đến vấn đề tích hợp chương trình lên hệ thống CI của công ty. Khi đó, phần source code sẽ cần có một số xử lý để đảm bảo cho test case chạy được.

Hệ thống CI của mỗi công ty có kỹ thuật triển khai khác nhau. Trong phần này, tôi trình bày dựa trên thực tế đã triển khai tại Cybozu. Chúng tôi xây dựng hệ thống Selenium Grid, và sử dụng công nghệ ảo hóa Docker. Bạn có thể xem thêm về cài đặt Selenium Grid bằng kỹ thuật Docker, ở đây tôi sẽ chỉ tập trung trình bày vào những phần liên quan đến file upload testing.

Giả sử trên CI, tôi xây dựng hệ thống Selenium Grid như sau:

Sơ đồ hoạt động của Selenium Grid, sử dụng công nghệ Docker

Hình 4.0: Sơ đồ hoạt động của Selenium Grid, sử dụng công nghệ Docker

Tôi có các Docker Container với các vai trò như sau:

  • Test runner: chứa source code chương trình automation, là nơi khởi chạy các test case
  • Selenium Hub: đóng vai trò trung tâm quản lý các Node, phân bố các test case chạy trên Node
  • Hệ thống Selenium Node: là nơi mà các test case được thực hiện

Vậy khi upload hay download trên hệ thống CI này có điều gì cần lưu ý? Hãy cũng xem nội dung bên dưới:

Như bạn đã thấy, Test runner và Selenium Node liên hệ với nhau qua Selenium Hub, chúng không tương tác trực tiếp với nhau. Do đó, bạn sẽ gặp khó khăn trong việc làm sao để Node lấy được file để upload lên trong khi file đó đang nằm ở Test runner, cũng như làm sao để thực hiện so sánh nội dung file download về (nằm trên Node) và file upload lên (nằm trên Test runner).

Khi đó bạn cần phải sử dụng Volume trong Docker. Nó cho phép container mount một thư mục trong host machine. Như hình ảnh bên dưới, Test runner và Node container mount tới thư mục trong host machine. Nhờ vậy, Test runner sẽ thấy đọc được file download nằm tại Node.

Sơ đồ sử dụng Volume trong Docker

Hình 5.0: Sơ đồ sử dụng Volume trong Docker

Theo như sơ đồ tại Hình 5.0 thì bạn có thể hình dung việc mount directory như sau:

  • Host: /end_to_end mount với Test runner: /end_to_end
  • Host: /end_to_end/Downloads mount với Node: /home/seluser/Downloads

Giả sử chúng ta có tiến hành download file .pdf trên Node. Lúc này file .pdf sẽ được lưu vào /home/seluser/Downloads trên Node. Vì directory này đã được mount tới /end_to_end/Downloads, nên file .pdf cũng hiển thị trong Host /end_to_end/Downloads. Mặt khác, Test runner cũng mount tới Host /end_to_end. Vì vậy Test runner cũng đọc được nội dung file .pdf.

Về mặt config volume trên Docker, bạn có thể tham khảo đoạn code bên dưới:

docker-compose.yml

...
HOST_DOWNLOAD_DIRECTORY="Downloads"
NODE_DOWNLOAD_DIRECTORY="/home/seluser/Downloads"
test_runner:
    image: node:12.22.1
    volumes:
    - .:/end_to_end
chrome_node:
    image: %NODE_IMAGE%
    volumes:
    - "./%HOST_DOWNLOAD_DIRECTORY%:%NODE_DOWNLOAD_DIRECTORY%"
...

Trong file docker-compose.yml bạn cần chỉ định mount giữa host directory và container directory của Test runner (line 7) và Node (line 11).

Ngoài cách download file trực tiếp bằng UI, bạn có thể sử dụng một số thư viện để download file thông qua HTTP Request như request-promise hoặc axios. Đối với cách làm này bạn cần biết chính xác url để download file. Nếu sử dụng thư viện, việc mount directory sẽ không cần thực hiện nữa, vì tại Test runner sẽ nhận được nội dung file trả về qua response. Tuy nhiên, bạn nên cân nhắc cách làm này, như tôi đã đề cập bên trên, việc download bằng thư viện không phản ánh chính xác hành vi người dùng cuối.

Tại Cybozu, chúng tôi sử dụng WebdriverIO cho phần automation testing của end to end testing. Hiện tại framework này cho phép upload file lên Node, sau đó nó trả về đường dẫn file đó trên con Node. Tiếp theo bạn chỉ cần chỉ định file được upload lên bằng đường dẫn này. Vì vậy nếu bạn sử dụng WebdriverIO, trường hợp upload file cũng không cần thực hiện mount directory.

// Khi chạy trên Docker, Test runner và Node nằm trên 2 container khác nhau, để upload file cần:
// step 1: upload file lên Node
const remoteFilePath = browser.uploadFile(filePath);
// step 2: upload file lên browser
$(attachFileLocator).addValue(remoteFilePath);

Tóm lại: Về vấn đề mount directory trên Docker có một vài tình huống như sau:

  • Thực hiện mount directory cho trường hợp download file bằng UI
  • Thực hiện mount directory cho trường hợp upload file nếu testing framework không hỗ trợ upload file lên Node
  • Không cần mount directory cho trường hợp download bằng HTTP Request
  • Không cần mount directory cho trường hợp upload file nếu sử dụng WebdriverIO

Một số lưu ý khác

Như bạn đã thấy, test case upload file không đơn thuần chỉ tương tác với browser mà còn liên quan đến ghi và đọc file trên máy. Một số chú ý liên quan đến file bạn cần biết như:

  • Đường dẫn nơi chứa file upload hay lưu file download cần chỉ định chính xác. Đối với WebDriverIO, bạn có thể chỉ định folder để chứa file về như sau:

    capabilities: [{
        browserName: 'chrome',
        // this overrides the default chrome download directory with downloadDir
        goog:chromeOptions: {
          prefs: {
            'download.default_directory': downloadDir
          }
        }
    }]
  • Khi bạn download file về máy, để bỏ qua bước confirm khi save file về, có thể tham khảo setting sau đối với WebdriverIO:

    capabilities: [{
        browserName: 'chrome',
        goog:chromeOptions: {
          prefs: {
            'download.prompt_for_download': false
          }
        }
    }]
  • Những vấn đề về quyền truy cập file, folder trên máy cũng cần lưu ý, đặc biệt là khi tích hợp chương trình test vào hệ thống CI.

Lời kết

Các test case upload hay download file không giống với các test case thông thường khác, có phần khó hơn. Bạn sẽ có cơ hội đối mặt với các vấn đề về file trên cả local machine hay remote machine. Nhưng đây là những test case quan trọng và rất thường gặp trong các ứng dụng hiện nay.

Hi vọng qua bài viết này các bạn có thể viết được một test case đơn giản về upload file cũng như giải quyết được một vài vấn đề cơ bản thường gặp đối với loại test này.