اخبار منصات الأفلام

تقديم SafeTest: نهج جديد لاختبار الواجهة الأمامية | بواسطة مدونة Netflix للتكنولوجيا | فبراير 2024


مدونة نيتفليكس التقنية

بواسطة موشيه كولودني

في هذا المنشور، يسعدنا تقديم SafeTest، وهي مكتبة ثورية تقدم منظورًا جديدًا للاختبارات الشاملة (E2E) لتطبيقات واجهة المستخدم (UI) المستندة إلى الويب.

تقليديًا، تم إجراء اختبارات واجهة المستخدم إما من خلال اختبار الوحدة أو اختبار التكامل (يُشار إليه أيضًا باسم الاختبار الشامل (E2E)). ومع ذلك، فإن كل طريقة من هذه الطرق تقدم مقايضة فريدة من نوعها: عليك الاختيار بين التحكم في أداة الاختبار والإعداد، أو التحكم في برنامج تشغيل الاختبار.

على سبيل المثال، عند استخدام مكتبة رد الفعل، وهو حل اختبار الوحدة، فإنك تحافظ على التحكم الكامل في ما سيتم عرضه وكيف يجب أن تتصرف الخدمات الأساسية وعمليات الاستيراد. ومع ذلك، فإنك تفقد القدرة على التفاعل مع الصفحة الفعلية، مما قد يؤدي إلى عدد لا يحصى من نقاط الضعف:

  • صعوبة التفاعل مع عناصر واجهة المستخدم المعقدة مثل مكونات .
  • عدم القدرة على اختبار إعداد CORS أو مكالمات GraphQL.
  • عدم القدرة على رؤية مشكلات مؤشر z التي تؤثر على إمكانية النقر على الأزرار.
  • التأليف والتصحيح المعقد وغير البديهي للاختبارات.

على العكس من ذلك، فإن استخدام أدوات اختبار التكامل مثل Cypress أو Playwright يوفر التحكم في الصفحة، ولكنه يضحي بالقدرة على استخدام كود التشغيل للتطبيق. تعمل هذه الأدوات عن طريق التحكم عن بعد في المتصفح لزيارة عنوان URL والتفاعل مع الصفحة. هذا النهج له مجموعة من التحديات الخاصة به:

  • صعوبة إجراء مكالمات إلى نقطة نهاية API بديلة دون تطبيق قواعد إعادة كتابة API لطبقة الشبكة المخصصة.
  • عدم القدرة على تقديم تأكيدات على الجواسيس/المحاكاة أو تنفيذ التعليمات البرمجية داخل التطبيق.
  • يستلزم اختبار شيء مثل الوضع المظلم النقر فوق مبدل السمات أو معرفة آلية التخزين المحلي لتجاوزها.
  • عدم القدرة على اختبار أجزاء من التطبيق، على سبيل المثال، إذا كان أحد المكونات مرئيًا فقط بعد النقر فوق الزر والانتظار لمدة 60 ثانية للعد التنازلي، فسيحتاج الاختبار إلى تشغيل تلك الإجراءات وسيستغرق دقيقة واحدة على الأقل.

وإدراكًا لهذه التحديات، ظهرت حلول مثل اختبار مكونات E2E، مع عروض من Cypress وPlaywright. بينما تحاول هذه الأدوات تصحيح أوجه القصور في طرق اختبار التكامل التقليدية، إلا أنها تعاني من قيود أخرى بسبب بنيتها. يبدأون خادم تطوير باستخدام رمز التمهيد لتحميل المكون و/أو رمز الإعداد الذي تريده، مما يحد من قدرتهم على التعامل مع تطبيقات المؤسسة المعقدة التي قد تحتوي على OAuth أو خط أنابيب بناء معقد. علاوة على ذلك، قد يؤدي تحديث استخدام TypeScript إلى تعطيل اختباراتك حتى يقوم فريق Cypress/Playwright بتحديث برنامج التشغيل الخاص به.

يهدف SafeTest إلى معالجة هذه المشكلات من خلال نهج جديد لاختبار واجهة المستخدم. الفكرة الرئيسية هي أن يكون لدينا مقتطف من التعليمات البرمجية في مرحلة تمهيد التطبيق لدينا والذي يُدخل خطافات لتشغيل اختباراتنا (راجع أقسام كيفية عمل Safetest لمزيد من المعلومات حول ما يفعله ذلك). لاحظ أن كيفية عمل ذلك ليس لها أي تأثير ملموس على الاستخدام المنتظم لتطبيقك نظرًا لأن SafeTest يستفيد من التحميل البطيء لتحميل الاختبارات ديناميكيًا فقط عند تشغيل الاختبارات (في مثال README، الاختبارات ليست موجودة في حزمة الإنتاج على الإطلاق). وبمجرد الانتهاء من ذلك، يمكننا استخدام Playwright لإجراء اختبارات منتظمة، وبالتالي تحقيق التحكم المثالي في المتصفح الذي نريده لاختباراتنا.

يفتح هذا النهج أيضًا بعض الميزات المثيرة:

  • الارتباط العميق باختبار معين دون الحاجة إلى تشغيل خادم اختبار العقدة.
  • اتصال ثنائي الاتجاه بين المتصفح وسياق الاختبار (العقدة).
  • الوصول إلى جميع ميزات DX التي تأتي مع Playwright (باستثناء تلك التي تأتي مع @playwright/test).
  • تسجيل فيديو للاختبارات وعرض التتبع وإيقاف وظائف الصفحة مؤقتًا لتجربة محددات/إجراءات مختلفة للصفحة.
  • القدرة على تقديم تأكيدات على الجواسيس في المتصفح في العقدة، ومطابقة لقطة المكالمة داخل المتصفح.

تم تصميم SafeTest ليشعر بأنه مألوف لأي شخص أجرى اختبارات واجهة المستخدم من قبل، لأنه يستفيد من أفضل أجزاء الحلول الحالية. فيما يلي مثال لكيفية اختبار التطبيق بأكمله:

import { describe, it, expect } from 'safetest/jest';
import { render } from 'safetest/react';

describe('my app', () => {
it('loads the main page', async () => {
const { page } = await render();

await expect(page.getByText('Welcome to the app')).toBeVisible();
expect(await page.screenshot()).toMatchImageSnapshot();
});
});

يمكننا بسهولة اختبار مكون معين

import { describe, it, expect, browserMock } from 'safetest/jest';
import { render } from 'safetest/react';

describe('Header component', () => {
it('has a normal mode', async () => {
const { page } = await render(<Header />);

await expect(page.getByText('Admin')).not.toBeVisible();
});

it('has an admin mode', async () => {
const { page } = await render(<Header admin={true} />);

await expect(page.getByText('Admin')).toBeVisible();
});

it('calls the logout handler when signing out', async () => {
const spy = browserMock.fn();
const { page } = await render(<Header handleLogout={fn} />);

await page.getByText('logout').click();
expect(await spy).toHaveBeenCalledWith();
});
});

يستخدم SafeTest سياق التفاعل للسماح بتجاوزات القيمة أثناء الاختبارات. للحصول على مثال لكيفية عمل ذلك، لنفترض أن لدينا دالة fetchPeople مستخدمة في أحد المكونات:

import { useAsync } from 'react-use';
import { fetchPerson } from './api/person';

export const People: React.FC = () => {
const { data: people, loading, error } = useAsync(fetchPeople);

if (loading) return <Loader />;
if (error) return <ErrorPage error={error} />;
return <Table data={data} rows=[...] />;
}

يمكننا تعديل مكون الأشخاص لاستخدام التجاوز:

 import { fetchPerson } from './api/person';
+import { createOverride } from 'safetest/react';

+const FetchPerson = createOverride(fetchPerson);

export const People: React.FC = () => {
+ const fetchPeople = FetchPerson.useValue();
const { data: people, loading, error } = useAsync(fetchPeople);

if (loading) return <Loader />;
if (error) return <ErrorPage error={error} />;
return <Table data={data} rows=[...] />;
}

الآن، في اختبارنا، يمكننا تجاوز الاستجابة لهذه المكالمة:

const pending = new Promise(r => { /* Do nothing */ });
const resolved = [{name: 'Foo', age: 23], {name: 'Bar', age: 32]}];
const error = new Error('Whoops');

describe('People', () => {
it('has a loading state', async () => {
const { page } = await render(
<FetchPerson.Override with={() => () => pending}>
<People />
</FetchPerson.Override>
);

await expect(page.getByText('Loading')).toBeVisible();
});

it('has a loaded state', async () => {
const { page } = await render(
<FetchPerson.Override with={() => async () => resolved}>
<People />
</FetchPerson.Override>
);

await expect(page.getByText('User: Foo, name: 23')).toBeVisible();
});

it('has an error state', async () => {
const { page } = await render(
<FetchPerson.Override with={() => async () => { throw error }}>
<People />
</FetchPerson.Override>
);

await expect(page.getByText('Error getting users: "Whoops"')).toBeVisible();
});
});

تقبل وظيفة العرض أيضًا وظيفة سيتم تمريرها إلى مكون التطبيق الأولي، مما يسمح بإدخال أي عناصر مرغوبة في أي مكان في التطبيق:

it('has a people loaded state', async () => {
const { page } = await render(app =>
<FetchPerson.Override with={() => async () => resolved}>
{app}
</FetchPerson.Override>
);
await expect(page.getByText('User: Foo, name: 23')).toBeVisible();
});

من خلال التجاوزات، يمكننا كتابة حالات اختبار معقدة مثل ضمان طريقة خدمة تجمع بين طلبات واجهة برمجة التطبيقات (API) من /foo, /bar، و /baz، لديه آلية إعادة المحاولة الصحيحة لطلبات API الفاشلة فقط ولا يزال يعين قيمة الإرجاع بشكل صحيح. حتى إذا /bar يستغرق الأمر 3 محاولات لحل الطريقة، وسيجري إجمالي 5 استدعاءات لواجهة برمجة التطبيقات.

لا تقتصر التجاوزات على مكالمات واجهة برمجة التطبيقات (API) فقط (حيث يمكننا استخدام use page.route)، يمكننا أيضًا تجاوز قيم معينة على مستوى التطبيق مثل علامات الميزات أو تغيير بعض القيم الثابتة:

+const UseFlags = createOverride(useFlags);
export const Admin = () => {
+ const useFlags = UseFlags.useValue();
const { isAdmin } = useFlags();
if (!isAdmin) return <div>Permission error</div>;
// ...
}

+const Language = createOverride(navigator.language);
export const LanguageChanger = () => {
- const language = navigator.language;
+ const language = Language.useValue();
return <div>Current language is { language } </div>;
}

describe('Admin', () => {
it('works with admin flag', async () => {
const { page } = await render(
<UseIsAdmin.Override with={oldHook => {
const oldFlags = oldHook();
return { ...oldFlags, isAdmin: true };
}}>
<MyComponent />
</UseIsAdmin.Override>
);

await expect(page.getByText('Permission error')).not.toBeVisible();
});
});

describe('Language', () => {
it('displays', async () => {
const { page } = await render(
<Language.Override with={old => 'abc'}>
<MyComponent />
</Language.Override>
);

await expect(page.getByText('Current language is abc')).toBeVisible();
});
});

تعد التجاوزات ميزة قوية في SafeTest والأمثلة هنا تخدش السطح فقط. لمزيد من المعلومات والأمثلة، راجع قسم التجاوزات في ملف README.

يأتي SafeTest خارج الصندوق مزودًا بإمكانيات إعداد تقارير قوية، مثل الارتباط التلقائي لإعادة تشغيل الفيديو، وعارض تتبع Playwright، وحتى الارتباط العميق مباشرةً بالمكون الذي تم اختباره. يرتبط ملف README الخاص بـ SafeTest repo بجميع أمثلة التطبيقات بالإضافة إلى التقارير

صورة لتقرير SafeTest تعرض مقطع فيديو لعملية تشغيل تجريبية

تحتاج العديد من الشركات الكبيرة إلى شكل من أشكال المصادقة لاستخدام التطبيق. عادةً، يؤدي الانتقال إلى localhost:3000 إلى تحميل الصفحة بشكل دائم. أنت بحاجة إلى الانتقال إلى منفذ مختلف، مثل localhost:8000، الذي يحتوي على خادم وكيل للتحقق و/أو إدخال بيانات اعتماد المصادقة في مكالمات الخدمة الأساسية. يعد هذا القيد أحد الأسباب الرئيسية التي تجعل اختبارات مكونات Cypress/Playwright غير مناسبة للاستخدام في Netflix.

ومع ذلك، توجد عادةً خدمة يمكنها إنشاء مستخدمين اختباريين يمكننا استخدام بيانات اعتمادهم لتسجيل الدخول والتفاعل مع التطبيق. وهذا يسهل إنشاء غلاف خفيف حول SafeTest لإنشاء مستخدم الاختبار هذا وافتراضه تلقائيًا. على سبيل المثال، إليك كيفية القيام بذلك في Netflix:

import { setup } from 'safetest/setup';
import { createTestUser, addCookies } from 'netflix-test-helper';

type Setup = Parameters<typeof setup>[0] & {
extraUserOptions?: UserOptions;
};

export const setupNetflix = (options: Setup) => {
setup({
...options,
hooks: { beforeNavigate: [async page => addCookies(page)] },
});

beforeAll(async () => {
createTestUser(options.extraUserOptions)
});
};

بعد إعداد هذا، نقوم ببساطة باستيراد الحزمة المذكورة أعلاه بدلاً من المكان الذي كنا سنستخدم فيه Safetest/setup.

على الرغم من أن هذا المنشور ركز على كيفية عمل SafeTest مع React، إلا أنه لا يقتصر على React فقط. يعمل SafeTest أيضًا مع Vue وSvelte وAngular، ويمكنه حتى التشغيل على NextJS أو Gatsby. يتم تشغيله أيضًا باستخدام Jest أو Vitest استنادًا إلى عداء الاختبار الذي بدأت السقالات الخاصة بك به. يوضح مجلد الأمثلة كيفية استخدام SafeTest مع مجموعات أدوات مختلفة، ونحن نشجع المساهمات على إضافة المزيد من الحالات.

يعد SafeTest في جوهره بمثابة غراء ذكي لمشغل الاختبار ومكتبة واجهة المستخدم ومشغل المتصفح. على الرغم من أن الاستخدام الأكثر شيوعًا في Netflix يستخدم Jest/React/Playwright، إلا أنه من السهل إضافة المزيد من المحولات للخيارات الأخرى.

يعد SafeTest إطار عمل اختبارًا قويًا يتم اعتماده داخل Netflix. فهو يسمح بتأليف الاختبارات بسهولة ويقدم تقارير شاملة عن متى وكيف حدثت أي أعطال، مع استكمال الروابط لعرض مقطع فيديو التشغيل أو تشغيل خطوات الاختبار يدويًا لمعرفة ما الذي حدث. يسعدنا أن نرى كيف سيحدث ثورة في اختبار واجهة المستخدم ونتطلع إلى تعليقاتك ومساهماتك.

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *

زر الذهاب إلى الأعلى