Mọi framework kiểm thử bạn đã từng sử dụng, dù là Jest, Mocha, hay node:test, đều dựa trên một ý tưởng đơn giản: nêu rõ điều bạn mong đợi, sau đó ném ra một lỗi nếu thực tế không khớp. Node.js cung cấp ý tưởng đó dưới dạng một module tích hợp sẵn có tên là assert. Không cần cài đặt, không phụ thuộc, chỉ cần require nó và bắt đầu kiểm tra các giả định.
Module assert rất đáng để tìm hiểu. Nó cung cấp các kiểm tra nhanh chóng trong các script, nó là nền t tảng cho nhiều test runner, và nó dạy cho bạn một assertion thực sự là gì trước khi bất kỳ framework nào "trang điểm" cho nó. Hướng dẫn này bao gồm các phương thức quan trọng, sự khác biệt giữa chế độ nghiêm ngặt (strict) và kế thừa (legacy) mà nhiều người mắc lỗi, cách kiểm tra lỗi và mã bất đồng bộ, và cách cùng một module này giúp bạn xác thực các phản hồi API.
Module assert làm gì
Một assertion là một câu lệnh phải đúng để chương trình của bạn được coi là chính xác. Khi bạn viết assert.strictEqual(total, 100), bạn đang tuyên bố rằng total phải bằng 100. Nếu đúng, không có gì xảy ra và quá trình thực thi tiếp tục. Nếu không, assert sẽ ném ra một AssertionError dừng quá trình thực thi và cho bạn biết điều gì đã sai.
Import module. Dạng hiện đại, được khuyến nghị là phiên bản nghiêm ngặt:
const assert = require('node:assert/strict');
// hoặc với ES modules:
// import assert from 'node:assert/strict';
Assertion đơn giản nhất kiểm tra xem một giá trị có phải là truthy hay không:
const user = getUser(42);
assert(user, 'getUser should return a user object');
Đối số đầu tiên là giá trị đang được kiểm tra. Đối số thứ hai, tùy chọn, là một thông báo hiển thị khi assertion thất bại. Luôn viết thông báo đó. Một lỗi hiển thị "getUser should return a user object" hữu ích hơn nhiều so với một AssertionError trần trụi. Hiểu về các assertion cũng giúp ích khi bạn chuyển sang các assertion API chuyên dụng trong một công cụ kiểm thử.
Chế độ nghiêm ngặt so với chế độ kế thừa
Đây là điều quan trọng nhất cần nắm bắt. Module assert có hai chế độ.
Chế độ kế thừa (Legacy mode), mà bạn nhận được từ require('node:assert'), sử dụng phép so sánh lỏng lẻo (==) cho assert.equal và assert.deepEqual. Điều đó có nghĩa là assert.equal(1, '1') sẽ vượt qua, vì 1 == '1' là đúng trong JavaScript. So sánh lỏng lẻo là một nguồn lỗi nổi tiếng.
Chế độ nghiêm ngặt (Strict mode), mà bạn nhận được từ require('node:assert/strict'), sử dụng phép so sánh nghiêm ngặt (===) cho mọi thứ. assert.equal(1, '1') sẽ thất bại, đúng như vậy, vì các kiểu dữ liệu khác nhau.
const looseAssert = require('node:assert');
looseAssert.equal(1, '1'); // vượt qua, bất ngờ và nguy hiểm
const strict = require('node:assert/strict');
strict.equal(1, '1'); // ném ra AssertionError, chính xác
Sử dụng chế độ nghiêm ngặt. Không có lý do chính đáng nào để chấp nhận so sánh lỏng lẻo trong các bài kiểm thử. Phần còn lại của hướng dẫn này giả định sử dụng node:assert/strict, nơi assert.equal hoạt động giống như assert.strictEqual.
So sánh giá trị: equal và strictEqual
assert.strictEqual(actual, expected) kiểm tra xem hai giá trị có giống hệt nhau bằng cách sử dụng ===. Nó là công cụ chính cho các kiểu dữ liệu nguyên thủy như số, chuỗi và boolean.
const assert = require('node:assert/strict');
function priceWithTax(price, rate) {
return price + price * rate;
}
assert.strictEqual(priceWithTax(100, 0.08), 108, 'tính thuế phải cộng thêm 8 phần trăm');
assert.strictEqual(typeof priceWithTax(100, 0.08), 'number', 'kết quả phải là một số');
Cũng có assert.notStrictEqual, sẽ vượt qua khi hai giá trị không giống hệt nhau. Sử dụng nó để xác nhận một giá trị đã thay đổi:
const before = getCacheKey();
refreshCache();
const after = getCacheKey();
assert.notStrictEqual(before, after, 'khóa cache phải thay đổi sau khi refresh');
Đối với các đối tượng và mảng, strictEqual sẽ không hữu ích. Hai đối tượng literal có cùng nội dung là các tham chiếu khác nhau, vì vậy === trả về false. Đó là lý do tồn tại deep equality.
So sánh đối tượng: deepStrictEqual
assert.deepStrictEqual(actual, expected) so sánh cấu trúc và giá trị một cách đệ quy. Hai đối tượng sẽ vượt qua nếu mọi khóa đều chứa một giá trị nghiêm ngặt, cho đến tận cùng.
const assert = require('node:assert/strict');
function buildOrder(id, items) {
return { id, items, status: 'pending' };
}
assert.deepStrictEqual(
buildOrder(7, ['keyboard', 'mouse']),
{ id: 7, items: ['keyboard', 'mouse'], status: 'pending' },
'đối tượng đơn hàng phải khớp với hình dạng mong đợi'
);
Đây là phương thức bạn sẽ sử dụng nhiều nhất khi kiểm thử các hàm trả về đối tượng, và đặc biệt là khi kiểm thử các phản hồi API, vì các body JSON là đối tượng. Đối tác của nó assert.notDeepStrictEqual sẽ vượt qua khi các cấu trúc khác nhau.
Một lưu ý: deepStrictEqual cũng kiểm tra các kiểu. Một thuộc tính giữ số 7 sẽ không khớp với một thuộc tính giữ chuỗi '7'. Sự nghiêm ngặt đó là một tính năng; nó giúp phát hiện sự lệch kiểu trong dữ liệu của bạn. Nếu bạn đang kiểm thử các hàm với nhiều sự kết hợp đầu vào, hướng dẫn của chúng tôi về kiểm thử API dựa trên dữ liệu với CSV và JSON sẽ chỉ cho bạn cách mở rộng các assertion trên các bộ dữ liệu.
Khi deepStrictEqual thất bại, Node sẽ in ra một diff làm nổi bật chính xác những thuộc tính nào không khớp. Diff đó là một trong những điều hữu ích nhất của module này. Thay vì nhìn chằm chằm vào hai đối tượng lớn cố gắng tìm ra sự khác biệt, bạn đọc vài dòng được làm nổi bật. Đối với dữ liệu lồng nhau, điều đó đủ để biện minh cho việc sử dụng deepStrictEqual thay vì kiểm tra thủ công từng trường với strictEqual.
Khớp một phần với assert.match và assert.ok
Không phải mọi kiểm tra đều cần sự bằng nhau hoàn toàn. Đôi khi bạn chỉ quan tâm rằng một giá trị trông có vẻ đúng. assert.ok(value) sẽ vượt qua bất cứ khi nào giá trị là truthy, đây là công cụ phù hợp cho các kiểm tra "điều này phải tồn tại": một chuỗi không rỗng, một đối tượng được định nghĩa, một số lượng khác không.
assert.match(string, regexp) kiểm tra xem một chuỗi có khớp với một biểu thức chính quy hay không, và assert.doesNotMatch kiểm tra điều ngược lại. Chúng hữu ích cho các giá trị mà nội dung chính xác thay đổi nhưng hình dạng của chúng là cố định.
const assert = require('node:assert/strict');
function generateOrderId() {
return 'ORD-' + Date.now();
}
const id = generateOrderId();
// timestamp chính xác thay đổi, vì vậy kiểm tra định dạng, không phải giá trị
assert.match(id, /^ORD-\d+$/, 'ID đơn hàng phải là ORD- theo sau bởi các chữ số');
assert.ok(id.length > 4, 'ID đơn hàng không được rỗng');
Mẫu này đặc biệt quan trọng đối với kiểm thử API, nơi các phản hồi chứa timestamp, ID được tạo và token mà bạn không thể dự đoán. Bạn khẳng định định dạng bằng match và sự tồn tại bằng ok, và bạn dành strictEqual cho các giá trị mà bạn thực sự kiểm soát.
Khẳng định rằng mã ném ra lỗi
Đôi khi hành vi đúng đắn có nghĩa là ném ra một lỗi. assert.throws(fn, expectedError) chạy một hàm và chỉ vượt qua nếu nó ném ra lỗi.
const assert = require('node:assert/strict');
function parsePort(value) {
const port = Number(value);
if (!Number.isInteger(port) || port < 1 || port > 65535) {
throw new RangeError('cổng phải là một số nguyên từ 1 đến 65535');
}
return port;
}
// vượt qua: đầu vào không hợp lệ phải ném ra RangeError
assert.throws(
() => parsePort('70000'),
RangeError,
'cổng ngoài phạm vi phải ném ra RangeError'
);
// bạn cũng có thể khớp thông báo lỗi với một biểu thức chính quy
assert.throws(
() => parsePort('abc'),
/phải là một số nguyên/,
'cổng không phải là số phải ném ra lỗi mô tả'
);
// vượt qua: đầu vào hợp lệ không được ném lỗi
assert.doesNotThrow(
() => parsePort('8080'),
'một cổng hợp lệ không được ném lỗi'
);
Lưu ý rằng bạn truyền một hàm, không phải một lời gọi. assert.throws(parsePort('70000')) sẽ thực thi parsePort trước khi assert kịp thấy nó, và lỗi sẽ thoát ra mà không bị bắt. Luôn bao bọc nó: () => parsePort('70000').
Khẳng định trên mã bất đồng bộ: rejects
Mã Node.js hiện đại đầy rẫy các promise, và một promise bị từ chối là tương đương bất đồng bộ của một lỗi được ném ra. assert.rejects và assert.doesNotReject xử lý trường hợp đó. Cả hai đều trả về promise, vì vậy bạn cần await chúng.
const assert = require('node:assert/strict');
async function fetchUser(id) {
if (typeof id !== 'number') {
throw new TypeError('id phải là một số');
}
// ... việc tra cứu thực tế sẽ diễn ra ở đây
return { id, name: 'Dana Lee' };
}
async function runTests() {
// vượt qua: đầu vào không tốt bị từ chối với TypeError
await assert.rejects(
fetchUser('not-a-number'),
TypeError,
'id chuỗi phải bị từ chối'
);
// vượt qua: đầu vào tốt được giải quyết mà không bị từ chối
await assert.doesNotReject(
fetchUser(101),
'id hợp lệ phải được giải quyết sạch sẽ'
);
}
runTests();
Quên await một lời gọi assert.rejects là một lỗi phổ biến. Nếu không có await, hàm kiểm thử kết thúc trước khi assertion được giải quyết, và một lỗi trở thành một rejected promise chưa được xử lý thay vì một lỗi kiểm thử rõ ràng.
Sử dụng assert để kiểm thử phản hồi API
Module assert thực sự hữu ích để kiểm tra đầu ra của một yêu cầu HTTP. Sau khi bạn lấy một endpoint, bạn có mã trạng thái và body JSON, và cả hai đều là những thứ cần được kiểm tra.
const assert = require('node:assert/strict');
async function testGetUser() {
const res = await fetch('https://api.example.com/users/101');
// kiểm tra mã trạng thái
assert.strictEqual(res.status, 200, 'GET /users/101 phải trả về 200');
const body = await res.json();
// kiểm tra hình dạng và kiểu dữ liệu của phản hồi
assert.strictEqual(typeof body.id, 'number', 'id phải là một số');
assert.strictEqual(body.id, 101, 'id phải khớp với người dùng được yêu cầu');
assert.ok(body.name, 'phản hồi phải bao gồm một tên');
assert.ok(Array.isArray(body.roles), 'roles phải là một mảng');
}
testGetUser().then(
() => console.log('Kiểm thử API đã vượt qua'),
(err) => { console.error('Kiểm thử API thất bại:', err.message); process.exitCode = 1; }
);
Mẫu này hoạt động và nó dạy những kiến thức cơ bản. Nhưng đối với một bộ kiểm thử API thực sự, bạn sẽ muốn các lần chạy có cấu trúc, thử lại, xử lý môi trường và báo cáo mà assert thô không cung cấp. Các hướng dẫn của chúng tôi về cách viết script kiểm thử tự động và framework tự động hóa API pytest bao gồm bước tiếp theo đó.
Nếu bạn không muốn tự xây dựng một công cụ kiểm thử, Apidog cho phép bạn định nghĩa các yêu cầu API và đính kèm các assertion trực quan trên mã trạng thái, header và các trường JSON mà không cần viết mã assertion. Bạn có thể xâu chuỗi các yêu cầu thành các kịch bản kiểm thử tự động, chạy chúng trong CI/CD, và vẫn có thể sử dụng các script tùy chỉnh khi bạn cần sự kiểm soát mà assert mang lại. Đây là một con đường nhanh hơn từ "Tôi có một endpoint" đến "Tôi có một bộ kiểm thử", và bạn có thể tải Apidog và sử dụng miễn phí. Cùng với Apidog, module assert của Node.js bao gồm cả các kiểm tra nhanh và các bộ kiểm thử đầy đủ.
Các câu hỏi thường gặp
Tôi có cần cài đặt module assert không?
Không. assert được tích hợp sẵn trong Node.js. Import nó bằng require('node:assert/strict') hoặc require('node:assert'). Không có gói npm nào để cài đặt và không có dependency nào để quản lý. Tiền tố node: làm rõ rằng bạn đang tải một module lõi.
Sự khác biệt giữa assert.equal và assert.strictEqual là gì?
Ở chế độ kế thừa, assert.equal sử dụng so sánh lỏng lẻo (==) và assert.strictEqual sử dụng so sánh nghiêm ngặt (===). Ở chế độ nghiêm ngặt, được tải qua node:assert/strict, assert.equal hoạt động giống hệt như assert.strictEqual. Luôn sử dụng chế độ nghiêm ngặt để ép kiểu không bao giờ ngầm vượt qua một bài kiểm thử.
Khi nào tôi nên sử dụng deepStrictEqual thay vì strictEqual?
Sử dụng strictEqual cho các kiểu nguyên thủy: số, chuỗi, boolean. Sử dụng deepStrictEqual cho các đối tượng và mảng, vì strictEqual so sánh các tham chiếu và hai đối tượng riêng biệt không bao giờ bằng nhau về tham chiếu. Bất cứ khi nào bạn kiểm tra một body JSON hoặc một đối tượng được trả về, hãy sử dụng deepStrictEqual.
Tôi nên sử dụng module assert hay một framework kiểm thử như Jest?
Đối với các script nhanh và để học, assert thô là tốt. Đối với một bộ kiểm thử thực sự, hãy sử dụng một framework. Node.js cũng cung cấp một test runner tích hợp sẵn, node:test, kết hợp tự nhiên với assert và cung cấp cho bạn việc tổ chức kiểm thử, báo cáo và song song hóa mà không cần dependency bên ngoài.
Làm cách nào để khẳng định rằng một hàm bất đồng bộ bị từ chối?
Sử dụng assert.rejects, và nhớ await nó. Truyền promise hoặc một lời gọi hàm bất đồng bộ, tùy chọn với loại lỗi mong đợi hoặc biểu thức chính quy của thông báo. assert.rejects(somePromise, TypeError) chỉ vượt qua nếu promise bị từ chối với TypeError. Nếu không có await, một lỗi sẽ trở thành một rejected promise chưa được xử lý thay vì một lỗi assertion rõ ràng.
