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