0

Cách sử dụng hỗ trợ Vitest thử nghiệm trong Angular 20 bên ngoài ng test

Gần đây, nhóm Angular đã phát hành phiên bản 20 với nhiều cải tiến — hầu hết đã được thảo luận và giải thích rộng rãi ở nhiều nơi. Tuy nhiên, có một tính năng mới mà mình không tìm thấy nhiều thông tin: hỗ trợ thử nghiệm cho Vitest (ở dạng thử nghiệm).

Trước đó, đã có một số hỗ trợ Vitest dành cho Angular, nhờ dự án Analog và plugin của nó — mình cũng từng thử dùng từ thời Angular 18 và thấy hoạt động khá ổn. Nhưng giờ đây, Angular đã tự tích hợp hỗ trợ chính thức cho Vitest, điều này rất đáng chú ý. Vì vậy, mình quyết định tìm hiểu cách sử dụng.

May mắn thay, Angular đã cung cấp đủ tài liệu trong docs để giúp mình triển khai.

Tình huống thực tế

Mình đã thử nghiệm Angular 20 mới trong một dự án cá nhân để khám phá những tính năng này kết hợp với các công cụ mình yêu thích và thường dùng như VSCode, ESLint, Prettier,... Và mình đã thiết lập thành công.

Tuy nhiên, có một giới hạn quan trọng:

  • Hỗ trợ Vitest thử nghiệm chỉ hoạt động thông qua ng test.

Điều này không sao, nhưng với mình — một dev luôn thích cải thiện DX (developer experience) — thì như vậy chưa đủ. Mình muốn chạy test bằng npx vitest trực tiếp và dùng với Vitest extension chính thức của VSCode.

Giải pháp: dùng Vitest bên ngoài ng test

Sau khi thử đủ loại lỗi và nghiên cứu mã nguồn Angular, mình đã rút ra được một cách làm hoạt động được. Sau đây là kết quả, không cần bạn phải mày mò nhiều:

Yêu cầu ban đầu

  • Giả định bạn đã có một project Angular 20 đang chạy, đã cấu hình hỗ trợ Vitest, với component App và file test tương ứng.

1. src/test-setup.ts

import { NgModule } from '@angular/core';
import {
  ɵgetCleanupHook as getCleanupHook,
  getTestBed
} from '@angular/core/testing';
import {
  BrowserTestingModule,
  platformBrowserTesting
} from '@angular/platform-browser/testing';
import { afterEach, beforeEach } from 'vitest';

const providers: NgModule['providers'] = [];

beforeEach(getCleanupHook(false));
afterEach(getCleanupHook(true));

@NgModule({ providers })
export class TestModule {}

getTestBed().initTestEnvironment(
  [BrowserTestingModule, TestModule],
  platformBrowserTesting(),
  {
    errorOnUnknownElements: true,
    errorOnUnknownProperties: true
  }
);

2. vitest.config.ts

import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    root: './',
    globals: true,
    setupFiles: ['src/test-setup.ts'],
    environment: 'jsdom',
    watch: false,
    reporters: ['default'],
    coverage: {
      enabled: false,
      excludeAfterRemap: true
    }
  },
  plugins: [
    {
      name: 'angular-coverage-exclude',
      configureVitest(context) {
        context.project.config.coverage.exclude = ['**/*.{test,spec}.?(c|m)ts'];
      }
    }
  ]
});

3. src/app/app.spec.ts

import { readFileSync } from 'fs';

import {
  provideZonelessChangeDetection,
  ɵresolveComponentResources as resolveComponentResources
} from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';

import { App } from './app';

describe('App', () => {
  beforeAll(async () => {
    await resolveComponentResources(url =>
      Promise.resolve(readFileSync(new URL(url, import.meta.url), 'utf-8'))
    );
  });

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [App],
      providers: [provideZonelessChangeDetection()]
    }).compileComponents();
  });

  it('should create the app', () => {
    const fixture = TestBed.createComponent(App);
    const app = fixture.componentInstance;
    expect(app).toBeTruthy();
  });

  it('should render title', () => {
    const fixture = TestBed.createComponent(App);
    fixture.detectChanges();
    const compiled = fixture.nativeElement as HTMLElement;
    expect(compiled.querySelector('h1')?.textContent).toContain('Hello, ng20');
  });
});

Những điều cần lưu ý

Mặc dù cách làm này hoạt động với Vitest ngoài ng test (với Angular 20.0.0), nó có một số rủi ro do tính thử nghiệm:

  • Phụ thuộc vào tính năng thử nghiệm, có thể sẽ thay đổi trong tương lai.
  • Sử dụng biến nội bộ của Angular, có thể không ổn định nếu framework thay đổi.
  • Chưa được thử nghiệm rộng rãi, có thể phát sinh lỗi ở các edge-case.

Kết luận

Cách làm này chủ yếu mang tính thử nghiệm, dành cho những ai yêu thích khám phá công cụ, và hy vọng cũng là một khởi đầu để mở ra thảo luận về các cách triển khai tốt hơn, ít rủi ro hơn.

Mọi góp ý mang tính xây dựng đều rất hoan nghênh.

Cảm ơn bạn đã đọc! 🎉


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí