•8 min read

Kiểm thử Vue Components trong Trình duyệt Thực: Kiến trúc, Tính phản ứng và QUnit

Kiểm thử Vue Components trong Trình duyệt Thực: Kiến trúc, Tính phản ứng và QUnit

Hầu hết các quy trình kiểm thử frontend hiện đại chạy bên trong NodeJS sử dụng các môi trường DOM mô phỏng như jsdom hoặc happy-dom. Mặc dù các DOM mô phỏng này nhanh, nhưng chúng gặp phải những điểm mù cơ bản: chúng không tính toán hình học bố cục CSS thực (getBoundingClientRect), chúng không hiển thị đồ họa bộ lọc SVG và chúng không thể mô phỏng chính xác hành vi vẽ và nổi sự kiện của trình duyệt thực.

Khi kiểm thử các thành phần UI tương tác phức tạp—như đồ họa canvas, trình soạn thảo văn bản phong phú, bảng kéo thả động hoặc hoạt ảnh có độ trung thực cao—việc chạy kiểm thử trực tiếp bên trong một phiên bản trình duyệt thực sẽ loại bỏ khoảng cách về độ trung thực.

Trong hướng dẫn này, chúng ta sẽ cùng xây dựng một trình chạy kiểm thử nhẹ, hiệu suất cao trong trình duyệt cho các thành phần Vue 3 sử dụng QUnit, quản lý tính phản ứng không đồng bộ với nextTick(), và so sánh kiến trúc này với các trình chạy trình duyệt không đầu hiện đại.


Audio Briefing
0:00 / 0:00

Tại sao phải kiểm thử bên trong một trình duyệt thực?

Các môi trường DOM mô phỏng NodeJS (như JSDOM) là các ước tính gần đúng của các API trình duyệt. Dưới đây là những nơi mà các môi trường mô phỏng thất bại trong sản xuất:

  1. Bố cục & Hình học: jsdom trả về 0 cho tất cả các kích thước phần tử (offsetHeight, clientWidth, getBoundingClientRect). Nếu thành phần của bạn điều chỉnh bố cục dựa trên kích thước vùng chứa, các kiểm thử trong JSDOM sẽ vô dụng.
  2. Tiêu điểm & Lựa chọn: Các vòng tiêu điểm, phạm vi lựa chọn và chỉ mục tab bàn phím cụ thể của trình duyệt thường hoạt động khác nhau giữa các công cụ kết xuất trình duyệt thực (Blink so với WebKit).
  3. Gỡ lỗi trực quan tức thì: Khi một kiểm thử trong trình duyệt thất bại, thành phần thực vẫn được hiển thị trên màn hình của bạn. Bạn có thể mở Chrome DevTools, kiểm tra các kiểu CSS đã tính toán và đặt các điểm dừng trực tiếp trong thành phần đang chạy.

Advertisement

Thiết lập trình chạy kiểm thử trong trình duyệt

QUnit là một trong những framework kiểm thử JavaScript lâu đời nhất, đáng tin cậy nhất. Nó đi kèm với một giao diện trình duyệt sạch và một vùng chứa DOM #qunit-fixture được cách ly tự động đặt lại sau mỗi lần thực thi kiểm thử.

1. HTML của bộ kiểm thử (test-runner.html)

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Vue 3 Browser Component Tests</title>
  <link rel="stylesheet" href="https://code.jquery.com/qunit/qunit-2.20.0.css">
</head>
<body>
  <div id="qunit"></div>
  <div id="qunit-fixture"></div>

  <!-- Load QUnit, Vue 3, and Bundled Components -->
  <script src="https://code.jquery.com/qunit/qunit-2.20.0.js"></script>
  <script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
  <script src="./dist/components.bundle.js"></script>
  <script src="./tests/components.test.js"></script>
</body>
</html>

2. Trình trợ giúp gắn kết

Tạo một tiện ích gắn kết được cách ly để liên kết các thành phần vào #qunit-fixture và trả về các xử lý gỡ bỏ:

// test-helpers.js
function mountVueComponent(Component, props = {}, initialData = {}) {
  const fixture = document.getElementById('qunit-fixture');
  const container = document.createElement('div');
  fixture.appendChild(container);

  const app = Vue.createApp({
    render() {
      return Vue.h(Component, {
        ...props,
        ref: 'componentInstance',
      });
    },
    data() {
      return initialData;
    }
  });

  const vm = app.mount(container);

  return {
    app,
    container,
    vm: vm.$refs.componentInstance,
    // Helper to query elements within this component's DOM
    find: (selector) => container.querySelector(selector),
    findAll: (selector) => Array.from(container.querySelectorAll(selector)),
    // Teardown
    unmount: () => app.unmount(),
  };
}

Kiểm thử tính phản ứng của Vue 3: Yêu cầu nextTick()

Vue cập nhật DOM không đồng bộ. Khi trạng thái phản ứng của một thành phần thay đổi, Vue không ngay lập tức thay đổi DOM. Thay vào đó, nó đệm các cập nhật trong một hàng đợi tác vụ nhỏ nội bộ để tránh các phép tính bố cục dư thừa.

Nếu kiểm thử của bạn khẳng định nội dung DOM ngay sau khi thay đổi trạng thái, khẳng định sẽ thất bại vì DOM chưa được làm mới:

// ❌ FAILS: DOM has not flushed updates yet
component.count++;
assert.equal(element.textContent, 'Count: 1'); // Fails! Text is still 'Count: 0'

Mẫu đúng: await Vue.nextTick()

Luôn chờ hàng đợi phản ứng của Vue hoàn tất:

// components.test.js
QUnit.module('CounterComponent Tests', (hooks) => {
  let mounted;

  hooks.afterEach(() => {
    if (mounted) mounted.unmount();
  });

  QUnit.test('increments counter on button click', async (assert) => {
    mounted = mountVueComponent(window.CounterComponent, { initialCount: 5 });
    
    const button = mounted.find('button.increment');
    const display = mounted.find('span.count');

    assert.equal(display.textContent.trim(), '5', 'Initial count renders correctly');

    // Simulate real user click event
    button.dispatchEvent(new MouseEvent('click', { bubbles: true }));

    // Await reactivity batch
    await Vue.nextTick();

    assert.equal(display.textContent.trim(), '6', 'DOM updates after click event');
  });
});

Kiểm thử các biểu mẫu và đầu vào không đồng bộ

Mô phỏng người dùng gõ yêu cầu kích hoạt cả sự kiện input và change để các chỉ thị v-model của Vue đồng bộ hóa trạng thái nội bộ:

QUnit.module('FeedbackForm Tests', (hooks) => {
  let mounted;

  hooks.afterEach(() => {
    if (mounted) mounted.unmount();
  });

  QUnit.test('submits valid feedback and displays confirmation', async (assert) => {
    mounted = mountVueComponent(window.FeedbackForm);

    const input = mounted.find('input[name="email"]');
    const textarea = mounted.find('textarea[name="message"]');
    const form = mounted.find('form');

    // Populate form fields
    input.value = 'developer@locionic.com';
    input.dispatchEvent(new Event('input', { bubbles: true }));

    textarea.value = 'The browser test runner is incredibly fast!';
    textarea.dispatchEvent(new Event('input', { bubbles: true }));

    await Vue.nextTick();

    // Trigger form submit
    form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));

    // Wait for async submission simulation
    await new Promise((resolve) => setTimeout(resolve, 50));
    await Vue.nextTick();

    const successMessage = mounted.find('.alert-success');
    assert.ok(successMessage, 'Success message appears in the DOM');
    assert.includes(successMessage.textContent, 'Thank you', 'Correct confirmation text rendered');
  });
});

Advertisement

So sánh kiến trúc: QUnit trong trình duyệt so với Chế độ trình duyệt Vitest hiện đại

Trong những năm gần đây, hệ sinh thái frontend đã phát triển Chế độ trình duyệt Vitest (khởi chạy các phiên bản Chromium/Firefox/WebKit thực thông qua Playwright). Dưới đây là cách kiểm thử trong trình duyệt so sánh:

Tính năngBộ kiểm thử QUnit nhẹ trong trình duyệtChế độ trình duyệt VitestJSDOM (NodeJS)
Tốc độ thực thiDưới mili giây (Tức thì)Nhanh (~100ms mỗi tệp)Rất nhanh
Kết xuất trình duyệt thựcCó (DOM & CSS thực)Có (Chromium/WebKit)Không (DOM giả)
Tính toán hình học bố cụcChính xác 100%Chính xác 100%Không được hỗ trợ (0px)
Độ phức tạp thiết lậpKhông cần cài đặt (HTML tĩnh)Trung bình (cấu hình Vite)Thấp (trình chạy Node)
Tự động hóa CIYêu cầu Headless ChromeCLI Playwright tích hợp sẵnCLI Node gốc

Các câu hỏi thường gặp

Tại sao không chỉ sử dụng Playwright cho mọi thứ?

Playwright được thiết kế cho các hành trình người dùng end-to-end đầy đủ trải dài nhiều trang, API máy chủ và di chuyển cơ sở dữ liệu. Mặc dù mạnh mẽ, các kiểm thử E2E chậm hơn và yêu cầu khởi động các máy chủ backend đầy đủ. Kiểm thử thành phần trong trình duyệt mang lại cho bạn độ chính xác DOM trình duyệt thực với tốc độ và độ chi tiết của các kiểm thử đơn vị.

Làm cách nào để giả lập các yêu cầu HTTP API trong các kiểm thử trình duyệt?

Bạn có thể thay thế hàm window.fetch toàn cục bên trong bộ kiểm thử của mình hoặc sử dụng Mock Service Worker (MSW). MSW cài đặt một Service Worker trình duyệt gốc chặn các cuộc gọi HTTP đi ở lớp mạng trình duyệt, trả về các dữ liệu JSON xác định mà không làm thay đổi mã thành phần.

#qunit-fixture có dọn dẹp các trình nghe sự kiện của Vue không?

Vùng chứa #qunit-fixture dọn dẹp các phần tử DOM, nhưng nếu thành phần của bạn đã đính kèm các trình nghe vào window hoặc document (ví dụ: lắng nghe keydown hoặc resize), bạn phải đảm bảo phương thức unmount() của thành phần của bạn tách chúng một cách rõ ràng để tránh rò rỉ bộ nhớ giữa các kiểm thử.


Bạn cũng có thể thích

Share this article:

Stay Updated

Get the latest posts delivered straight to your inbox.

Free Developer Utilities

Free In-Browser Developer Tools

Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.

Explore Tools
Advertisement