۲۲. آزمون یکپارچه؛ بررسی API واقعی با دادهٔ مستقل
در آزمون واحد، canRead را با چند شیء صدا میزنیم. هنوز نمیدانیم مسیر HTTP واقعاً آن را اجرا میکند یا نه. آزمون یکپارچه سرور واقعی آزمایشگاه را روی درگاه آزاد بالا میآورد، نشست میسازد و پاسخ درخواستها را بررسی میکند. دادهٔ آزمون باید از کار دستی دانشجو جدا باشد تا اجرای دوباره، تیکتهای اصلی او را تغییر ندهد. فایل tests/api.test.mjs در بسته دقیقاً با پوشهٔ موقت کار میکند.
چرخهٔ راهاندازی و پاکسازی
نمونهٔ زیر یک فایل مستقل کامل است. آن را در ریشهٔ بسته با نام integration-demo.test.mjs ذخیره و node --test integration-demo.test.mjs اجرا کنید. import مسیر ./server.mjs به همین محل وابسته است. listen(0) درگاه آزاد میگیرد؛ چون سرور آزمایشگاه Host و Origin دقیق را بررسی میکند، از نشانی چاپشده یا origin ساختهشده استفاده کنید، نه دامنهٔ متفاوت.
import test from 'node:test';
import assert from 'node:assert/strict';
import { mkdtemp, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { once } from 'node:events';
import { createLab } from './server.mjs';
test('anonymous request cannot read tickets', async () => {
const dir = await mkdtemp(join(tmpdir(), 'darsavand-demo-'));
const server = createLab({ dataFile: join(dir, 'tickets.json') });
server.listen(0, '127.0.0.1');
await once(server, 'listening');
const origin = 'http://127.0.0.1:' + server.address().port;
try {
const response = await fetch(origin + '/api/tickets');
assert.equal(response.status, 401);
assert.equal(typeof (await response.json()).error, 'string');
} finally {
await new Promise(resolve => server.close(resolve));
await rm(dir, { recursive: true, force: true });
}
});پوشهٔ موقت فقط دادهٔ این آزمون را میگیرد. finally باعث میشود حتی با شکست assertion، سرور بسته و دادهٔ آزمایشی پاک شود. حذف روی dir ساختهشده توسط mkdtemp محدود است؛ مسیر آن را با پوشهٔ اصلی پروژه عوض نکنید. سرور اصلی npm start را لازم نیست همزمان بالا بیاورید، چون این آزمون نمونهٔ جداگانهٔ خودش را دارد.
نشست در Node مانند تب مرورگر نیست
fetch در این آزمون Node کوکی پاسخ را خودکار برای درخواست بعدی نگه نمیدارد. بنابراین آزمون همراه، Set-Cookie نشست ساختگی خودش را میخواند و در درخواست بعدی همان نشست آزمایشی را میفرستد. این کار در کد محلی آزمون است و ارتباطی با برداشت کوکی حساب واقعی سایت ندارد. نمونهٔ دوم زیر را داخل try مثال قبلی، پس از بررسی 401، اضافه کنید.
const login = await fetch(origin + '/api/demo-session', {
method: 'POST',
headers: { Origin: origin, 'Content-Type': 'application/json' },
body: JSON.stringify({ profile: 'alice' })
});
assert.equal(login.status, 200);
const cookie = login.headers.get('set-cookie').split(';')[0];
const made = await fetch(origin + '/api/tickets', {
method: 'POST',
headers: { Origin: origin, 'Content-Type': 'application/json', Cookie: cookie },
body: JSON.stringify({
title: 'تمرین آزمون یکپارچه',
message: 'این داده فقط برای اجرای آزمون در پوشهٔ موقت ساخته شده است.',
category: 'course'
})
});
assert.equal(made.status, 201);
assert.equal((await made.json()).owner, 'alice');Origin را از همان آدرس سرور میسازیم؛ حذف آن در POST این آزمایشگاه باید 403 بدهد. این انتظار جزئی از قرارداد است، نه مزاحم آزمون. اگر برای سبزشدن تست بررسی Origin را حذف کنید، چیزی را که باید بسنجید دور زدهاید. هر ورودی نامعتبر باید عمداً در آزمون منفی مستقل بررسی شود تا شکست بعدی علت روشنی داشته باشد.
سناریوی کامل چه چیزی را ثابت میکند؟
آزمون همراه ابتدا دسترسی ناشناس و مبدأ غیرمجاز را رد میکند، سپس آلیس و باب و کارشناس آزمایشی را میسازد. ساخت، مالکیت، نسخهٔ کهنه، دو نوشتن همزمان، بستن و بازگشایی را بررسی میکند. در پایان فایل داده را میخواند تا وجود تغییر ذخیرهشده هم سنجیده شود. شمار درخواستها بهتنهایی مهم نیست؛ رابطهٔ علت و نتیجه مهم است. مثلاً 404 باب باید روی تیکتی باشد که واقعاً توسط آلیس ساخته شده است.
این آزمون، ذخیرهشدن داده در فایل را بررسی میکند؛ سناریوی راهاندازی دوبارهٔ سرور و بازیابی پس از خرابی دیسک را کامل پوشش نمیدهد. همچنین فوکوس، رنگ، اندازهٔ موبایل و خواندن پیام خطا توسط ابزار کمکی را نمیسنجد. ادعای «همه چیز تست شد» از یک تست API نتیجه نمیشود. در گزارش هر آزمون محدودهٔ اثبات و محدودیت را بنویسید.
تمرین، پاسخ راهنما و معیار تحویل
آزمون جدا بنویسید که ساخت تیکت با دستهٔ ناشناخته 400 بگیرد و پس از آن فهرست آلیس هنوز خالی باشد. پاسخ راهنما: پیش از ساخت نامعتبر نشست معتبر لازم است، وگرنه 401 علت دیگری را میسنجد. بعد از درخواست ردشده GET بزنید و items را با [] مقایسه کنید. این بخش نشان میدهد رد درخواست اثر نیمهکاره نگذاشته است.
برای تحویل، فایل مستقل باید دو بار پشت سر هم پاس شود، به درگاه ثابت وابسته نباشد و فایل دادهٔ اصلی را تغییر ندهد. خروجی شکست را هنگام خرابکردن عمدی یک انتظار ببینید و سپس آن را برگردانید. اگر اجرا تمام نمیشود، ابتدا Promiseهای awaitنشده و server.close را بررسی کنید؛ افزودن تأخیر تصادفی راه حل قابل اعتماد نیست.
منابع رسمی برای مطالعهٔ بیشتر
https://nodejs.org/docs/latest-v24.x/api/test.html
https://nodejs.org/docs/latest-v24.x/api/http.html
https://nodejs.org/docs/latest-v24.x/api/events.html
تألیف و مثالهای اختصاصی: تحریریه درسآوند.