آزمون و کیفیت کد
Unit و Integration و E2E Testing، Test Case و Assertion، Mocking و Stubbing و Spying، Coverage، TDD، Linting و Formatting و کیفیت کد.
تست چیست و چرا؟
تست کدی است که کد دیگر را بررسی میکند:
- باگها را زود پیدا میکند
- جسارت تغییر میدهد — refactor بدون ترس
- مستندسازی زنده رفتار مورد انتظار
- کاهش هزینه رفع باگ در مراحل بعدی
هرم تست
▲ E2E (کم، کند، مطمئن)
▲▲▲
▲▲▲▲▲ Integration (متوسط)
▲▲▲▲▲▲▲
▲▲▲▲▲▲▲▲▲ Unit (زیاد، سریع، ارزان)
Unit Testing — تست واحد
تست کوچکترین واحد — یک تابع یا کلاس در ایزوله:
// sum.js — کد زیر تست:
function sum(a, b) {
return a + b;
}
function capitalize(str) {
if (!str) return "";
return str.charAt(0).toUpperCase() + str.slice(1);
}
module.exports = { sum, capitalize };
// sum.test.js — تست:
// describe — گروه تستها:
describe("تابع sum", () => {
// it/test — یک مورد تست:
test("جمع دو عدد مثبت", () => {
expect(sum(2, 3)).toBe(5);
});
test("با اعداد منفی", () => {
expect(sum(-1, -2)).toBe(-3);
});
test("با صفر", () => {
expect(sum(0, 0)).toBe(0);
});
});
describe("تابع capitalize", () => {
test("حرف اول بزرگ", () => {
expect(capitalize("hello")).toBe("Hello");
});
test("رشته خالی", () => {
expect(capitalize("")).toBe("");
});
});
Assertions — ادعاها
// Jest — رایجترین:
expect(2 + 2).toBe(4); // === دقیق
expect({a: 1}).toEqual({a: 1}); // عمیق — برای اشیا
expect(arr).toContain(3); // شامل
expect(str).toMatch(/regex/); // regex
expect(fn).toThrow(); // پرتاب خطا
expect(value).toBeNull();
expect(value).toBeDefined();
expect(value).toBeTruthy();
expect(value).toBeFalsy();
expect(n).toBeGreaterThan(5);
expect(n).toBeLessThanOrEqual(10);
// NOT — نفی:
expect(x).not.toBe(y);
expect(str).not.toContain("bad");
Setup و Teardown
describe("دیتابیس", () => {
let db;
// قبل از همه تستهای این describe:
beforeAll(async () => {
db = await connectDB();
});
// بعد از همه:
afterAll(async () => {
await db.close();
});
// قبل از هر تست:
beforeEach(() => {
db.clear();
});
// بعد از هر تست:
afterEach(() => {
// پاکسازی
});
test("insert کاربر", async () => {
await db.insert({ name: "علی" });
const count = await db.count();
expect(count).toBe(1);
});
});
Mocking — جعل وابستگیها
// mock کردن تابع:
test("فراخوانی callback", () => {
const mockCallback = jest.fn();
[1, 2, 3].forEach(mockCallback);
// بررسی فراخوانی:
expect(mockCallback).toHaveBeenCalledTimes(3);
expect(mockCallback).toHaveBeenCalledWith(1);
});
// mock کردن ماژول — مثل fetch:
jest.mock("./api");
test("دریافت کاربر", async () => {
// مقدار برگشتی جعلی:
api.getUser.mockResolvedValue({ id: 1, name: "علی" });
const user = await getUser(1);
expect(user.name).toBe("علی");
// بررسی فراخوانی:
expect(api.getUser).toHaveBeenCalledWith(1);
});
// mock برگرداندن خطا:
api.getUser.mockRejectedValue(new Error("Not Found"));
Stub و Spy
// Stub — جایگزین با رفتار مشخص:
const stub = jest.fn().mockImplementation(x => x * 2);
console.log(stub(5)); // 10
// Spy — رصد بدون تغییر:
const video = {
play() { return true; }
};
const spy = jest.spyOn(video, "play");
video.play();
expect(spy).toHaveBeenCalled();
// Mock پیادهسازی سفارشی:
jest.spyOn(Math, "random").mockReturnValue(0.5);
تست Async
// با async/await:
test("دریافت داده کاربر", async () => {
const data = await fetchUser(1);
expect(data).toHaveProperty("name");
});
// با resolves:
test("Promise موفق", () => {
expect(fetchUser(1)).resolves.toMatchObject({ id: 1 });
});
// با rejects:
test("Promise ناموفق", async () => {
await expect(fetchUser(-1)).rejects.toThrow("یافت نشد");
});
// با fake timers — برای setTimeout:
test("بعد از تاخیر", () => {
jest.useFakeTimers();
const callback = jest.fn();
setTimeout(callback, 1000);
expect(callback).not.toHaveBeenCalled();
jest.advanceTimersByTime(1000);
expect(callback).toHaveBeenCalled();
});
Integration Testing — تست یکپارچه
بررسی تعامل اجزای واقعی:
// test integration — API و دیتابیس واقعی (تستی):
const request = require("supertest");
const app = require("../app");
describe("Users API", () => {
test("GET /api/users — لیست کاربران", async () => {
const res = await request(app).get("/api/users");
expect(res.status).toBe(200);
expect(res.body).toBeInstanceOf(Array);
expect(res.body.length).toBeGreaterThan(0);
});
test("POST /api/users — ایجاد", async () => {
const res = await request(app)
.post("/api/users")
.send({ name: "سارا", email: "sara@test.com" });
expect(res.status).toBe(201);
expect(res.body).toHaveProperty("id");
});
test("اعتبارسنجی — بدون نام", async () => {
const res = await request(app)
.post("/api/users")
.send({ email: "x@test.com" });
expect(res.status).toBe(422);
});
});
E2E Testing — انتها به انتها
شبیهسازی کاربر واقعی در مرورگر — Playwright/Selenium:
// Playwright:
const { test, expect } = require("@playwright/test");
test("فرم ورود", async ({ page }) => {
// باز کردن صفحه:
await page.goto("https://app.com/login");
// پر کردن فرم:
await page.fill('input[name="email"]', "user@test.com");
await page.fill('input[name="password"]', "pass123");
// کلیک:
await page.click('button[type="submit"]');
// انتظار نتیجه:
await expect(page).toHaveURL("/dashboard");
await expect(page.locator(".welcome")).toContainText("خوش آمدید");
});
test("جستجو کار میکند", async ({ page }) => {
await page.goto("/shop");
await page.fill("#search", "لپتاپ");
await page.press("#search", "Enter");
const results = page.locator(".product");
await expect(results).toHaveCount(5);
});
Coverage — پوشش تست
// اجرای با coverage:
// npx jest --coverage
// خروجی:
// File | % Stmts | % Branch | % Funcs | Lines
// sum.js | 100 | 100 | 100 | 100
// api.js | 85 | 75 | 90 | 85
// پیکربندی حداقل:
// jest.config.js:
module.exports = {
coverageThreshold: {
global: {
statements: 80,
branches: 70,
functions: 80,
lines: 80
}
}
};
۱۰۰٪ coverage ≠ بدون باگ — کیفیت تستها مهمتر از عدد است!
TDD — توسعه تستمحور
اول تست، بعد کد:
// چرخه Red → Green → Refactor:
// ۱. RED — تست بنویس (fail):
test("FizzBuzz برای 3", () => {
expect(fizzBuzz(3)).toBe("Fizz");
});
// ✗ ReferenceError: fizzBuzz is not defined
// ۲. GREEN — حداقل کد برای پاس:
function fizzBuzz(n) {
return "Fizz"; // سادهترین!
}
// ✓ پاس!
// ۳. REFACTOR — بهبود با تست سبز:
function fizzBuzz(n) {
if (n % 3 === 0) return "Fizz";
if (n % 5 === 0) return "Buzz";
return String(n);
}
// تستهای بیشتر — تکرار چرخه:
test("5 → Buzz", () => expect(fizzBuzz(5)).toBe("Buzz"));
test("15 → FizzBuzz", () => expect(fizzBuzz(15)).toBe("FizzBuzz"));
test("1 → 1", () => expect(fizzBuzz(1)).toBe("1"));
Linting — تحلیل استاتیک
// ESLint — پیدا کردن باگها قبل از اجرا:
// npm install eslint
// .eslintrc.json:
{
"extends": ["eslint:recommended"],
"rules": {
"no-unused-vars": "error", // متغیر بلااستفاده
"no-undef": "error", // تعریف نشده
"eqeqeq": ["error", "always"], // === اجباری
"no-var": "error", // let/const
"prefer-const": "error"
}
}
// اجرا:
// npx eslint .
// مثال خطاها:
var x = 1; // no-var
if (x == 2) {} // eqeqeq — === بنویس!
let unused = 5; // no-unused-vars
Formatting — قالببندی خودکار
// Prettier — فرمت یکدست:
// npm install prettier
// .prettierrc:
{
"semi": true,
"singleQuote": true,
"tabWidth": 4,
"printWidth": 80,
"trailingComma": "es5"
}
// قبل:
const x={a:1,b:2,c:3}
function foo( a,b ){return a+b}
// بعد — خودکار:
const x = { a: 1, b: 2, c: 3 };
function foo(a, b) {
return a + b;
}
// اجرا:
// npx prettier --write .
کیفیت کد — اصول
| اصل | توضیح |
|---|---|
| SRP | هر تابع یک کار — کوچک نگه دار |
| نامگذاری | خوانا و گویا — getUserByEmail |
| DRY | تکرار نکن — استخراج مشترک |
| KISS | ساده — پیچیدهسازی بیمورد نه |
| تشخیص زودهنگام | fail fast — خطا در ورودی |
جمعبندی
| سطح | ابزار | سرعت |
|---|---|---|
| Unit | Jest / Vitest | خیلی سریع |
| Integration | Jest + Supertest | متوسط |
| E2E | Playwright / Cypress | کند |
| Lint | ESLint | فوری |
| Format | Prettier | فوری |
این مبحث بخشی از دوره آموزش کامل جاوا اسکریپت است.