Một repo nhỏ của mình có hai file nguồn: math.js đã có test và unused.js chưa được import ở đâu. Mình chạy coverage bằng test runner tích hợp của Node, thấy cả line, branch lẫn function đều xanh 100%. Nhìn nhanh thì quá đẹp, nhưng mở cây thư mục ra mới thấy báo cáo đã bỏ luôn file chưa test khỏi mẫu số.

Trong Node.js 26.7, bài toán “test coverage include all” có một cờ mới là --test-coverage-include-all. Mình tải đúng binary Node.js v26.7.0 cho Windows x64, kiểm tra SHA-256, rồi chạy cùng một fixture trước và sau khi bật cờ. Kết quả không chỉ đổi con số: nó đổi câu hỏi từ “những file đã chạy được cover bao nhiêu?” sang “toàn bộ mã nguồn mình muốn kiểm tra được cover bao nhiêu?”.

Coverage 100% nhưng mình vẫn bỏ quên một file

Fixture của mình cố tình rất nhỏ để tránh nhiễu. src/math.js có hàm cộng, src/unused.js có hàm nhân, src/nested/format.js nằm trong thư mục con. Test duy nhất chỉ import hàm cộng. Mình còn thêm src/generated/skip.jssrc/hidden.test.js để xem file generated và file mang tên test được xử lý ra sao.

// src/math.js
export function add(a, b) {
  return a + b;
}

// test/math.test.js
import assert from 'node:assert/strict';
import test from 'node:test';
import { add } from '../src/math.js';

test('cộng hai số', () => {
  assert.equal(add(2, 3), 5);
});

Môi trường chạy là Windows 11 Pro build 26200, Node.js v26.7.0 x64, ngày kiểm tra 09/08/2026. Mình không cài c8, nyc, Jest hay package coverage nào cho lab này. Trang phát hành Node.js 26.7.0 ghi cờ mới trong nhóm thay đổi semver-minor của test runner; bản này vẫn là nhánh Current, không phải LTS.

Baseline 100% thực ra đo phần đã được nạp

Đầu tiên mình chạy đúng test, chỉ bật coverage thử nghiệm:

node --test --experimental-test-coverage test/math.test.js

Test pass, exit code là 0. Báo cáo chỉ có math.js và tổng line coverage là 100%. unused.js, file trong thư mục nested và file generated đều không xuất hiện. Con số 100% không sai về mặt tính toán; vấn đề là phạm vi quan sát nhỏ hơn phạm vi mã nguồn mình tưởng đang đo.

Mình thử thêm --test-coverage-include="src/**/*.js" nhưng không bật cờ mới. Kết quả vẫn chỉ có math.js và vẫn 100%. Đây là chỗ dễ nhầm nhất: include glob lọc các file trong báo cáo, nhưng ở lần chạy của mình nó không tự đi tìm file chưa từng được nạp.

So sánh output coverage Node.js 26.7 trước và sau khi bật include all
Cùng một fixture và một test: baseline chỉ thấy math.js 100%, còn include-all đưa hai file chưa nạp vào báo cáo ở 0% line.

Hai loại lỗ hổng coverage cũng khác nhau. Một file đã được import nhưng có vài nhánh chưa chạy thường hiện ngay với dòng thiếu. Một file chưa hề được load có thể biến mất hoàn toàn ở baseline. Nếu chỉ chăm chăm tăng phần trăm của các file đang hiện, anh em có thể sửa rất nhiều test mà vẫn không chạm đến vùng mù thứ hai.

Bật include all làm file chưa nạp hiện ra

Mình chạy lại cùng test, chỉ thêm --test-coverage-include-all. Lúc này Node tìm file ứng viên từ working directory. unused.js, nested/format.js và cả generated/skip.js xuất hiện với line coverage 0%; tổng line coverage rơi từ 100% xuống 25%.

node --test \
  --experimental-test-coverage \
  --test-coverage-include-all \
  test/math.test.js

Kết quả này khớp mô tả trong CLI docs của Node.js 26.7: file nguồn chưa được load sẽ được đưa vào báo cáo với coverage bằng 0, và danh sách ứng viên chịu cùng bộ lọc include/exclude. PR #64830 cũng nói rõ working directory là điểm bắt đầu tìm kiếm.

Có một chi tiết mình không muốn lướt qua: trong fixture này, các file chưa nạp có line coverage 0% nhưng branch và function vẫn hiện 100%. Vì file chưa được thực thi, hai loại tổng đó không giúp mình phát hiện vùng mù như cột line. Do vậy đặt đủ ba threshold nghe chặt, nhưng line threshold mới là thứ làm ca thử này thất bại.

Include và exclude phải mô tả đúng mã nguồn

include-all khép vùng mù, nhưng nếu bật trần trụi ở root repo thì báo cáo có thể thu thập nhiều file hơn ý định. Trong lab, file generated cũng bị tính 0%. Ngược lại, file hidden.test.js không xuất hiện ở lần chạy mặc định vì test file phù hợp mẫu được loại khỏi coverage. Tài liệu collecting code coverage còn ghi module core và file trong node_modules không được tính mặc định.

Cấu hình mình dùng để lọc fixture

node --test \
  --experimental-test-coverage \
  --test-coverage-include-all \
  --test-coverage-include="src/**/*.js" \
  --test-coverage-exclude="src/generated/**" \
  --test-coverage-exclude="src/**/*.test.js" \
  test/math.test.js

Sau khi lọc, báo cáo còn math.js, unused.jsnested/format.js; tổng line coverage là 33,33%. Mình viết cả exclude cho test file thay vì dựa hoàn toàn vào mặc định, vì cấu hình CI đọc lại sau vài tháng sẽ dễ hiểu hơn. Với repo thật, anh em nên cân nhắc thêm dist, file sinh từ schema, migration snapshot hay vendor code tùy cấu trúc, đừng bê nguyên glob của mình.

Lưu ý quoting trên Windows

Mình chạy bằng PowerShell và truyền glob dưới dạng một argument có dấu ngoặc kép. Node xử lý glob, không phải shell. Trên Bash, cũng nên quote glob để shell không nở nó trước khi Node nhận được. Đây là kiểu khác biệt nhỏ nhưng đủ làm local pass, CI fail rồi tới công chuyện.

Đặt threshold để CI thất bại đúng chỗ

Coverage chỉ hữu ích cho pipeline khi thiếu coverage tạo ra trạng thái thất bại. Mình đặt line, branch và function cùng 80%. Với baseline, lệnh vẫn exit 0 vì file duy nhất được nhìn thấy đạt 100%. Với include-all và bộ lọc ở trên, test vẫn pass nhưng tiến trình exit 1 kèm thông báo line coverage 33,33% không đạt ngưỡng 80%.

node --test \
  --experimental-test-coverage \
  --test-coverage-include-all \
  --test-coverage-include="src/**/*.js" \
  --test-coverage-exclude="src/generated/**" \
  --test-coverage-lines=80 \
  --test-coverage-branches=80 \
  --test-coverage-functions=80 \
  test/math.test.js
Ma trận include all, include exclude và ngưỡng coverage trong CI
Ba lớp cần đi cùng nhau: khám phá file, lọc đúng phạm vi và đặt threshold để CI thất bại đúng chỗ.

Mình sẽ đưa lệnh này vào một job thử nghiệm trước, pin chính xác Node 26.7.0 và xem danh sách file 0% trong vài pull request. Khi phạm vi đã đúng mới biến nó thành required check. Cách làm chậm một nhịp này giống lúc xử lý workflow GitHub Actions đáng ngờ: quality gate chỉ đáng tin khi mình hiểu điều kiện nào khiến nó đỏ.

Có nên bỏ c8, nyc hoặc Vitest?

Với repo JavaScript nhỏ dùng sẵn node:test, cờ mới làm test runner tích hợp hấp dẫn hơn hẳn. Nó có include/exclude, threshold và reporter lcov; ít dependency hơn, lệnh chạy thẳng. Nếu nhu cầu chỉ là thấy file bỏ quên và chặn line coverage tối thiểu, mình chưa cần thêm một package chỉ để làm đúng việc đó.

Mình thích kiểu workflow CLI gọn và có output đọc được ngay, tương tự tiêu chí từng dùng khi xem Superfile trong terminal. Tuy vậy, “ít dependency” chỉ là một lợi ích vận hành; nó không tự bù cho reporter hay tích hợp mà đội đang thực sự cần.

Nhưng mình chưa xem đây là thay thế toàn diện. c8 có hệ sinh thái và workflow report lâu năm quanh V8 coverage. Vitest coverage tích hợp sâu với runner, cấu hình workspace và trải nghiệm frontend. nyc/Istanbul vẫn phổ biến ở pipeline cần reporter, remap, merge output hoặc convention đã ổn định.

Nhu cầuTest runner Node 26.7Công cụ ngoài vẫn có lợi
Repo nhỏ, JavaScript thuầnGọn, đủ threshold và lcovKhông bắt buộc
File chưa từng importinclude-all xử lý trực tiếpCần đối chiếu cấu hình tương đương
Monorepo nhiều packageCần quản lý working directory và glob kỹWorkflow merge/report có thể tiện hơn
Frontend, TS, transform phức tạpPhải thử source map và reporter thực tếVitest/c8 thường quen tay hơn

Mình không benchmark tốc độ và cũng chưa migrate một monorepo thật trong lần này, nên không kết luận công cụ nào nhanh hơn. Nếu repo đang có pipeline coverage ổn, lý do đổi nên là giảm độ phức tạp hoặc sửa một lỗ hổng cụ thể, không phải vì cờ mới nhìn vui mắt.

Chỗ này vẫn là experimental trên nhánh Current

API test runner đã trưởng thành qua nhiều phiên bản, nhưng phần collecting code coverage và các cờ threshold/include vẫn mang Stability 1 — Experimental trong tài liệu 26.7. Node.js 26 hiện là Current và dự kiến chuyển LTS theo lịch phát hành của dự án, nên mình không khuyên nâng production chỉ để lấy một cờ test.

Nếu hệ thống đang chạy LTS, cách an toàn là tách runtime của job coverage khỏi runtime production, pin phiên bản trong CI, giữ lockfile và có đường lui về lệnh cũ. Bài Node.js hoãn bản vá bảo mật mình từng viết cũng có cùng nguyên tắc: inventory và canary quan trọng hơn việc chạy theo nhãn “mới nhất”.

Một giới hạn khác là cờ tìm từ current working directory. Chạy lệnh ở root monorepo và chạy trong từng package có thể tạo hai tập ứng viên khác nhau. Trước khi khóa threshold, hãy lưu report lcov hoặc output text để review chính xác file nào vừa được đưa vào mẫu số.

Khi nào mình sẽ bật cờ này mặc định?

Mình sẽ bật --test-coverage-include-all cho repo nhỏ hoặc vừa đang dùng node:test, sau khi viết include/exclude đủ hẹp và chạy thử vài vòng CI. Giá trị lớn nhất của nó không phải kéo coverage lên; ngược lại, nó làm con số xấu đi theo cách trung thực hơn bằng cách đưa file bị quên trở lại báo cáo.

  • Chạy baseline và lưu danh sách file đang được tính.
  • Bật include-all, review từng file 0% thay vì sửa threshold ngay.
  • Loại generated, build output và test fixture bằng glob có chủ đích.
  • Đặt line threshold trước; kiểm tra riêng ý nghĩa branch/function với file chưa nạp.
  • Pin Node 26.7 trong job thử nghiệm vì coverage vẫn experimental.

Trên máy mình, thay đổi một cờ đã biến báo cáo 100% exit 0 thành 33,33% exit 1 mà không đổi test. Oke, bảng đỏ hơn, nhưng đó là cái đỏ đáng có. Anh em đang dùng test runner tích hợp có thể dựng fixture nhỏ tương tự trước khi áp dụng cho cả repo; nếu kết quả branch/function khác, góp lại để mình đối chiếu thêm.

Nguồn và phạm vi kiểm tra

  • Node.js 26.7.0 release note, ngày nguồn 05/08/2026; kiểm tra 09/08/2026.
  • Node.js CLI và test runner docs tại tag v26.7.0; kiểm tra 09/08/2026.
  • nodejs/node PR #64830, mở 29/07/2026 và landed 31/07/2026; kiểm tra 09/08/2026.
  • c8 README và Vitest coverage guide; chỉ dùng để đối chiếu phạm vi công cụ, kiểm tra 09/08/2026.

Lab chỉ chạy trên Windows 11 x64 với Node.js v26.7.0. Mình chưa thử macOS, Linux, TypeScript transform, source map hay monorepo; không có số benchmark trong bài.