Протестируйте свое приложение React и сделайте его пуленепробиваемым.
Одной из самых важных, но наиболее игнорируемых практик в гибкой разработке программного обеспечения является модульное тестирование. За свою карьеру я работал со многими инструментами и языками, но не нашел ни одного, который позволял бы так же легко тестировать ваш код, как в TypeScript/JavaScript. Как фронтенд-инженеры, мы обязаны убедиться, что как можно больше кода покрыто каким-либо модульным тестом. Помимо очевидного преимущества проверки на наличие ошибок, модульное тестирование может служить формой документации и упражнением по уменьшению размеров наших функций и компонентов и повышению их возможности повторного использования.
За последние несколько лет мы были благословлены в мире React возможностью работать с TypeScript и Redux Slices. TypeScript дает нам типы (очевидно 🙂), в то время как срезы Redux делают наш код управления состоянием менее подробным, чем традиционный Redux. Вместе они служат мощным инструментом для вашего приложения React. В этой статье будет использоваться пример списка задач, чтобы показать пользователям этих инструментов, как эффективно проводить модульное тестирование своего кода и создавать пуленепробиваемые приложения.
Начало работы
Чтобы построить наш список задач, мы собираемся использовать красно-зеленую модель рефакторинга модульного тестирования. Это означает, что для каждой функции мы выполним следующие шаги:
- Напишите функцию с достаточным количеством кода для компиляции (пока без логики 🙅)
- Напишите модульный тест, содержащий желаемый результат для этой функции.
- Запустите тест и посмотрите, как он провалится
- Напишите функцию, которая, по нашему мнению, пройдет модульный тест.
- Запустите функцию и наблюдайте за прохождением теста
Для начала мы создадим стандартный редуктор и модульный тест, чтобы убедиться, что все работает. Мы не пишем никакой логики, достаточно кода для компиляции приложения. Redux Slice должен выглядеть так:
import { createSlice } from "@reduxjs/toolkit"; const todoSlice = createSlice({ name: "todo", reducers: {}, initialState: {}, }); export default todoSlice.reducer;
Ваш файл модульного теста должен выглядеть так:
import todoSlice from "../todo.reducer"; describe("Todo Slice", () => { describe("My First function", () => { it("should compile", () => { expect(1).toEqual(1); }); }); });
Если вы запустите yarn test
в этот момент, тесты должны скомпилироваться и пройти.
Добавление задачи
Первая функция, которую мы создадим в этой статье, — это та, которая добавляет todo в наше состояние. Поскольку мы используем красно-зеленый метод модульного тестирования, мы начинаем с написания кода, достаточного для компиляции, но недостаточного для прохождения нашего теста. Как только наш тест не пройден, мы можем работать в обратном направлении, чтобы создать нашу функцию. Давайте начнем с создания модели для наших задач. Наша модель задач будет иметь свойства isCompleted
, text
и id
. Затем мы можем использовать эту модель для создания нашего начального состояния. Это будет выглядеть примерно так:
type Todo = { id: String; text: String; isCompleted: boolean; }; export type TodoState = { todos: Todo[]; }; export const INITIAL_STATE: TodoState = { todos: [], }; const todoSlice = createSlice({ name: "todo", reducers: {}, initialState: INITIAL_STATE, });
Также напишем функцию с телом, которое ничего не делает, и добавим ее в редьюсеры:
const todoSlice = createSlice({ name: "todo", reducers: { addTodo: (state, action) => { return state; }, }, initialState: INITIAL_STATE, }); export const { addTodo } = todoSlice.actions;
Наконец-то мы можем написать наш первый провальный тест 😄! Наш тест создаст модель задачи, создаст действие задачи и попытается изменить состояние. Мы можем преобразовать тест, который у нас был раньше, в следующую форму:
import todoSlice, { addTodo, INITIAL_STATE, Todo, TodoState, } from "../todo.reducer"; describe("Todo Slice", () => { describe("addTodo", () => { it("should add a todo to the state", () => { const todo: Todo = { id: "1", isCompleted: false, text: "hello world", }; const action = addTodo(todo); const expectedResult: TodoState = { todos: [todo], }; const actualResult = todoSlice(INITIAL_STATE, action); expect(actualResult).toEqual(expectedResult); }); }); });
Если мы запустим наши тесты в этот момент, мы должны увидеть значок красного текста с ошибкой теста.
Прохладный! Итак, теперь мы настроили нашу модель задач, чтобы знать, как она выглядит, и мы настроили состояние задач, чтобы знать, как это должно выглядеть. Давайте составим функцию, которая действительно может добавлять данные в состояние. Измените функцию редуктора, чтобы иметь логику для добавления новой задачи.
reducers: { addTodo: ( state: TodoState, action: PayloadAction<Todo> ) => { const newTodo = action.payload; state.todos = [...state.todos, newTodo]; }, },
На этом этапе вы должны получить проходной тест! Мы завершили наш первый красно-зеленый рефакторинг 🎉.
Изменить задачу
Давайте сделаем еще пару функций, чтобы вы могли лучше понять опыт модульного тестирования TS/Redux Slice. На этот раз мы рассмотрим ситуацию, когда в состоянии уже есть данные, обновив существующую задачу до завершенного состояния. Как и в прошлый раз, добавляем пустую заглушку и экспортируем ее.
updateTodo: (state, action) => { return state; }, ... export const { addTodo, updateTodo } = todoSlice.actions;
Затем мы пишем наш неудачный тест, где мы показываем, как должно выглядеть состояние после того, как оно будет помечено как завершенное.
describe("updateTodo", () => { it("should update a todo in the state", () => { const todo: Todo = { id: "1", isCompleted: false, text: "hello world", }; const state: TodoState = { todos: [todo], }; const updatedTodo: Todo = { id: "1", isCompleted: true, text: "hello world", }; const action = updateTodo(updatedTodo); const expectedResult: TodoState = { todos: [updatedTodo], }; const actualResult = todoSlice(state, action); expect(actualResult).toEqual(expectedResult); }); });
Обратите внимание, как я использовал todo для создания «состояния» для имитации состояния приложения в функции редуктора todoSlice
? Этот способ «насмешки» над состоянием часто сложен для новичков. Я хотел выделить это как своего рода профессиональный совет для новичков в этой области. Однако с этим тестом мы снова в минусе.
Давайте исправим это, реализовав нашу функцию обновления по-настоящему.
updateTodo: ( state: TodoState, action: PayloadAction<Todo> ) => { const updatedTodo = action.payload; const newTodoArray = [...state.todos]; const index = newTodoArray.findIndex((todo) => { return updatedTodo.id === todo.id; }); newTodoArray[index] = updatedTodo; state.todos = newTodoArray; },
На этом второй красно-зеленый цикл завершен 🔥. Теперь мы можем добавлять и обновлять задачи в нашем приложении.
Удалить задачу
Давайте закончим, реализовав delete. Это будет очень похоже на обновление, но я хотел закончить последней из операций CRUD для полноты картины. Реализуйте заглушку deleteTodo
и экспортируйте ее следующим образом:
deleteTodo: (state, action) => { return state } ... export const { addTodo, updateTodo, deleteTodo } = todoSlice.actions;
Затем выполните тест deleteTodo
следующим образом:
describe("deleteTodo", () => { it("should delete a todo in the state", () => { const todo: Todo = { id: "1", isCompleted: false, text: "hello world", }; const state: TodoState = { todos: [todo], }; const action = deleteTodo("1"); const expectedResult: TodoState = { todos: [], }; const actualResult = todoSlice(state, action); expect(actualResult).toEqual(expectedResult); }); });
Давайте посмотрим, как это не удастся
Затем закончите, реализуя функцию:
deleteTodo: ( state: TodoState, action: PayloadAction<string> ) => { const id = action.payload; state.todos = [...state.todos].filter((todo) => id !== todo.id); },
При этом все тесты пройдены, и у нас есть работающий редюсер todo:
Заключительные мысли
Спасибо, что ознакомились с моей статьей о TDD с TypeScript и Redux Slices. Поскольку этот код очень короткий, я не буду публиковать его целиком на GitHub. Если вам нужен пример с проектом, в котором есть модульное тестирование с TS и TDD, вы можете проверить этот репозиторий и перейти к ветке final-solution
. Это небольшой проект, который извлекает покемонов из Интернета, используя их номер Pokédex. Надеюсь, вам понравилась эта статья, и вы с нетерпением ждете возможности опробовать красно-зеленую технику рефакторинга с TS и Redux в своем следующем проекте!
Больше контента на plainenglish.io. Подпишитесь на нашу бесплатную еженедельную рассылку новостей. Получите эксклюзивный доступ к возможностям написания и советам в нашем сообществе Discord.