Công cụ thương mại

Về cơ bản, kiểm thử tự động chỉ là đoạn mã sẽ gửi hoặc gây ra lỗi nếu có lỗi. Hầu hết các thư viện hoặc khung kiểm thử đều cung cấp nhiều dữ liệu gốc giúp việc viết kiểm thử dễ dàng hơn.

Như đã đề cập trong phần trước, các dữ liệu gốc này hầu như luôn bao gồm một cách để xác định các kiểm thử độc lập (gọi là trường hợp kiểm thử) và để đưa ra nhận định. Xác nhận là một cách để kết hợp việc kiểm tra kết quả và báo lỗi nếu có vấn đề. Việc này có thể được coi là nguyên hàm cơ bản của tất cả dữ liệu gốc kiểm thử.

Trang này trình bày cách tiếp cận chung đối với những dữ liệu nguyên gốc này. Khung mà bạn chọn có thể có dạng như sau, nhưng đây không phải là tham chiếu chính xác.

Ví dụ:

import { fibonacci, catalan } from '../src/math.js';
import { assert, test, suite } from 'a-made-up-testing-library';

suite('math tests', () => {
  test('fibonacci function', () => {
    // check expected fibonacci numbers against our known actual values
    // with an explanation if the values don't match
    assert.equal(fibonacci(0), 0, 'Invalid 0th fibonacci result');
    assert.equal(fibonacci(13), 233, 'Invalid 13th fibonacci result');
  });
  test('relationship between sequences', () => {
    // catalan numbers are greater than fibonacci numbers (but not equal)
    assert.isAbove(catalan(4), fibonacci(4));
  });
  test('bugfix: check bug #4141', () => {
    assert.isFinite(fibonacci(0)); // fibonacci(0) was returning NaN
  })
});

Ví dụ này tạo một nhóm kiểm thử (đôi khi gọi là bộ) được gọi là "kiểm thử toán học" và xác định 3 trường hợp kiểm thử độc lập, trong đó mỗi trường hợp chạy một số câu nhận định. Những trường hợp kiểm thử này thường có thể được xử lý hoặc chạy riêng lẻ, chẳng hạn như bằng một cờ bộ lọc trong trình chạy kiểm thử.

Trình trợ giúp xác nhận đóng vai trò là dữ liệu gốc

Hầu hết các khung kiểm thử, bao gồm cả Vitest, đều bao gồm một tập hợp trình trợ giúp xác nhận trên đối tượng assert cho phép bạn nhanh chóng kiểm tra giá trị trả về hoặc các trạng thái khác dựa trên một số expectation. Kỳ vọng đó thường là các giá trị "tốt đẹp". Trong ví dụ trước, chúng ta biết số Fibonacci thứ 13 phải là 233, vì vậy chúng ta có thể xác nhận trực tiếp điều đó bằng cách sử dụng assert.equal.

Bạn cũng có thể kỳ vọng rằng một giá trị có một dạng nhất định, lớn hơn một giá trị khác hoặc có một thuộc tính khác nào đó. Khoá học này sẽ không đề cập đầy đủ các trình trợ giúp xác nhận có thể có, nhưng khung kiểm thử luôn cung cấp ít nhất các bước kiểm tra cơ bản sau đây:

  • Hoạt động kiểm tra "trung thực", thường được mô tả là kiểm tra "OK", sẽ kiểm tra xem một điều kiện có đúng hay không, khớp với cách bạn có thể viết if nhằm kiểm tra xem điều gì đó thành công hay đúng. Giá trị này thường được cung cấp dưới dạng assert(...) hoặc assert.ok(...), đồng thời nhận một giá trị duy nhất cùng với một nhận xét không bắt buộc.

  • Một quy trình kiểm tra đẳng thức, chẳng hạn như trong ví dụ kiểm thử toán học, trong đó bạn kỳ vọng giá trị trả về hoặc trạng thái của một đối tượng bằng một giá trị chính xác đã biết. Các dữ liệu này dành cho đẳng thức ban đầu (chẳng hạn như đối với số và chuỗi) hoặc đẳng thức tham chiếu (các đối tượng này là cùng một đối tượng). Về bản chất, đây chỉ là thông tin kiểm tra "chính xác" với phép so sánh == hoặc ===.

    • JavaScript phân biệt giữa đẳng thức lỏng lẻo (==) và cân bằng nghiêm ngặt (===). Hầu hết các thư viện kiểm thử đều cung cấp cho bạn các phương thức assert.equalassert.strictEqual tương ứng.
  • Các quy trình kiểm tra đẳng thức sâu, giúp mở rộng quy trình kiểm tra đẳng thức, bao gồm cả việc kiểm tra nội dung của đối tượng, mảng và các loại dữ liệu phức tạp khác, cũng như logic nội bộ để truyền tải các đối tượng nhằm so sánh. Các chỉ số này rất quan trọng vì JavaScript không có cách tích hợp nào để so sánh nội dung của 2 đối tượng hoặc mảng. Ví dụ: [1,2,3] == [1,2,3] luôn là sai. Khung kiểm thử thường bao gồm các trình trợ giúp deepEqual hoặc deepStrictEqual.

Các trình trợ giúp xác nhận so sánh hai giá trị (thay vì chỉ kiểm tra "độ trung thực") thường sẽ lấy 2 hoặc 3 đối số:

  • Giá trị thực tế, như được tạo từ mã đang được kiểm thử hoặc mô tả trạng thái cần xác thực.
  • Giá trị dự kiến, thường được mã hoá cứng (ví dụ: một số cố định hoặc chuỗi).
  • Nhận xét không bắt buộc mô tả những điều dự kiến hoặc sự cố có thể không thành công. Nhận xét này sẽ được đưa vào nếu dòng này không thành công.

Một phương pháp khá phổ biến là kết hợp các câu nhận định để tạo nhiều quy trình kiểm tra khác nhau, vì hiếm khi người dùng có thể xác nhận chính xác trạng thái của hệ thống. Ví dụ:

  test('JWT parse', () => {
    const json = decodeJwt('eyJieSI6InNhbXRob3Ii…');

    assert.ok(json.payload.admin, 'user should be admin');
    assert.deepEqual(json.payload.groups, ['role:Admin', 'role:Submitter']);
    assert.equal(json.header.alg, 'RS265')
    assert.isAbove(json.payload.exp, +new Date(), 'expiry must be in future')
  });

Vitest sử dụng thư viện câu nhận định Chai trong nội bộ để cung cấp trình trợ giúp xác nhận. Bạn nên xem qua thông tin tham chiếu của thư viện này để xem những câu nhận định và trình trợ giúp nào có thể phù hợp với mã của bạn.

Xác nhận thành thạo và BDD

Một số nhà phát triển muốn sử dụng kiểu câu nhận định có thể được gọi là câu nhận định kiểu phát triển dựa trên hành vi (BDD) hoặc câu nhận định kiểu Fluent. Đây còn được gọi là trình trợ giúp "kỳ vọng", vì điểm truy cập để kiểm tra các kỳ vọng là một phương thức có tên expect().

Kỳ vọng trình trợ giúp hoạt động giống như câu nhận định được viết dưới dạng các lệnh gọi phương thức đơn giản như assert.ok hoặc assert.strictDeepEquals. Tuy nhiên, một số nhà phát triển thấy các câu nhận định này dễ đọc hơn. Lời xác nhận BDD có thể có dạng như sau:

// A failure here would generate "Expect result to be an array that does include 42"
const result = await possibleMeaningsOfLife();
expect(result).to.be.an('array').that.does.include(42);

// or a simpler form
expect(result).toBe('array').toContainEqual(42);

// the same in assert might be
assert.typeOf(result, 'array', 'Expected the result to be an array');
assert.include(result, 42, 'Expected the result to include 42');

Các kiểu nhận định này hoạt động nhờ một kỹ thuật gọi là chuỗi phương thức, trong đó đối tượng do expect trả về có thể liên tục xâu chuỗi với các lệnh gọi phương thức khác. Một số phần của lệnh gọi, bao gồm cả to.bethat.does trong ví dụ trước, không có hàm và chỉ được đưa vào để giúp lệnh gọi dễ đọc hơn và có khả năng tạo nhận xét tự động nếu kiểm thử không thành công. (Đáng chú ý là expect thường không hỗ trợ một nhận xét không bắt buộc vì chuỗi này phải mô tả rõ ràng lỗi.)

Nhiều khung kiểm thử hỗ trợ cả câu lệnh Fluent/BDD và câu nhận định thông thường. Ví dụ Vitest xuất cả hai phương pháp của Chai và có cách tiếp cận riêng ngắn gọn hơn một chút đối với BDD. Mặt khác, Jest chỉ bao gồm phương thức mong đợi theo mặc định.

Nhóm các bài kiểm thử trên các tệp

Khi viết kiểm thử, chúng tôi đã có xu hướng cung cấp các nhóm ngầm ẩn — thay vì tất cả các kiểm thử đều nằm trong một tệp, thường thì bạn sẽ viết mã kiểm thử trên nhiều tệp. Trên thực tế, trình chạy kiểm thử thường chỉ biết rằng một tệp là để kiểm thử do một bộ lọc xác định trước hoặc biểu thức chính quy – ví dụ: vitest bao gồm tất cả các tệp trong dự án của bạn kết thúc bằng phần mở rộng như ".test.jsx" hoặc ".spec.ts" (".test" và ".spec" cùng với một số đuôi hợp lệ).

Các bài kiểm thử thành phần thường nằm trong một tệp ngang hàng với thành phần đang được kiểm thử, như trong cấu trúc thư mục sau:

Danh sách các tệp trong một thư mục, bao gồm UserList.tsx và UserList.test.tsx.
Một tệp thành phần và tệp kiểm thử liên quan.

Tương tự, mã kiểm thử đơn vị có xu hướng được đặt bên cạnh mã đang được kiểm thử. Mỗi bài kiểm thử toàn diện có thể nằm trong tệp riêng và bài kiểm thử tích hợp thậm chí có thể được đặt trong các thư mục riêng. Những cấu trúc này có thể hữu ích khi các trường hợp kiểm thử phức tạp phát triển và đòi hỏi phải có tệp hỗ trợ không dùng cho mục đích kiểm thử, chẳng hạn như các thư viện hỗ trợ chỉ cần cho hoạt động kiểm thử.

Kiểm thử nhóm trong tệp

Như đã dùng trong các ví dụ trước, bạn nên đặt chương trình kiểm thử bên trong lệnh gọi tới suite() để nhóm các chương trình kiểm thử mà bạn đã thiết lập bằng test(). Bộ ứng dụng thường không phải là tự kiểm thử, nhưng chúng giúp tạo ra cấu trúc bằng cách nhóm các bài kiểm thử hoặc mục tiêu có liên quan bằng cách gọi phương thức đã truyền. Đối với test(), phương thức đã truyền sẽ mô tả các hành động của chính bài kiểm thử đó.

Giống như câu nhận định, có sự tương đương khá tiêu chuẩn trong Fluent/BDD để nhóm các bài kiểm thử. Một số ví dụ điển hình được so sánh trong đoạn mã sau:

// traditional/TDD
suite('math tests', () => {
  test('handle zero values', () => {
    assert.equal(fibonacci(0), 0);
  });
});

// Fluent/BDD
describe('math tests', () => {
  it('should handle zero values', () => {
    expect(fibonacci(0)).toBe(0);
  });
})

Trong hầu hết các khung, suitedescribe hoạt động tương tự nhau, cũng như testit, khác với sự khác biệt lớn hơn giữa việc sử dụng expectassert để viết câu nhận định.

Các công cụ khác có những phương pháp tinh tế để sắp xếp các bộ công cụ và kiểm thử. Ví dụ: trình chạy kiểm thử tích hợp của Node.js hỗ trợ các lệnh gọi lồng nhau tới test() để ngầm tạo một hệ phân cấp kiểm thử. Tuy nhiên, Vitest chỉ cho phép loại lồng ghép này bằng cách sử dụng suite() và sẽ không chạy test() được xác định bên trong một test() khác.

Tương tự như với câu nhận định, hãy nhớ rằng tổ hợp chính xác các phương thức nhóm mà ngăn xếp công nghệ cung cấp không quan trọng. Khoá học này sẽ tóm tắt nội dung của khoá học này, nhưng bạn sẽ cần tìm hiểu cách áp dụng các nguyên tắc đó vào công cụ mà bạn chọn.

Phương thức vòng đời

Một lý do để nhóm các bài kiểm thử, ngay cả khi ngầm ẩn ở cấp cao nhất trong một tệp, là để cung cấp các phương thức thiết lập và chia nhỏ chạy cho mỗi bài kiểm thử hoặc một lần cho một nhóm các bài kiểm thử. Hầu hết các khung cung cấp 4 phương thức:

Đối với mọi "test()" hoặc "it()" Một lần cho bộ
Trước khi chạy kiểm thử "beforeEach()" "beforeAll()"
Sau khi chạy kiểm thử "afterEach()" "afterAll()"

Ví dụ: bạn nên điền sẵn cơ sở dữ liệu người dùng ảo trước mỗi lần kiểm thử và xoá cơ sở dữ liệu đó sau đó:

suite('user test', () => {
  beforeEach(() => {
    insertFakeUser('bob@example.com', 'hunter2');
  });
  afterEach(() => {
    clearAllUsers();
  });

  test('bob can login', async () => { … });
  test('alice can message bob', async () => { … });
});

Việc này có thể giúp đơn giản hoá quy trình kiểm thử. Bạn có thể chia sẻ mã thiết lập và phân tích chung, thay vì sao chép mã trong mỗi hoạt động kiểm thử. Ngoài ra, nếu chính mã thiết lập và chia nhỏ gửi một lỗi, điều đó có thể chỉ ra vấn đề về cấu trúc không liên quan đến việc tự kiểm thử không đạt.

Lời khuyên chung

Sau đây là một vài mẹo cần nhớ khi nghĩ về các dữ liệu nguyên thuỷ này.

Miền nguyên gốc là một hướng dẫn

Hãy nhớ rằng các công cụ và dữ liệu nguyên gốc ở đây và trong một vài trang tiếp theo sẽ không khớp chính xác với Vitest, Jest, Mocha hoặc Web Test Runner hay bất kỳ khung cụ thể nào khác. Mặc dù chúng tôi đã sử dụng Vitest làm hướng dẫn chung, nhưng hãy nhớ liên kết chúng với khung mà bạn chọn.

Kết hợp và khớp câu nhận định nếu cần

Về cơ bản, kiểm thử là một đoạn mã có thể gửi lỗi. Mỗi trình chạy sẽ cung cấp một dữ liệu gốc, có thể là test(), để mô tả các trường hợp kiểm thử riêng biệt.

Tuy nhiên, nếu trình chạy đó cũng cung cấp assert(), expect() và trình trợ giúp xác nhận, hãy nhớ rằng phần này hướng đến sự tiện lợi và bạn có thể bỏ qua nếu cần. Bạn có thể chạy bất kỳ mã nào có khả năng gây ra lỗi, bao gồm cả các thư viện xác nhận khác hoặc một câu lệnh if lỗi thời.

Việc thiết lập IDE có thể là một giải pháp cứu sinh

Việc đảm bảo IDE của bạn, chẳng hạn như VSCode, có quyền truy cập vào tính năng tự động hoàn thành và tài liệu trên công cụ kiểm thử mà bạn chọn, có thể giúp bạn làm việc hiệu quả hơn. Ví dụ: có hơn 100 phương thức trên assert trong thư viện câu nhận định Chai, nên việc có tài liệu về đúng phương thức xuất hiện cùng dòng có thể thuận tiện.

Điều này có thể đặc biệt quan trọng đối với một số khung kiểm thử điền sẵn các phương thức kiểm thử vào không gian tên chung. Đây là một sự khác biệt nhỏ, nhưng thông thường, bạn có thể sử dụng thư viện kiểm thử mà không cần nhập thư viện đó nếu các thư viện đó được tự động thêm vào không gian tên chung:

// some.test.js
test('using test as a global', () => { … });

Bạn nên nhập trình trợ giúp ngay cả khi trình trợ giúp đó được hỗ trợ tự động, vì điều đó sẽ giúp IDE của bạn dễ dàng tra cứu các phương thức này. (Bạn có thể đã gặp phải vấn đề này khi xây dựng React, vì một số cơ sở mã có React phép thuật trên toàn cầu, nhưng một số cơ sở thì không và yêu cầu phải nhập mã này vào tất cả các tệp bằng React.)

// some.test.js
import { test } from 'vitest';
test('using test as an import', () => { … });