آزمون و کیفیت کد

Unit و Integration و E2E Testing، Test Case و Assertion، Mocking و Stubbing و Spying، Coverage، TDD، Linting و Formatting و کیفیت کد.

تست چیست و چرا؟

تست کدی است که کد دیگر را بررسی می‌کند:

هرم تست

        ▲ 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 — خطا در ورودی

جمع‌بندی

سطحابزارسرعت
UnitJest / Vitestخیلی سریع
IntegrationJest + Supertestمتوسط
E2EPlaywright / Cypressکند
LintESLintفوری
FormatPrettierفوری

این مبحث بخشی از دوره آموزش کامل جاوا اسکریپت است.

ابزارهای مرتبط