عرّف الاختبارات بواجهة برمجة تطبيقات مشابهة لـ Jest مستوردة من وحدة bun:test المدمجة. على المدى الطويل، يهدف Bun إلى التوافق الكامل مع Jest؛ في الوقت الحالي، يتم دعم مجموعة محدودة من أدوات مطابقة expect.
الاستخدام الأساسي
لتعريف اختبار بسيط:
import { expect, test } from "bun:test";
test("2 + 2", () => {
expect(2 + 2).toBe(4);
});تجميع الاختبارات
يمكن تجميع الاختبارات في مجموعات باستخدام describe.
import { expect, test, describe } from "bun:test";
describe("arithmetic", () => {
test("2 + 2", () => {
expect(2 + 2).toBe(4);
});
test("2 * 2", () => {
expect(2 * 2).toBe(4);
});
});اختبارات غير متزامنة
يمكن أن تكون الاختبارات غير متزامنة.
import { expect, test } from "bun:test";
test("2 * 2", async () => {
const result = await Promise.resolve(2 * 2);
expect(result).toEqual(4);
});بدلاً من ذلك، استخدم استدعاء回调 done للإشارة إلى الاكتمال. إذا قمت بتضمين استدعاء回调 done كمعلمة في تعريف الاختبار، يجب عليك استدعاؤه وإلا سيتوقف الاختبار.
import { expect, test } from "bun:test";
test("2 * 2", done => {
Promise.resolve(2 * 2).then(result => {
expect(result).toEqual(4);
done();
});
});المهلات
يمكنك اختياريًا تحديد مهلة لكل اختبار بالمللي ثانية عن طريق تمرير رقم كوسيطة ثالثة لـ test.
import { test } from "bun:test";
test("wat", async () => {
const data = await slowOperation();
expect(data).toBe(42);
}, 500); // يجب أن يعمل الاختبار في <500msفي bun:test، ترمي مهلات الاختبار استثناءً غير قابل للالتقاط لإجبار الاختبار على التوقف والفشل. نقوم أيضًا بإنهاء أي عمليات فرعية تم إنشاؤها في الاختبار لتجنب ترك عمليات زومبي تختبئ في الخلفية.
المهلة الافتراضية لكل اختبار هي 5000ms (5 ثوانٍ) إذا لم يتم تجاوزها بخيار المهلة هذا أو jest.setDefaultTimeout().
إعادة المحاولة والتكرار
test.retry
استخدم خيار retry لإعادة محاولة الاختبار تلقائيًا إذا فشل. ينجح الاختبار إذا نجح ضمن عدد المحاولات المحدد. هذا مفيد للاختبارات غير المستقرة التي قد تفشل بشكل متقطع.
import { test } from "bun:test";
test(
"طلب شبكة غير مستقر",
async () => {
const response = await fetch("https://example.com/api");
expect(response.ok).toBe(true);
},
{ retry: 3 }, // إعادة المحاولة حتى 3 مرات إذا فشل الاختبار
);test.repeats
استخدم خيار repeats لتشغيل الاختبار عدة مرات بغض النظر عن حالة النجاح/الفشل. يفشل الاختبار إذا فشلت أي تكرار. هذا مفيد لاكتشاف الاختبارات غير المستقرة أو اختبار الإجهاد. لاحظ أن repeats: N يشغّل الاختبار N+1 مرة إجمالاً (تشغيل أولي واحد + N تكرار).
import { test } from "bun:test";
test(
"التأكد من استقرار الاختبار",
() => {
expect(Math.random()).toBeLessThan(1);
},
{ repeats: 20 }, // يعمل 21 مرة إجمالاً (1 أولي + 20 تكرار)
);NOTE
لا يمكنك استخدام كل من `retry` و `repeats` في نفس الاختبار.🧟 قاتل العمليات الزومبي
عندما تنتهي مهلة الاختبار ولم يتم إنهاء العمليات التي تم إنشاؤها في الاختبار عبر Bun.spawn أو Bun.spawnSync أو node:child_process، سيتم إنهاؤها تلقائيًا وسيتم تسجيل رسالة إلى console. هذا يمنع العمليات الزومبي من البقاء في الخلفية بعد الاختبارات المنتهية المهلة.
معدلات الاختبار
test.skip
تخطي الاختبارات الفردية مع test.skip. لن يتم تشغيل هذه الاختبارات.
import { expect, test } from "bun:test";
test.skip("wat", () => {
// TODO: إصلاح هذا
expect(0.1 + 0.2).toEqual(0.3);
});test.todo
علّم اختبار كـ todo مع test.todo. لن يتم تشغيل هذه الاختبارات.
import { expect, test } from "bun:test";
test.todo("fix this", () => {
myTestFunction();
});لتشغيل اختبارات todo والعثور على أي منها ناجح، استخدم bun test --todo.
bun test --todomy.test.ts:
✗ unimplemented feature
^ هذا الاختبار معلم كـ todo لكنه ينجح. أزل `.todo` أو تحقق من صحة الاختبار.
0 pass
1 fail
1 expect() callsمع هذا العلم، لن تسبب اختبارات todo الفاشلة خطأً، لكن اختبارات todo الناجحة سيتم تعليمها كفاشلة حتى تتمكن من إزالة علامة todo أو إصلاح الاختبار.
test.only
لتشغيل اختبار معين أو مجموعة اختبارات استخدم test.only() أو describe.only().
import { test, describe } from "bun:test";
test("test #1", () => {
// لا يعمل
});
test.only("test #2", () => {
// يعمل
});
describe.only("only", () => {
test("test #3", () => {
// يعمل
});
});الأمر التالي سيشغّل فقط الاختبارين #2 و #3.
bun test --onlyالأمر التالي سيشغّل فقط الاختبارات #1 و #2 و #3.
bun testtest.if
لتشغيل اختبار بشكل مشروط، استخدم test.if(). سيعمل الاختبار إذا كانت القيمة صحيحة. هذا مفيد بشكل特别 للاختبارات التي يجب أن تعمل فقط على بنى معمارية أو أنظمة تشغيل محددة.
test.if(Math.random() > 0.5)("يعمل نصف الوقت", () => {
// ...
});
const macOS = process.platform === "darwin";
test.if(macOS)("يعمل على macOS", () => {
// يعمل إذا كان macOS
});test.skipIf
لتخطي اختبار بناءً على شرط ما، استخدم test.skipIf() أو describe.skipIf().
const macOS = process.platform === "darwin";
test.skipIf(macOS)("يعمل على غير macOS", () => {
// يعمل إذا لم يكن macOS
});test.todoIf
إذا كنت تريد بدلاً من ذلك تعليم الاختبار كـ TODO، استخدم test.todoIf() أو describe.todoIf(). اختيار skipIf أو todoIf بعناية يمكن أن يظهر فرقًا بين، على سبيل المثال، نية "غير صالح لهذا الهدف" و "مخطط له لكن لم يتم تنفيذه بعد".
const macOS = process.platform === "darwin";
// TODO: لقد قمنا بتنفيذ هذا لـ Linux فقط حتى الآن.
test.todoIf(macOS)("يعمل على posix", () => {
// يعمل إذا لم يكن macOS
});test.failing
استخدم test.failing() عندما تعرف أن اختبارًا يفشل حاليًا لكنك تريد تتبعه وإخطارك عندما يبدأ في النجاح. هذا يعكس نتيجة الاختبار:
- اختبار فاشل معلم بـ
.failing()سينجح - اختبار ناجح معلم بـ
.failing()سيفشل (مع رسالة تشير إلى أنه ينجح الآن ويجب إصلاحه)
// هذا سينجح لأن الاختبار يفشل كما هو متوقع
test.failing("math is broken", () => {
expect(0.1 + 0.2).toBe(0.3); // يفشل بسبب دقة الفاصلة العائمة
});
// هذا سيفشل مع رسالة أن الاختبار ينجح الآن
test.failing("fixed bug", () => {
expect(1 + 1).toBe(2); // ينجح، لكننا توقعنا أن يفشل
});هذا مفيد لتتبع الأخطاء المعروفة التي تخطط لإصلاحها لاحقًا، أو لتنفيذ التطوير القائم على الاختبار.
اختبارات شرطية لكتل Describe
يمكن تطبيق المعدلات الشرطية .if() و .skipIf() و .todoIf() أيضًا على كتل describe، مما يؤثر على جميع الاختبارات داخل المجموعة:
const isMacOS = process.platform === "darwin";
// يعمل فقط المجموعة الكاملة على macOS
describe.if(isMacOS)("macOS-specific features", () => {
test("feature A", () => {
// يعمل فقط على macOS
});
test("feature B", () => {
// يعمل فقط على macOS
});
});
// يتخطى المجموعة الكاملة على Windows
describe.skipIf(process.platform === "win32")("Unix features", () => {
test("feature C", () => {
// يتم تخطيه على Windows
});
});
// يعلم المجموعة الكاملة كـ TODO على Linux
describe.todoIf(process.platform === "linux")("Upcoming Linux support", () => {
test("feature D", () => {
// معلم كـ TODO على Linux
});
});اختبارات معلمية
test.each و describe.each
لتشغيل نفس الاختبار مع مجموعات بيانات متعددة، استخدم test.each. هذا ينشئ اختبارًا معلمات يعمل مرة واحدة لكل حالة اختبار مقدمة.
const cases = [
[1, 2, 3],
[3, 4, 7],
];
test.each(cases)("%p + %p should be %p", (a, b, expected) => {
expect(a + b).toBe(expected);
});يمكنك أيضًا استخدام describe.each لإنشاء مجموعة معلمية تعمل مرة واحدة لكل حالة اختبار:
describe.each([
[1, 2, 3],
[3, 4, 7],
])("add(%i, %i)", (a, b, expected) => {
test(`returns ${expected}`, () => {
expect(a + b).toBe(expected);
});
test(`sum is greater than each value`, () => {
expect(a + b).toBeGreaterThan(a);
expect(a + b).toBeGreaterThan(b);
});
});تمرير الوسائط
كيفية تمرير الوسائط إلى دالة الاختبار الخاصة بك تعتمد على بنية حالات الاختبار:
- إذا كان صف الجدول مصفوفة (مثل
[1, 2, 3])، يتم تمرير كل عنصر كوسيطة فردية - إذا لم يكن الصف مصفوفة (مثل كائن)، يتم تمريره كوسيطة واحدة
// عناصر المصفوفة تمرر كوسائط فردية
test.each([
[1, 2, 3],
[4, 5, 9],
])("add(%i, %i) = %i", (a, b, expected) => {
expect(a + b).toBe(expected);
});
// عناصر الكائن تمرر كوسيطة واحدة
test.each([
{ a: 1, b: 2, expected: 3 },
{ a: 4, b: 5, expected: 9 },
])("add($a, $b) = $expected", data => {
expect(data.a + data.b).toBe(data.expected);
});محددات التنسيق
هناك عدد من الخيارات المتاحة لتنسيق عنوان الاختبار:
| المحدد | الوصف |
|---|---|
%p | pretty-format |
%s | String |
%d | Number |
%i | Integer |
%f | Floating point |
%j | JSON |
%o | Object |
%# | Index of the test case |
%% | Single percent sign (%) |
أمثلة
// محددات أساسية
test.each([
["hello", 123],
["world", 456],
])("string: %s, number: %i", (str, num) => {
// "string: hello, number: 123"
// "string: world, number: 456"
});
// %p لمخرجات pretty-format
test.each([
[{ name: "Alice" }, { a: 1, b: 2 }],
[{ name: "Bob" }, { x: 5, y: 10 }],
])("user %p with data %p", (user, data) => {
// "user { name: 'Alice' } with data { a: 1, b: 2 }"
// "user { name: 'Bob' } with data { x: 5, y: 10 }"
});
// %# للفهرس
test.each(["apple", "banana"])("fruit #%# is %s", fruit => {
// "fruit #0 is apple"
// "fruit #1 is banana"
});عد التأكيدات
يدعم Bun التحقق من أن عددًا محددًا من التأكيدات تم استدعاؤها أثناء الاختبار:
expect.hasAssertions()
استخدم expect.hasAssertions() للتحقق من أن تأكيدًا واحدًا على الأقل تم استدعاؤه أثناء الاختبار:
test("async work calls assertions", async () => {
expect.hasAssertions(); // سيفشل إذا لم يتم استدعاء تأكيدات
const data = await fetchData();
expect(data).toBeDefined();
});هذا مفيد بشكل特别 للاختبارات غير المتزامنة للتأكد من أن تأكيداتك تعمل فعليًا.
expect.assertions(count)
استخدم expect.assertions(count) للتحقق من أن عددًا محددًا من التأكيدات تم استدعاؤها أثناء الاختبار:
test("exactly two assertions", () => {
expect.assertions(2); // سيفشل إذا لم يتم استدعاء تأكيدتين بالضبط
expect(1 + 1).toBe(2);
expect("hello").toContain("ell");
});هذا يساعد على ضمان تشغيل جميع تأكيداتك، خاصة في الكود غير المتزامن المعقد مع مسارات كود متعددة.
اختبار الأنواع
يتضمن Bun expectTypeOf لاختبار أنواع TypeScript، متوافق مع Vitest.
expectTypeOf
توفر دالة expectTypeOf تأكيدات على مستوى النوع يتم التحقق منها بواسطة مدقق أنواع TypeScript. لاختبار أنماطك:
- اكتب تأكيدات نوعك باستخدام
expectTypeOf - شغّل
bunx tsc --noEmitللتحقق من صحة أنماطك
import { expectTypeOf } from "bun:test";
// تأكيدات نوع أساسية
expectTypeOf<string>().toEqualTypeOf<string>();
expectTypeOf(123).toBeNumber();
expectTypeOf("hello").toBeString();
// مطابقة نوع الكائن
expectTypeOf({ a: 1, b: "hello" }).toMatchObjectType<{ a: number }>();
// أنواع الدوال
function greet(name: string): string {
return `Hello ${name}`;
}
expectTypeOf(greet).toBeFunction();
expectTypeOf(greet).parameters.toEqualTypeOf<[string]>();
expectTypeOf(greet).returns.toEqualTypeOf<string>();
// أنواع المصفوفات
expectTypeOf([1, 2, 3]).items.toBeNumber();
// أنواع الوعود
expectTypeOf(Promise.resolve(42)).resolves.toBeNumber();للوثائق الكاملة حول أدوات مطابقة expectTypeOf، راجع مرجع API.
أدوات المطابقة
ينفذ Bun أدوات المطابقة التالية. التوافق الكامل مع Jest على خارطة الطريق؛ تتبع التقدم هنا.
أدوات المطابقة الأساسية
| الحالة | الأداة |
|---|---|
| ✅ | .not |
| ✅ | .toBe() |
| ✅ | .toEqual() |
| ✅ | .toBeNull() |
| ✅ | .toBeUndefined() |
| ✅ | .toBeNaN() |
| ✅ | .toBeDefined() |
| ✅ | .toBeFalsy() |
| ✅ | .toBeTruthy() |
| ✅ | .toStrictEqual() |
أدوات مطابقة السلاسل والمصفوفات
| الحالة | الأداة |
|---|---|
| ✅ | .toContain() |
| ✅ | .toHaveLength() |
| ✅ | .toMatch() |
| ✅ | .toContainEqual() |
| ✅ | .stringContaining() |
| ✅ | .stringMatching() |
| ✅ | .arrayContaining() |
أدوات مطابقة الكائنات
| الحالة | الأداة |
|---|---|
| ✅ | .toHaveProperty() |
| ✅ | .toMatchObject() |
| ✅ | .toContainAllKeys() |
| ✅ | .toContainValue() |
| ✅ | .toContainValues() |
| ✅ | .toContainAllValues() |
| ✅ | .toContainAnyValues() |
| ✅ | .objectContaining() |
أدوات مطابقة الأرقام
| الحالة | الأداة |
|---|---|
| ✅ | .toBeCloseTo() |
| ✅ | .closeTo() |
| ✅ | .toBeGreaterThan() |
| ✅ | .toBeGreaterThanOrEqual() |
| ✅ | .toBeLessThan() |
| ✅ | .toBeLessThanOrEqual() |
أدوات مطابقة الدوال والفئات
| الحالة | الأداة |
|---|---|
| ✅ | .toThrow() |
| ✅ | .toBeInstanceOf() |
أدوات مطابقة الوعود
| الحالة | الأداة |
|---|---|
| ✅ | .resolves() |
| ✅ | .rejects() |
أدوات مطابقة الدوال الوهمية
| الحالة | الأداة |
|---|---|
| ✅ | .toHaveBeenCalled() |
| ✅ | .toHaveBeenCalledTimes() |
| ✅ | .toHaveBeenCalledWith() |
| ✅ | .toHaveBeenLastCalledWith() |
| ✅ | .toHaveBeenNthCalledWith() |
| ✅ | .toHaveReturned() |
| ✅ | .toHaveReturnedTimes() |
| ✅ | .toHaveReturnedWith() |
| ✅ | .toHaveLastReturnedWith() |
| ✅ | .toHaveNthReturnedWith() |
أدوات مطابقة اللقطات
| الحالة | الأداة |
|---|---|
| ✅ | .toMatchSnapshot() |
| ✅ | .toMatchInlineSnapshot() |
| ✅ | .toThrowErrorMatchingSnapshot() |
| ✅ | .toThrowErrorMatchingInlineSnapshot() |
أدوات المطابقة المساعدة
| الحالة | الأداة |
|---|---|
| ✅ | .extend |
| ✅ | .anything() |
| ✅ | .any() |
| ✅ | .assertions() |
| ✅ | .hasAssertions() |
لم يتم تنفيذها بعد
| الحالة | الأداة |
|---|---|
| ❌ | .addSnapshotSerializer() |
أفضل الممارسات
استخدام أسماء اختبار وصفية
// جيد
test("should calculate total price including tax for multiple items", () => {
// test implementation
});
// تجنب
test("price calculation", () => {
// test implementation
});تجميع الاختبارات ذات الصلة
describe("User authentication", () => {
describe("with valid credentials", () => {
test("should return user data", () => {
// test implementation
});
test("should set authentication token", () => {
// test implementation
});
});
describe("with invalid credentials", () => {
test("should throw authentication error", () => {
// test implementation
});
});
});استخدام أدوات مطابقة مناسبة
// جيد: استخدام أدوات مطابقة محددة
expect(users).toHaveLength(3);
expect(user.email).toContain("@");
expect(response.status).toBeGreaterThanOrEqual(200);
// تجنب: استخدام toBe لكل شيء
expect(users.length === 3).toBe(true);
expect(user.email.includes("@")).toBe(true);
expect(response.status >= 200).toBe(true);اختبار ظروف الخطأ
test("should throw error for invalid input", () => {
expect(() => {
validateEmail("not-an-email");
}).toThrow("Invalid email format");
});
test("should handle async errors", async () => {
await expect(async () => {
await fetchUser("invalid-id");
}).rejects.toThrow("User not found");
});استخدام الإعداد والتنظيف
import { beforeEach, afterEach, test } from "bun:test";
let testUser;
beforeEach(() => {
testUser = createTestUser();
});
afterEach(() => {
cleanupTestUser(testUser);
});
test("should update user profile", () => {
// Use testUser in test
});