Product Management
Đăng nhập
ESC

Nhập từ khóa để tìm kiếm

↑↓ Di chuyển
Enter Mở
ESC Đóng

Snapshot Testing với Postman

Mở đầu — vì sao bài này quan trọng

Hãy tưởng tượng bạn là QA của một sàn thương mại điện tử. Đội backend triển khai một bản vá "nhỏ" cho API /products/{id} — họ chỉ định sửa cách format giá tiền. Nhưng vô tình, trong quá trình refactor, trường discount_percent bị đổi tên thành discountPercent, và trường warranty_months biến mất khỏi response. Không có test nào của bạn fail, vì tất cả assertion hiện có chỉ kiểm tra status === 200pm.expect(json.price).to.be.a('number'). Ba ngày sau, khách hàng phàn nàn: app mobile không còn hiển thị bảo hành, còn phần giảm giá thì trống trơn.

Đây chính là loại lỗi mà snapshot testing sinh ra để bắt. Ý tưởng cực kỳ đơn giản nhưng mạnh mẽ: thay vì viết tay từng assertion cho từng trường (điều gần như bất khả thi với response có 50–100 trường), bạn chụp lại một "ảnh chụp" (snapshot) của response ở lần chạy đã biết là đúng, lưu nó lại, và ở những lần chạy sau bạn so sánh response mới với snapshot cũ. Bất kỳ sự khác biệt nào — trường bị xóa, thêm, đổi tên, đổi kiểu dữ liệu, đổi cấu trúc lồng nhau — đều bị phát hiện ngay lập tức.

Nếu bạn từng viết test cho React (Jest snapshot) hoặc test giao diện, bạn đã quen với triết lý này. Trong bài này chúng ta sẽ áp dụng nó vào API testing bằng Postman thuần — không cần thư viện ngoài, chỉ dùng những gì pm.* và biến (variables) đã cho sẵn. Đây là một kỹ thuật "tính chi phí thấp, giá trị cao" mà rất ít QA Việt Nam tận dụng, và nó sẽ giúp bạn bắt được cả một lớp lỗi regression mà assertion thủ công bỏ sót.

Khái niệm cốt lõi

Snapshot testing là gì

Snapshot testing là kỹ thuật kiểm thử trong đó bạn lưu trạng thái đầu ra "đúng" của một hệ thống dưới dạng dữ liệu tham chiếu (baseline/reference), rồi ở các lần chạy tiếp theo, so sánh đầu ra thực tế với baseline đó. Nếu khác nhau, test sẽ báo lỗi — buộc con người phải xem xét: đây là regression cần sửa, hay là thay đổi hợp lệ cần cập nhật lại snapshot.

Trong ngữ cảnh API, "đầu ra" thường là cấu trúc (shape) của JSON response — tức là có những trường nào, kiểu dữ liệu của chúng, cấu trúc lồng nhau ra sao. Chú ý điểm mấu chốt: snapshot testing quan tâm nhiều đến cấu trúc và hình dạng hơn là giá trị cụ thể. Bởi vì giá trị (như id, created_at, total_orders) thay đổi liên tục, nhưng cấu trúc thì phải ổn định.

So sánh với assertion truyền thống

Assertion thủ công (mà bạn học ở Bài 8 — Tests Scripts) trả lời câu hỏi "trường X có đúng giá trị Y không?". Bạn phải biết trước mình muốn kiểm tra gì và viết từng dòng.

Snapshot testing trả lời câu hỏi khác: "response lần này có còn giống hệt lần trước về mặt cấu trúc không?". Bạn không cần liệt kê trước — snapshot bắt được cả những thay đổi mà bạn không ngờ tới. Đây là điểm khác biệt sống còn:

  • Assertion truyền thống: whitelist — chỉ bắt những gì bạn đã nghĩ ra.
  • Snapshot testing: bắt mọi thay đổi ngoài dự kiến — kể cả những gì bạn chưa từng nghĩ tới.
Hai kỹ thuật này bổ sung cho nhau, không thay thế. Assertion kiểm tra logic nghiệp vụ ("giá phải > 0"); snapshot kiểm tra tính ổn định hợp đồng ("cấu trúc không được đổi ngoài ý muốn").

Snapshot đầy đủ vs. snapshot cấu trúc (structural snapshot)

Có hai cách tiếp cận:

  • Snapshot toàn bộ giá trị: lưu nguyên response và so sánh từng byte. Cách này rất chặt nhưng cực kỳ "giòn" (brittle) — chỉ cần một timestamp thay đổi là fail. Hầu như không dùng được cho API thật.
  • Snapshot cấu trúc: chỉ lưu "bộ khung" — danh sách các key và kiểu dữ liệu của chúng, bỏ qua giá trị động. Đây là cách thực dụng nhất cho API và là trọng tâm của bài này.
Để làm snapshot cấu trúc, ta thường viết một hàm "trích khung" (schema extractor): duyệt đệ quy qua JSON và tạo ra một bản đồ đường_dẫn → kiểu_dữ_liệu. Ví dụ { "user.name": "string", "user.age": "number", "items[]": "array" }.

Nơi lưu snapshot trong Postman

Postman không có "file system" cho collection chạy trên cloud, nên ta lưu snapshot vào biến — thường là collection variable hoặc environment variable (Bài 6). Lần chạy đầu tiên (không tìm thấy snapshot) sẽ tạo baseline và lưu lại. Các lần sau sẽ đọc baseline ra và so sánh. Khi chạy qua Newman (Bài 13), bạn export environment sau mỗi lần chạy để giữ snapshot bền vững giữa các lần chạy.

Tình huống thực tế

Tình huống 1: Tiki và trường bị "bốc hơi" sau refactor

Một đội QA giả định tại một sàn TMĐT lớn ở Việt Nam (gọi là "TikiMart") có collection kiểm thử API danh mục sản phẩm. Response /api/v2/products/{id} có 34 trường. Đội backend chuyển ORM từ raw SQL sang một layer mới. Trong quá trình đó, hai trường ít dùng — origin_countrytax_included — bị rớt khỏi câu SELECT.

Các test cũ chỉ assert 6 trường quan trọng, nên tất cả đều xanh. Nhưng khi bật snapshot cấu trúc, lần chạy sau refactor báo ngay:

AssertionError: Snapshot mismatch
  • Missing keys: origin_country, tax_included

Đội bắt được lỗi trước khi lên production, chỉ 4 phút sau khi backend push code lên môi trường staging (Bài 5). Bài học: snapshot bắt được những trường "phụ" mà không ai buồn viết assertion riêng — chính chúng lại là nguồn lỗi âm thầm nhất.

Tình huống 2: Fintech đổi kiểu dữ liệu số dư — lỗi tiền tỷ suýt xảy ra

Một ví điện tử (giả định "PayViet") có API /wallet/balance trả về { "balance": 1500000, "currency": "VND" }. Một hôm, đội backend "chuẩn hóa" số tiền sang chuỗi để tránh lỗi làm tròn floating-point: { "balance": "1500000", "currency": "VND" }.

Về mặt hiển thị thì trông giống hệt. Nhưng app mobile parse balance như số và cộng phí giao dịch vào — giờ nó nối chuỗi thành "1500000" + 5000 = "15000005000". Một thảm họa.

Assertion cũ dùng pm.expect(balance).to.equal(1500000) — thực ra vẫn pass ở một số nơi do so sánh lỏng. Nhưng snapshot cấu trúc lưu "balance": "number" ở baseline, và lần này thấy "balance": "string":

AssertionError: Type mismatch at 'balance'
  expected: number
  actual:   string

Bài học: thay đổi kiểu dữ liệu là loại regression nguy hiểm bậc nhất trong fintech, và assertion giá trị thường bỏ sót. Snapshot theo kiểu dữ liệu bắt nó ngay tức khắc.

Tình huống 3: Startup SaaS và cái bẫy "snapshot quá giòn"

Một startup SaaS ở Đà Nẵng áp dụng snapshot testing nhưng làm sai: họ lưu toàn bộ response bao gồm cả giá trị. API /dashboard/summarygenerated_at (timestamp) và request_id (UUID ngẫu nhiên). Kết quả: mỗi lần chạy đều fail, dù chẳng có gì hỏng. Sau 2 tuần, cả đội mất niềm tin và tắt hết snapshot test — hiện tượng "alert fatigue" (mệt mỏi vì báo động giả).

Họ sửa lại bằng cách chuyển sang snapshot cấu trúc, và bổ sung danh sách "trường động cần bỏ qua" (ignore list): ["generated_at", "request_id", "data.*.updated_at"]. Từ đó snapshot chỉ fail khi cấu trúc thật sự đổi. Bài học: snapshot chỉ hữu ích khi nó ổn định. Phải chủ động loại bỏ các trường động, nếu không bạn sẽ tự huấn luyện cả đội bỏ qua báo động.

Hướng dẫn từng bước

Ta sẽ xây dựng một snapshot cấu trúc hoàn chỉnh trong tab Tests của một request.

Bước 1 — Viết hàm trích khung cấu trúc. Hàm này duyệt đệ quy JSON và trả về map đường_dẫn → kiểu:

function extractSchema(obj, prefix = '') {
    const schema = {};
    if (Array.isArray(obj)) {
        // Chỉ lấy phần tử đầu làm đại diện cho mảng (tránh phụ thuộc số lượng)
        schema[prefix + '[]'] = 'array';
        if (obj.length > 0) {
            Object.assign(schema, extractSchema(obj[0], prefix + '[]'));
        }
    } else if (obj !== null && typeof obj === 'object') {
        for (const key of Object.keys(obj).sort()) {
            const path = prefix ? ${prefix}.${key} : key;
            const value = obj[key];
            if (value !== null && typeof value === 'object') {
                Object.assign(schema, extractSchema(value, path));
            } else {
                schema[path] = value === null ? 'null' : typeof value;
            }
        }
    }
    return schema;
}

Lưu ý ta .sort() các key để thứ tự không ảnh hưởng kết quả so sánh, và với mảng chỉ lấy phần tử đầu làm đại diện — nhờ đó snapshot không fail chỉ vì số lượng phần tử đổi.

Bước 2 — Loại bỏ trường động. Định nghĩa danh sách bỏ qua và lọc chúng khỏi schema:

const IGNORE_KEYS = ['request_id', 'generated_at', 'server_time'];
function stripIgnored(schema) {
    const result = {};
    for (const key of Object.keys(schema)) {
        const leaf = key.split('.').pop().replace('[]', '');
        if (!IGNORE_KEYS.includes(leaf)) result[key] = schema[key];
    }
    return result;
}

Bước 3 — Lấy schema hiện tại và baseline đã lưu:

const current = stripIgnored(extractSchema(pm.response.json()));
const snapshotKey = 'snapshot_' + pm.info.requestName;
const stored = pm.collectionVariables.get(snapshotKey);

Bước 4 — Nếu chưa có baseline thì tạo mới; nếu có thì so sánh:

if (!stored) {
    pm.collectionVariables.set(snapshotKey, JSON.stringify(current));
    console.log('Đã tạo snapshot baseline cho: ' + pm.info.requestName);
} else {
    const baseline = JSON.parse(stored);
    const missing = [], added = [], typeChanged = [];

for (const key of Object.keys(baseline)) { if (!(key in current)) missing.push(key); else if (baseline[key] !== current[key]) typeChanged.push(${key}: ${baseline[key]} → ${current[key]}); } for (const key of Object.keys(current)) { if (!(key in baseline)) added.push(key); }

pm.test('Response khớp snapshot cấu trúc', function () { const problems = []; if (missing.length) problems.push('Thiếu: ' + missing.join(', ')); if (added.length) problems.push('Thêm mới: ' + added.join(', ')); if (typeChanged.length) problems.push('Đổi kiểu: ' + typeChanged.join('; ')); pm.expect(problems.length, problems.join(' | ')).to.equal(0); }); }

Bước 5 — Cơ chế cập nhật snapshot có chủ đích. Khi API thay đổi hợp lệ, bạn cần "duyệt" snapshot mới. Thêm một biến cờ:

if (pm.environment.get('UPDATE_SNAPSHOTS') === 'true') {
    pm.collectionVariables.set(snapshotKey, JSON.stringify(current));
    console.log('Đã cập nhật snapshot cho: ' + pm.info.requestName);
}

Đặt UPDATE_SNAPSHOTS=true khi bạn chủ động muốn ghi đè baseline, rồi tắt đi.

Bước 6 — Bền hóa snapshot qua Newman. Trên máy hoặc CI, chạy với --export-environment để lưu lại biến snapshot cho lần chạy sau:

newman run collection.json -e env.json \
  --export-environment env.json

Lần đầu tạo baseline, lần sau đọc lại. Bạn nên commit file env.json (đã chứa snapshot) vào Git để cả đội dùng chung một baseline.

Lỗi thường gặp & mẹo

Lỗi 1 — Snapshot toàn giá trị nên fail liên tục. Đây là cái bẫy phổ biến nhất (xem tình huống 3). Luôn snapshot cấu trúc + kiểu, không phải giá trị. Nếu bắt buộc phải kiểm tra một vài giá trị cụ thể, dùng assertion thường cho riêng chúng.

Lỗi 2 — Quên loại trừ trường động. Timestamp, UUID, token, total_count thay đổi liên tục sẽ phá snapshot. Duy trì một IGNORE_KEYS rõ ràng và review nó định kỳ.

Lỗi 3 — Baseline "bẩn" ngay từ đầu. Nếu lần chạy tạo baseline lại trúng lúc API đang lỗi (ví dụ trả về thiếu trường do bug), bạn đã "đóng băng cái sai". Luôn tạo baseline ở môi trường ổn định, đã kiểm chứng bằng mắt, rồi mới khóa lại.

Lỗi 4 — Lấy cả mảng làm snapshot theo từng phần tử. Nếu bạn extract mọi phần tử của mảng, snapshot sẽ phụ thuộc số lượng và fail vô lý. Chỉ lấy một phần tử đại diện như code ở trên.

Mẹo — Phân biệt "regression" và "thay đổi hợp lệ". Khi snapshot fail, đừng vội cập nhật baseline. Hãy hỏi backend: thay đổi này có chủ đích không? Nếu có → cập nhật baseline và ghi chú trong commit. Nếu không → đó là bug, giữ nguyên baseline để test tiếp tục đỏ.

Mẹo — Kết hợp với JSON Schema Validation (Bài 10). Snapshot tự sinh baseline nhanh và bắt thay đổi ngoài dự kiến; JSON Schema là hợp đồng viết tay chặt chẽ hơn. Với API quan trọng, dùng cả hai: snapshot cho vòng phản hồi nhanh, schema cho ràng buộc chính thức.

Mẹo — Đặt tên snapshot theo request cộng môi trường. Ví dụ snapshot_GetProduct_staging. Cấu trúc response giữa dev/staging/prod có thể khác nhau (feature flag), nên tách baseline theo môi trường tránh so sánh nhầm.

Bài tập thực hành

  • Xây snapshot cơ bản: Chọn một API công khai bất kỳ (ví dụ https://reqres.in/api/users/2). Viết đầy đủ đoạn code từ Bước 1 đến Bước 4 vào tab Tests. Chạy lần đầu để tạo baseline, xem console log xác nhận.
  • Giả lập regression: Dùng một Mock Server (Bài 2/19) trả về response có cấu trúc cố định. Chạy tạo baseline. Sau đó sửa mock để (a) xóa một trường, (b) đổi một trường số thành chuỗi. Chạy lại và xác nhận snapshot test báo đúng cả hai loại lỗi với thông điệp rõ ràng.
  • Xử lý trường động: Thêm vào response một trường server_time chứa timestamp. Chạy hai lần liên tiếp — chứng minh snapshot không fail nhờ IGNORE_KEYS. Sau đó thử bỏ trường này khỏi ignore list và quan sát nó fail như thế nào.
  • Bền hóa qua Newman: Export collection và environment, chạy bằng Newman với --export-environment. Chạy hai lần và xác nhận lần hai đọc lại baseline đã lưu từ lần một thay vì tạo mới.
  • Nâng cao: Cải tiến hàm so sánh để phân biệt trường "optional" (đôi khi có, đôi khi không xuất hiện) — ví dụ chỉ cảnh báo (warning) chứ không fail khi trường nằm trong danh sách optional bị thiếu.

Tóm tắt

Snapshot testing là kỹ thuật "chụp ảnh" cấu trúc response ở trạng thái đúng rồi so sánh các lần chạy sau để bắt mọi thay đổi ngoài dự kiến. Điểm mấu chốt là snapshot cấu trúc và kiểu dữ liệu, không phải giá trị — nhờ đó test ổn định mà vẫn bắt được các regression âm thầm nhất: trường bị xóa, thêm, đổi tên, hay đổi kiểu (như case fintech biến number thành string). Nó bổ sung chứ không thay thế assertion nghiệp vụ và JSON Schema.

Ba nguyên tắc để snapshot testing thành công: (1) luôn loại bỏ trường động để tránh báo động giả và alert fatigue; (2) tạo baseline ở môi trường sạch, đã kiểm chứng bằng mắt; (3) khi snapshot fail, luôn phân biệt regression thật với thay đổi hợp lệ trước khi cập nhật baseline. Trong Postman, ta lưu snapshot vào collection/environment variable và bền hóa qua Newman bằng --export-environment. Đây là kỹ thuật chi phí thấp, giá trị cao — chỉ vài chục dòng code nhưng che phủ được cả một lớp lỗi mà không assertion thủ công nào với tới.

Học xong bài này rồi? Tạo tài khoản miễn phí để lưu lại — lần sau vào là biết ngay đang dở ở đâu, và học hết khóa thì có chứng chỉ. Lưu tiến độ của tôi