Contract Testing với Pact: Giải pháp ‘chống gãy’ API cho Microservices Node.js

Development tutorial - IT technology blog
Development tutorial - IT technology blog

Nỗi ám ảnh khi Integration Test ‘phình to’ theo quy mô hệ thống

Bạn đã bao giờ rơi vào cảnh: Unit Test của Service A xanh, Service B cũng xanh, nhưng vừa deploy lên Staging thì hệ thống sập? Lỗi thường gặp nhất là Producer thay đổi cấu trúc JSON trả về khiến Consumer ‘gãy’ code, dù cả hai vẫn pass test riêng lẻ.

Thông thường, chúng ta sẽ viết thêm End-to-End (E2E) test để an tâm. Tuy nhiên, khi hệ thống chạm mốc 20-30 service, việc dựng môi trường đủ để chạy test sẽ ngốn hàng chục GB RAM và cả tiếng đồng hồ chờ đợi. Tôi từng làm một dự án mà bộ Integration Test chạy mất 45 phút, khiến tốc độ release của team bị kéo lùi thảm hại.

Contract Testing ra đời để giải quyết bài toán này. Trong đó, Pact là công cụ tiêu chuẩn giúp bạn xác thực sự tương thích giữa các service mà không cần dựng toàn bộ hệ thống lên để test.

Contract Testing: Bản giao kèo giữa các Service

Hãy coi Contract Testing như một bản hợp đồng kinh tế. Bên thuê (Consumer) và bên cung cấp (Producer) ký kết các điều khoản về dữ liệu. Chỉ cần cả hai tuân thủ đúng bản hợp đồng này, họ không cần phải gặp mặt trực tiếp (Integration Test) mà vẫn đảm bảo công việc trôi chảy.

Pact hoạt động theo mô hình Consumer-Driven Contract Testing (CDCT). Phía Consumer sẽ chủ động định nghĩa: “Tôi cần API này trả về đúng cấu trúc này”. Một file JSON (Pact file) được sinh ra làm bằng chứng. Sau đó, Producer chỉ cần lấy file này về để kiểm chứng xem code của mình có đáp ứng đúng kỳ vọng hay không.

Thực hành: Triển khai Pact trong dự án Node.js

Giả sử chúng ta có Order Service (Consumer) cần lấy thông tin từ Product Service (Producer).

1. Thiết lập phía Consumer (Order Service)

Đầu tiên, hãy cài đặt thư viện:

npm install @pact-foundation/pact --save-dev

Chúng ta sẽ viết một đoạn test để mô phỏng request và định nghĩa cấu trúc dữ liệu mong muốn từ API /products/:id.

// product.pact.test.js
const { PactV3 } = require('@pact-foundation/pact');
const path = require('path');

const provider = new PactV3({
  consumer: 'OrderService',
  provider: 'ProductService',
  dir: path.resolve(process.cwd(), 'pacts'),
});

describe('GET /product/:id', () => {
  it('trả về thông tin sản phẩm khi ID tồn tại', async () => {
    provider.addInteraction({
      states: [{ description: 'product with ID 10 exists' }],
      uponReceiving: 'a request for product 10',
      withRequest: {
        method: 'GET',
        path: '/products/10',
      },
      willRespondWith: {
        status: 200,
        headers: { 'Content-Type': 'application/json' },
        body: {
          id: '10',
          name: 'Bàn phím cơ',
          price: 1500000,
        },
      },
    });

    await provider.executeTest(async (mockService) => {
      // Gọi hàm thực tế trong source code của bạn
      // const response = await getProduct(mockService.url, '10');
      // expect(response.data.name).toBe('Bàn phím cơ');
    });
  });
});

Sau khi chạy test, Pact sẽ tự động xuất ra một file JSON trong thư mục /pacts. Đây chính là “hợp đồng” để gửi cho phía Producer.

2. Xác minh phía Producer (Product Service)

Ở phía Producer, nhiệm vụ rất đơn giản: Đảm bảo phản hồi của API khớp với những gì Consumer đã yêu cầu trong file JSON.

// verify-pact.test.js
const { Verifier } = require('@pact-foundation/pact');

describe('Pact Verification', () => {
  it('phải tuân thủ hợp đồng từ OrderService', async () => {
    const opts = {
      provider: 'ProductService',
      providerBaseUrl: 'http://localhost:3000',
      pactUrls: [path.resolve(__dirname, '../../pacts/orderservice-productservice.json')],
    };

    return new Verifier(opts).verifyProvider();
  });
});

Khi thực thi, Pact tự động gửi các request giả lập tới server Producer và so khớp response trả về. Nếu bạn lỡ tay đổi field name thành product_name, test sẽ fail ngay lập tức.

Kinh nghiệm thực tế để không ‘tẩu hỏa nhập ma’

Áp dụng Pact vào dự án thực tế có thể gặp khó khăn nếu bạn không nắm vững 3 nguyên tắc sau:

  • Đừng test logic nghiệp vụ: Pact chỉ dùng để kiểm tra cấu trúc dữ liệu (Schema). Đừng cố test xem giá sản phẩm có tính đúng thuế hay không. Việc đó hãy để Unit Test lo.
  • Sử dụng Pact Broker: Đừng copy file JSON thủ công. Hãy dùng Pact Broker để quản lý version. Công cụ này cung cấp tính năng can-i-deploy cực kỳ đáng giá, giúp bạn biết chính xác service A có an toàn để lên Production hay không.
  • Giảm tải Integration Test: Khi đã có Pact, bạn có thể tự tin cắt giảm đến 60-70% các kịch bản E2E rườm rà, giúp pipeline CI/CD chạy nhanh hơn đáng kể.

Lời kết

Contract Testing không phải là liều thuốc vạn năng, nhưng nó là tấm khiên bảo vệ hệ thống Microservices khỏi những lỗi thay đổi API ngớ ngẩn. Nếu bạn đang mệt mỏi vì phải fix bug sau mỗi lần deploy, hãy thử đưa Pact vào quy trình phát triển ngay hôm nay. Sự yên tâm khi thấy các bản hợp đồng được verify thành công là cảm giác rất xứng đáng để đầu tư.

Share: