المحاكاة ضرورية للاختبار حيث تتيح لك استبدال التبعيات بتنفيذات خاضعة للتحكم. يوفر Bun إمكانات محاكاة شاملة بما في ذلك محاكاة الدوال والجواسيس ومحاكاة الوحدات.
محاكاة الدوال الأساسية
أنشئ محاكاة باستخدام دالة mock.
import { test, expect, mock } from "bun:test";
const random = mock(() => Math.random());
test("random", () => {
const val = random();
expect(val).toBeGreaterThan(0);
expect(random).toHaveBeenCalled();
expect(random).toHaveBeenCalledTimes(1);
});توافق Jest
بدلاً من ذلك، يمكنك استخدام دالة jest.fn()، كما في Jest. تتصرف بشكل متطابق.
import { test, expect, jest } from "bun:test";
const random = jest.fn(() => Math.random());
test("random", () => {
const val = random();
expect(val).toBeGreaterThan(0);
expect(random).toHaveBeenCalled();
expect(random).toHaveBeenCalledTimes(1);
});خصائص دالة المحاكاة
نتيجة mock() هي دالة جديدة تم تزيينها ببعض الخصائص الإضافية.
import { mock } from "bun:test";
const random = mock((multiplier: number) => multiplier * Math.random());
random(2);
random(10);
random.mock.calls;
// [[ 2 ], [ 10 ]]
random.mock.results;
// [
// { type: "return", value: 0.6533907460954099 },
// { type: "return", value: 0.6452713933037312 }
// ]الخصائص والدوال المتاحة
يتم تنفيذ الخصائص والدوال التالية على دوال المحاكاة:
| الخاصية/الدالة | الوصف |
|---|---|
mockFn.getMockName() | ترجع اسم المحاكاة |
mockFn.mock.calls | مصفوفة وسيطات الاستدعاء لكل استدعاء |
mockFn.mock.results | مصفوفة قيم الإرجاع لكل استدعاء |
mockFn.mock.instances | مصفوفة سياقات this لكل استدعاء |
mockFn.mock.contexts | مصفوفة سياقات this لكل استدعاء |
mockFn.mock.lastCall | وسيطات آخر استدعاء |
mockFn.mockClear() | تمسح سجل الاستدعاءات |
mockFn.mockReset() | تمسح سجل الاستدعاءات وتزيل التنفيذ |
mockFn.mockRestore() | تستعيد التنفيذ الأصلي |
mockFn.mockImplementation(fn) | تحدد تنفيذًا جديدًا |
mockFn.mockImplementationOnce(fn) | تحدد التنفيذ للاستدعاء التالي فقط |
mockFn.mockName(name) | تحدد اسم المحاكاة |
mockFn.mockReturnThis() | تحدد قيمة الإرجاع إلى this |
mockFn.mockReturnValue(value) | تحدد قيمة إرجاع |
mockFn.mockReturnValueOnce(value) | تحدد قيمة إرجاع للاستدعاء التالي فقط |
mockFn.mockResolvedValue(value) | تحدد قيمة Promise محلولة |
mockFn.mockResolvedValueOnce(value) | تحدد Promise محلولة للاستدعاء التالي فقط |
mockFn.mockRejectedValue(value) | تحدد قيمة Promise مرفوضة |
mockFn.mockRejectedValueOnce(value) | تحدد Promise مرفوضة للاستدعاء التالي فقط |
mockFn.withImplementation(fn, callback) | تغير التنفيذ مؤقتًا |
أمثلة عملية
استخدام المحاكاة الأساسي
import { test, expect, mock } from "bun:test";
test("سلوك دالة المحاكاة", () => {
const mockFn = mock((x: number) => x * 2);
// استدعاء المحاكاة
const result1 = mockFn(5);
const result2 = mockFn(10);
// التحقق من الاستدعاءات
expect(mockFn).toHaveBeenCalledTimes(2);
expect(mockFn).toHaveBeenCalledWith(5);
expect(mockFn).toHaveBeenLastCalledWith(10);
// التحقق من النتائج
expect(result1).toBe(10);
expect(result2).toBe(20);
// فحص سجل الاستدعاءات
expect(mockFn.mock.calls).toEqual([[5], [10]]);
expect(mockFn.mock.results).toEqual([
{ type: "return", value: 10 },
{ type: "return", value: 20 },
]);
});تنفيذات المحاكاة الديناميكية
import { test, expect, mock } from "bun:test";
test("تنفيذات المحاكاة الديناميكية", () => {
const mockFn = mock();
// تحديد تنفيذات مختلفة
mockFn.mockImplementationOnce(() => "first");
mockFn.mockImplementationOnce(() => "second");
mockFn.mockImplementation(() => "default");
expect(mockFn()).toBe("first");
expect(mockFn()).toBe("second");
expect(mockFn()).toBe("default");
expect(mockFn()).toBe("default"); // يستخدم التنفيذ الافتراضي
});محاكاة غير متزامنة
import { test, expect, mock } from "bun:test";
test("دوال المحاكاة غير المتزامنة", async () => {
const asyncMock = mock();
// محاكاة القيم المحلولة
asyncMock.mockResolvedValueOnce("first result");
asyncMock.mockResolvedValue("default result");
expect(await asyncMock()).toBe("first result");
expect(await asyncMock()).toBe("default result");
// محاكاة القيم المرفوضة
const rejectMock = mock();
rejectMock.mockRejectedValue(new Error("Mock error"));
await expect(rejectMock()).rejects.toThrow("Mock error");
});الجواسيس مع spyOn()
من الممكن تتبع استدعاءات دالة دون استبدالها بمحاكاة. استخدم spyOn() لإنشاء جاسوس؛ يمكن تمرير هؤلاء الجواسيس إلى .toHaveBeenCalled() و .toHaveBeenCalledTimes().
import { test, expect, spyOn } from "bun:test";
const ringo = {
name: "Ringo",
sayHi() {
console.log(`Hello I'm ${this.name}`);
},
};
const spy = spyOn(ringo, "sayHi");
test("spyon", () => {
expect(spy).toHaveBeenCalledTimes(0);
ringo.sayHi();
expect(spy).toHaveBeenCalledTimes(1);
});استخدام الجاسوس المتقدم
import { test, expect, spyOn, afterEach } from "bun:test";
class UserService {
async getUser(id: string) {
// التنفيذ الأصلي
return { id, name: `User ${id}` };
}
async saveUser(user: any) {
// التنفيذ الأصلي
return { ...user, saved: true };
}
}
const userService = new UserService();
afterEach(() => {
// استعادة جميع الجواسيس بعد كل اختبار
jest.restoreAllMocks();
});
test("التجسس على دوال الخدمة", async () => {
// التجسس دون تغيير التنفيذ
const getUserSpy = spyOn(userService, "getUser");
const saveUserSpy = spyOn(userService, "saveUser");
// استخدام الخدمة بشكل طبيعي
const user = await userService.getUser("123");
await userService.saveUser(user);
// التحقق من الاستدعاءات
expect(getUserSpy).toHaveBeenCalledWith("123");
expect(saveUserSpy).toHaveBeenCalledWith(user);
});
test("التجسس مع تنفيذ المحاكاة", async () => {
// التجسس واستبدال التنفيذ
const getUserSpy = spyOn(userService, "getUser").mockResolvedValue({
id: "123",
name: "مستخدم محاكى",
});
const result = await userService.getUser("123");
expect(result.name).toBe("مستخدم محاكى");
expect(getUserSpy).toHaveBeenCalledWith("123");
});محاكاة الوحدات مع mock.module()
تتيح لك محاكاة الوحدات استبدال سلوك وحدة ما. استخدم mock.module(path: string, callback: () => Object) لمحاكاة وحدة.
import { test, expect, mock } from "bun:test";
mock.module("./module", () => {
return {
foo: "bar",
};
});
test("mock.module", async () => {
const esm = await import("./module");
expect(esm.foo).toBe("bar");
const cjs = require("./module");
expect(cjs.foo).toBe("bar");
});مثل بقية Bun، تدعم محاكاة الوحدات كلاً من import و require.
استبدال الوحدات المستوردة بالفعل
إذا كنت بحاجة إلى استبدال وحدة تم استيرادها بالفعل، فلا يوجد شيء خاص تحتاج إلى فعله. فقط استدعِ mock.module() وسيتم استبدال الوحدة.
import { test, expect, mock } from "bun:test";
// الوحدة التي سنحاكيها هنا:
import { foo } from "./module";
test("mock.module", async () => {
const cjs = require("./module");
expect(foo).toBe("bar");
expect(cjs.foo).toBe("bar");
// نقوم بتحديثها هنا:
mock.module("./module", () => {
return {
foo: "baz",
};
});
// ويتم تحديث الروابط المباشرة.
expect(foo).toBe("baz");
// تم تحديث الوحدة أيضًا لـ CJS.
expect(cjs.foo).toBe("baz");
});الرفع والتحميل المسبق
إذا كنت بحاجة إلى التأكد من محاكاة وحدة قبل استيرادها، يجب استخدام --preload لتحميل محاكياتك قبل تشغيل اختباراتك.
import { mock } from "bun:test";
mock.module("./module", () => {
return {
foo: "bar",
};
});bun test --preload ./my-preloadلتسهيل حياتك، يمكنك وضع التحميل المسبق في bunfig.toml:
[test]
# تحميل هذه الوحدات قبل تشغيل الاختبارات.
preload = ["./my-preload"]أفضل ممارسات محاكاة الوحدات
متى تستخدم التحميل المسبق
ماذا يحدث إذا حاكيّت وحدة تم استيرادها بالفعل؟
إذا حاكيّت وحدة تم استيرادها بالفعل، سيتم تحديث الوحدة في ذاكرة التخزين المؤقت للوحدة. هذا يعني أن أي وحدات تستورد الوحدة ستحصل على النسخة المحاكية، لكن الوحدة الأصلية لا تزال قد تم تقييمها. هذا يعني أن أي آثار جانبية من الوحدة الأصلية لا تزال قد حدثت.
إذا كنت تريد منع تقييم الوحدة الأصلية، يجب استخدام --preload لتحميل محاكياتك قبل تشغيل اختباراتك.
أمثلة عملية لمحاكاة الوحدات
import { test, expect, mock, beforeEach } from "bun:test";
// محاكاة وحدة عميل API
mock.module("./api-client", () => ({
fetchUser: mock(async (id: string) => ({ id, name: `User ${id}` })),
createUser: mock(async (user: any) => ({ ...user, id: "new-id" })),
updateUser: mock(async (id: string, user: any) => ({ ...user, id })),
}));
test("خدمة المستخدم مع API محاكى", async () => {
const { fetchUser } = await import("./api-client");
const { UserService } = await import("./user-service");
const userService = new UserService();
const user = await userService.getUser("123");
expect(fetchUser).toHaveBeenCalledWith("123");
expect(user.name).toBe("User 123");
});محاكاة التبعيات الخارجية
import { test, expect, mock } from "bun:test";
// محاكاة مكتبة قاعدة البيانات الخارجية
mock.module("pg", () => ({
Client: mock(function () {
return {
connect: mock(async () => {}),
query: mock(async (sql: string) => ({
rows: [{ id: 1, name: "مستخدم اختبار" }],
})),
end: mock(async () => {}),
};
}),
}));
test("عمليات قاعدة البيانات", async () => {
const { Database } = await import("./database");
const db = new Database();
const users = await db.getUsers();
expect(users).toHaveLength(1);
expect(users[0].name).toBe("مستخدم اختبار");
});دوال المحاكاة العامة
مسح جميع المحاكاة
إعادة تعيين حالة جميع دوال المحاكاة (الاستدعاءات، النتائج، إلخ) دون استعادة تنفيذها الأصلي:
import { expect, mock, test } from "bun:test";
const random1 = mock(() => Math.random());
const random2 = mock(() => Math.random());
test("مسح جميع المحاكاة", () => {
random1();
random2();
expect(random1).toHaveBeenCalledTimes(1);
expect(random2).toHaveBeenCalledTimes(1);
mock.clearAllMocks();
expect(random1).toHaveBeenCalledTimes(0);
expect(random2).toHaveBeenCalledTimes(0);
// ملاحظة: التنفيذات محفوظة
expect(typeof random1()).toBe("number");
expect(typeof random2()).toBe("number");
});هذا يعيد تعيين خصائص .mock.calls و .mock.instances و .mock.contexts و .mock.results لجميع المحاكاة، لكن على عكس mock.restore()، لا يستعيد التنفيذ الأصلي.
استعادة جميع المحاكاة
بدلاً من استعادة كل محاكاة يدويًا بشكل فردي مع mockFn.mockRestore()، استعد جميع المحاكاة بأمر واحد باستدعاء mock.restore(). القيام بذلك لا يعيد تعيين قيمة الوحدات المستبدلة مع mock.module().
import { expect, mock, spyOn, test } from "bun:test";
import * as fooModule from "./foo.ts";
import * as barModule from "./bar.ts";
import * as bazModule from "./baz.ts";
test("foo, bar, baz", () => {
const fooSpy = spyOn(fooModule, "foo");
const barSpy = spyOn(barModule, "bar");
const bazSpy = spyOn(bazModule, "baz");
// القيم الأصلية
expect(fooSpy).toBe("foo");
expect(barSpy).toBe("bar");
expect(bazSpy).toBe("baz");
// تنفيذات المحاكاة
fooSpy.mockImplementation(() => 42);
barSpy.mockImplementation(() => 43);
bazSpy.mockImplementation(() => 44);
expect(fooSpy()).toBe(42);
expect(barSpy()).toBe(43);
expect(bazSpy()).toBe(44);
// استعادة الكل
mock.restore();
expect(fooSpy()).toBe("foo");
expect(barSpy()).toBe("bar");
expect(bazSpy()).toBe("baz");
});استخدام mock.restore() يمكن أن يقلل من كمية الكود في اختباراتك بإضافته إلى كتل afterEach في كل ملف اختبار أو حتى في كود التحميل المسبق للاختبار.
توافق Vitest
لتحسين التوافق مع الاختبارات المكتوبة لـ Vitest، يوفر Bun الكائن العام vi كاسم مستعار لأجزاء من واجهة برمجة تطبيقات محاكاة Jest:
import { test, expect } from "bun:test";
// استخدام الاسم المستعار 'vi' المشابه لـ Vitest
test("توافق vitest", () => {
const mockFn = vi.fn(() => 42);
mockFn();
expect(mockFn).toHaveBeenCalled();
// الدوال التالية متاحة على كائن vi:
// vi.fn
// vi.spyOn
// vi.mock
// vi.restoreAllMocks
// vi.clearAllMocks
});هذا يجعل من السهل نقل الاختبارات من Vitest إلى Bun دون الحاجة إلى إعادة كتابة جميع محاكياتك.
تفاصيل التنفيذ
فهم كيفية عمل mock.module() يساعدك على استخدامها بفعالية أكبر:
التفاعل مع ذاكرة التخزين المؤقت
تتفاعل محاكاة الوحدات مع كل من ذاكرات التخزين المؤقت لوحدات ESM و CommonJS.
التقييم الكسول
يتم تقييم دالة استدعاء مصنع المحاكاة فقط عند استيراد الوحدة أو طلبها فعليًا.
حل المسار
يقوم Bun تلقائيًا بحل محدد الوحدة كما لو كنت تقوم باستيراد، ويدعم:
- المسارات النسبية (
'./module') - المسارات المطلقة (
'/path/to/module') - أسماء الحزم (
'lodash')
تأثيرات توقيت الاستيراد
- عند المحاكاة قبل أول استيراد: لا تحدث أي آثار جانبية من الوحدة الأصلية
- عند المحاكاة بعد الاستيراد: لقد حدثت بالفعل الآثار الجانبية للوحدة الأصلية
لهذا السبب، يوصى باستخدام --preload للمحاكيات التي تحتاج إلى منع الآثار الجانبية.
الروابط المباشرة
تحافظ وحدات ESM المحاكية على روابط مباشرة، لذا فإن تغيير المحاكاة سيحدث جميع الاستيرادات الموجودة.
أنماط متقدمة
دوال المصنع
import { mock } from "bun:test";
function createMockUser(overrides = {}) {
return {
id: "mock-id",
name: "مستخدم محاكى",
email: "mock@example.com",
...overrides,
};
}
const mockUserService = {
getUser: mock(async (id: string) => createMockUser({ id })),
createUser: mock(async (data: any) => createMockUser(data)),
updateUser: mock(async (id: string, data: any) => createMockUser({ id, ...data })),
};المحاكاة الشرطية
import { test, expect, mock } from "bun:test";
const shouldUseMockApi = process.env.NODE_ENV === "test";
if (shouldUseMockApi) {
mock.module("./api", () => ({
fetchData: mock(async () => ({ data: "mocked" })),
}));
}
test("استخدام API شرطي", async () => {
const { fetchData } = await import("./api");
const result = await fetchData();
if (shouldUseMockApi) {
expect(result.data).toBe("mocked");
}
});أنماط تنظيف المحاكاة
import { afterEach, beforeEach } from "bun:test";
beforeEach(() => {
// إعداد محاكيات شائعة
mock.module("./logger", () => ({
log: mock(() => {}),
error: mock(() => {}),
warn: mock(() => {}),
}));
});
afterEach(() => {
// تنظيف جميع المحاكاة
mock.restore();
mock.clearAllMocks();
});أفضل الممارسات
اجعل المحاكاة بسيطة
// جيد: محاكاة بسيطة ومركّزة
const mockUserApi = {
getUser: mock(async id => ({ id, name: "مستخدم اختبار" })),
};
// تجنب: سلوك محاكاة معقد للغاية
const complexMock = mock(input => {
if (input.type === "A") {
return processTypeA(input);
} else if (input.type === "B") {
return processTypeB(input);
}
// ... الكثير من المنطق المعقد
});استخدم محاكاة آمنة للنوع
interface UserService {
getUser(id: string): Promise<User>;
createUser(data: CreateUserData): Promise<User>;
}
const mockUserService: UserService = {
getUser: mock(async (id: string) => ({ id, name: "مستخدم اختبار" })),
createUser: mock(async data => ({ id: "new-id", ...data })),
};اختبار سلوك المحاكاة
test("الخدمة تستدعي API بشكل صحيح", async () => {
const mockApi = { fetchUser: mock(async () => ({ id: "1" })) };
const service = new UserService(mockApi);
await service.getUser("123");
// التحقق من استدعاء المحاكاة بشكل صحيح
expect(mockApi.fetchUser).toHaveBeenCalledWith("123");
expect(mockApi.fetchUser).toHaveBeenCalledTimes(1);
});ملاحظات
المحاكاة التلقائية
دليل __mocks__ والمحاكاة التلقائية غير مدعومين بعد. إذا كان هذا يمنعك من التبديل إلى Bun، يرجى تقديم مشكلة.
ESM مقابل CommonJS
تحتوي محاكاة الوحدات على تنفيذات مختلفة لوحدات ESM و CommonJS. لوحدات ES، أضاف Bun تصحيحات إلى JavaScriptCore تسمح لـ Bun باستبدال قيم التصدير في وقت التشغيل وتحديث الروابط المباشرة بشكل متكرر.