Мало кто из разработчиков сомневается в эффективности тестирования, но на практике тестируется часто только бэкенд. Да еще и встречается убеждение, что фронтенд не имеет отношения к реальной разработке программного обеспечения, даже с учетом того, что во многих случаях полностью проработанный бэкенд просто не может существовать без фронтенда.
О тестировании фронтенда в своей статье рассказал 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-тест и убедиться, что дизайн по-прежнему выглядит так, как задуман. Когда появится версия, которая будет соответствовать требованиям, можно настроить автоматическое регрессионное тестирование с помощью инструмента визуальной регрессии.
Перевод статьи: Юлия Шепталина
На фоне роста спроса на ликвидность в бычьем рынке 2025 года, криптозаймы снова выходят на…
Прокси (proxy), или прокси-сервер — это программа-посредник, которая обеспечивает соединение между пользователем и интернет-ресурсом. Принцип…
Согласитесь, было бы неплохо соединить в одно сайт и приложение для смартфона. Если вы еще…
Повсеместное распространение смартфонов привело к огромному спросу на мобильные игры и приложения. Миллиарды пользователей гаджетов…
В перечне популярных чат-ботов с искусственным интеллектом Google Bard (Gemini) еще не пользуется такой популярностью…
Скрипт (англ. — сценарий), — это небольшая программа, как правило, для веб-интерфейса, выполняющая определенную задачу.…