Как тестируют фронтенд Netflix

Проблемы традиционного UI-тестирования

«Традиционное фронтенд-тестирование UI проводится на уровне модульного, интеграционного и сквозного тестирования. Однако каждый из этих уровней представляет собой компромисс между контролем тестовых фикстур и настроек и контролем тестовых драйверов.

Например, при использовании react-testing-library, решения для юнит-тестирования, вы сохраняете полный контроль над отрисовкой и поведением базовых сервисов и импортов. Однако теряете возможность взаимодействовать с реальной страницей, что может привести к неприятным моментам:

  • Сложности с элементами пользовательского интерфейса, такими как компоненты <Dropdown />.
  • Невозможность протестировать настройки CORS или GraphQL-вызовы.
  • Отсутствие контроля проблем с z-индексом, влияющих на кликабельность кнопок.
  • Сложные и не интуитивные создание/отладка тестов.

И наоборот, инструменты интеграционного и сквозного тестирования, типа Cypress и Playwright, обеспечивают контроль страницы, но теряется возможность управлять bootstrapping-кодом приложения. Эти инструменты работают через удаленное управление браузером, заходят по URL и взаимодействуют со страницей. Этот подход тоже не идеален:

  • Сложность выполнения вызовов альтернативных API-эндпойнтов без кастомных правил перезаписи API сетевого уровня.
  • Невозможность делать ассерты в шпионах/моках или выполнять код внутри приложения.
  • Тестирование таких пользовательских фич как темный режим, требует нажатия на переключатель тем, или знания механизма localStorage для переопределения.
  • Невозможность тестировать компонент приложения в некоторых ситуациях, например если компонент становится видимым только после нажатия кнопки и ожидания отсчета 60-секундного таймера, тест должен будет выполнять эти действия и это займет не менее минуты.

Для решения подобных проблем были разработаны некоторые функции в Cypress и Playwright. Хотя эти инструменты пытаются устранить недостатки традиционных методов интеграционного тестирования, у них есть другие ограничения, связанные с их архитектурой. Они запускают dev-сервер с bootstrapping-кодом для загрузки нужного компонента и/или кода настройки, что ограничивает их возможности по работе с корпоративными приложениями уровня Netflix, в которых может быть OAuth или сложный конвейер сборки. Далее, очередное обновление TypeScript может привести к негодности ваших тестов, пока команды Cypress/Playwright не обновят их раннер.

Решение в виде SafeTest

SafeTest должен решить эти проблемы с помощью нового подхода к тестированию пользовательского интерфейса. Основная идея заключается в том, чтобы на этапе загрузки приложения иметь фрагмент кода, который инжектирует хуки для запуска тестов (Как работает SafeTest). Обратите внимание, что этот метод не оказывает заметного влияния на обычную работу вашего приложения, поскольку SafeTest использует ленивую загрузку для динамической загрузки тестов только при их выполнении (в примере с README тестов нет в production-пакете). После этого можем использовать Playwright для запуска обычных тестов, тем самым получая идеальный контроль браузера.

Этот подход также открывает некоторые интересные возможности:

  • Deep linking к конкретному тесту без необходимости запускать node test server.
  • Двусторонняя связь между браузером и контекстом теста (нода).
  • Доступ ко всем DX-функциям Playwright (за исключением в @playwright/test).
  • Видеозапись тестов, просмотр трассировки и функция паузы, для проверки различных селекторов/действий на странице.
  • Возможность создавать ассерты в шпионах в браузере в ноде, сопоставляя снапшот вызова в браузере.

Примеры кода

SafeTest разработан таким образом, чтобы быть
«знакомым» любому, кто проводил UI-тесты, используя лучшее из существующих решений. Вот пример того, как можно протестировать все приложение:

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 использует React Context для переопределения значений во время тестирования. Для примера того, как это работает, предположим, что у нас есть функция 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=[...] />;
}

Мы можем модифицировать компонент People, чтобы использовать Override:

 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();
  });
});

Функция render также принимает функцию, которая будет передана исходному компоненту приложения, что позволяет внедрять любые нужные элементы в любом месте приложения:

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.

Переопределения не ограничиваются только API-вызовами (поскольку мы также можем использовать page.route); можно переопределять отдельные значения на уровне приложения, например feature flags, или менять статические значения:

+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 есть ссылки на примеры приложений, а также на примеры репортов.

В корпоративных окружениях

Все крупные корпорации внедряют формы аутентификации в своих приложениях. Как правило, переход на 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.

Не только React

Хотя этот пост посвящен тому, как SafeTest работает с React, он не ограничивается только React. SafeTest также работает с Vue, Svelte, Angular и даже может работать с NextJS или Gatsby. Он также работает с Jest или Vitest, в зависимости от тест-раннера. Папка с примерами — как использовать SafeTest с этими инструментами.

По своей сути SafeTest — это комбинация тест-раннера, библиотеки для UI, и раннера браузера. Хотя в Netflix чаще всего используется Jest/React/Playwright, можно добавить адаптеры для других вариантов.

Итак 

SafeTest позволяет легко создавать тесты и предоставляет исчерпывающие отчеты, а также ссылки для просмотра видео, или ручного выполнения шагов проблемных тестов.

Источник

Гитхаб проекта


Какой была ваша первая зарплата в QA и как вы искали первую работу?

Мега обсуждение в нашем телеграм-канале о поиске первой работы. Обмен опытом и мнения.

Подписаться
Уведомить о
guest

0 комментариев
Межтекстовые Отзывы
Посмотреть все комментарии

Мы в Telegram

Наш официальный канал
Полезные материалы и тесты
Готовимся к собеседованию
Project- и Product-менеджмент

? Популярное

? Telegram-обсуждения

Наши подписчики обсуждают, как искали первую работу в QA. Некоторые ищут ее прямо сейчас.
Наши подписчики рассказывают о том, как не бояться задавать тупые вопросы и чувствовать себя уверенно в новой команде.
Обсуждаем, куда лучше податься - в менеджмент или по технической ветке?
Говорим о конфликтных ситуациях в команде и о том, как их избежать
$1100*
медианная зарплата в QA в июне 2023

*по результатам опроса QA-инженеров в нашем телеграм-канале

Собеседование

19%*
IT-специалистов переехало или приняло решение о переезде из России по состоянию на конец марта 2022

*по результатам опроса в нашем телеграм-канале

live

Обсуждают сейчас