Рубріки: Тестирование

Тестирование фронтенда на примере React-приложения

Роман Гармидер

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

О тестировании фронтенда в своей статье рассказал Senior Software Architect берлинского стартапа Candis.io Дэниел Бартоломе. На всякий случай оставим ссылку и на Twitter автора.

Он рассматривает проблему на примере фреймворка React, чей декларативный стиль проще поддается тестированию, чем чистые JavaScript, HTML и CSS. Но многие идеи из статьи применимы и в других случаях.

Почему тестирование фронтенда сложнее, чем бэкенда?

Часто можно видеть, что full-stack-разработчики, которые добросовестно тестируют код бэкенда, не тратят времени на тесты фронтенд-кода.

Одна из причин — большие интерфейсы по сравнению с бэкендом. Задачу усложняет то, что сложно объяснить машине, что в этих интерфейсах важно, а что не очень. Некоторые элементы можно заменить, и ничего не произойдет, а при замене других все перестанет работать. Долгое время не было и инструментов для быстрого тестирования.

Написание тестового фронтенд-кода

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

В качестве примера возьмем классическое todo-приложение на React. Готовое приложение находится здесь.

Связующий код

Компонент App.tsx и хук useTodos — это связующий код. Он связывает вместе остальную часть кода, чтобы тот мог функционировать:

const TodoApp: FunctionComponent = () => {
  const { todos, addTodo, completeTodo, deleteTodo } = useTodos([]);

  return (
    <>
      <TodoList
        todos={todos}
        onCompleteTodo={completeTodo}
        onDeleteTodo={deleteTodo}
      />
      <AddTodo onAdd={addTodo} />
    </>
  );
};
export function useTodos(initialTodos: Todo[]) {
  const [todos, dispatch] = useReducer(todosReducer, initialTodos);
  return {
    todos,
    addTodo: (description: string) =>
      dispatch(createAddTodoAction(description)),
    completeTodo: (id: Todo["id"]) => dispatch(createCompleteTodoAction(id)),
    deleteTodo: (id: Todo["id"]) => dispatch(createDeleteTodoAction(id)),
  };
}

Лучше всего подходит интеграционное тестирование:

describe("TodoApp", () => {
  it("shows an added todo", async () => {
    render(<App />);

    const todoInput = screen.getByLabelText("New todo");
    const todoDescription = "My new todo";
    userEvent.type(todoInput, todoDescription);
    const addTodoButton = screen.getByText("Add todo");
    userEvent.click(addTodoButton);

    expect(await screen.findByText(todoDescription)).toBeInTheDocument();
  });
});

Такие интеграционные тесты должны быть независимы от используемой технологии, насколько это возможно. Тесты, приведенные выше, зависимы от React (если переписать приложение без использования React, то нужно будет менять, в том числе, и тесты).

Бизнес-логика

Это тесты, с которыми люди, пришедшие из бэкенд-тестирования, знакомы лучше всего. Бизнес-логика нашего todo-приложения заботится о создании, удалении и пометке задач как выполненных. То же самое может быть использовано в бэкенде.

export function todosReducer(todos: Todo[], action: TodoAction) {
  switch (action.type) {
    case TodoActionType.AddTodo:
      return [...todos, action.payload];
    case TodoActionType.CompleteTodo:
      return todos.map((todo) =>
        todo.id === action.payload.id ? { ...todo, completed: true } : todo
      );
    case TodoActionType.DeleteTodo:
      return todos.filter((todo) => todo.id !== action.payload.id);
  }
}

Тесты для такого типа кода на первый взгляд просты:

describe("todo reducer", () => {
  describe("addTodoAction", () => {
    it("adds a new todo to the list", () => {
      const description = "This is a todo";
      expect(todosReducer([], createAddTodoAction(description))).toContainEqual(
        expect.objectContaining({ description })
      );
    });

    it("does not remove an existing todo", () => {
      const existingTodo = new TodoMock();
      expect(
        todosReducer([existingTodo], createAddTodoAction("This is a todo"))
      ).toContainEqual(existingTodo);
    });
  });
});

Сложнее всего в тестировании бизнес-логики не создать тесты, а отделить бизнес-логику от остального кода. Давайте взглянем на useTodos.ts, который выступает связующим кодом и переносит этот редюсер в React:

export function useTodos(initialTodos: Todo[]) {
  const [todos, dispatch] = useReducer(todosReducer, initialTodos);
  return {
    todos,
    addTodo: (description: string) =>
      dispatch(createAddTodoAction(description)),
    completeTodo: (id: Todo["id"]) => dispatch(createCompleteTodoAction(id)),
    deleteTodo: (id: Todo["id"]) => dispatch(createDeleteTodoAction(id)),
  };
}

Неправильно писать бизнес-логику так, чтобы ее можно было протестировать, только тестируя весь хук. Вместо этого можно использовать хук, только чтобы связать редюсер и функции действий с логикой React.

Визуальные компоненты

Наконец, взглянем на код визуальных компонентов. Они определяют интерфейс для пользователя, но не несут в себе какой-либо бизнес-логики.  Здесь проявляются многие проблемы, которые были обозначены в начале статьи. Автор приводит концепцию, которая близка к их решению:

Story – это эквивалент юнит-теста для визуальных компонентов. Основной недостаток заключается в том, что выявлять успех или провал тестирования приходится вручную.

Применение story для кнопки:

const Template: Story<Props> = (args) => <Button {...args} />;

const actionArgs = {
  onClick: action("onClick"),
};

export const Default = Template.bind({});

Default.args = {
  ...actionArgs,
  children: "Click me!",
  color: ButtonColor.Success,
};

А здесь сама кнопка:

export enum ButtonColor {
  Alert = "Alert",
  Success = "Success",
}

export enum ButtonType {
  Submit = "submit",
  Reset = "reset",
  Button = "button",
}

export interface Props {
  children: ReactNode;
  color: ButtonColor;
  onClick?: () => void;
  type?: ButtonType;
}

export const Button: FunctionComponent<Props> = ({
  children,
  color,
  onClick,
  type,
}) => {
  const colorStyles = {
    [ButtonColor.Alert]: {
      border: "#b33 solid 1px",
      borderRadius: "4px",
      boxShadow: "2px 2px 2px rgba(100,0,0,0.8)",
      color: "white",
      backgroundColor: "#a00",
    },
    [ButtonColor.Success]: {
      border: "#3b3 solid 1px",
      borderRadius: "4px",
      boxShadow: "2px 2px 2px rgba(0,100,0,0.8)",
      color: "white",
      backgroundColor: "#0a0",
    },
  };
  return (
    <button
      style={{
        ...colorStyles[color],
        padding: "0.2rem 0.5rem",
      }}
      onClick={onClick}
      type={type}
    >
      {children}
    </button>
  );
};

Story отображает кнопку отдельно. Сначала пишется story-тест, который позволяет подумать о предполагаемом интерфейсе для этого компонента, а потом реализовать сам компонент. В случае изменения каких-то деталей реализации, story не нужно будет менять, пока интерфейс будет оставаться прежним. Это дает возможность посмотреть на отображенный story-тест и убедиться, что дизайн по-прежнему выглядит так, как задуман. Когда появится версия, которая будет соответствовать требованиям, можно настроить автоматическое регрессионное тестирование с помощью инструмента визуальной регрессии.

Перевод статьи: Юлия Шепталина

Останні статті

Обучение Power BI – какие онлайн курсы аналитики выбрать

Сегодня мы поговорим о том, как выбрать лучшие курсы Power BI в Украине, особенно для…

13.01.2024

Work.ua назвал самые конкурентные вакансии в IТ за 2023 год

В 2023 году во всех крупнейших регионах конкуренция за вакансию выросла на 5–12%. Не исключением…

08.12.2023

Украинская IT-рекрутерка создала бесплатный трекер поиска работы

Unicorn Hunter/Talent Manager Лина Калиш создала бесплатный трекер поиска работы в Notion, систематизирующий все этапы…

07.12.2023

Mate academy отправит работников в 10-дневный оплачиваемый отпуск

Edtech-стартап Mate academy принял решение отправить своих работников в десятидневный отпуск – с 25 декабря…

07.12.2023

Переписки, фото, история браузера: киевский программист зарабатывал на шпионаже

Служба безопасности Украины задержала в Киеве 46-летнего программиста, который за деньги устанавливал шпионские программы и…

07.12.2023

Как вырасти до сеньйора? Девелопер создал популярную подборку на Github

IT-специалист Джордан Катлер создал и выложил на Github подборку разнообразных ресурсов, которые помогут достичь уровня…

07.12.2023