Протестируйте свое приложение React и сделайте его пуленепробиваемым.

Одной из самых важных, но наиболее игнорируемых практик в гибкой разработке программного обеспечения является модульное тестирование. За свою карьеру я работал со многими инструментами и языками, но не нашел ни одного, который позволял бы так же легко тестировать ваш код, как в TypeScript/JavaScript. Как фронтенд-инженеры, мы обязаны убедиться, что как можно больше кода покрыто каким-либо модульным тестом. Помимо очевидного преимущества проверки на наличие ошибок, модульное тестирование может служить формой документации и упражнением по уменьшению размеров наших функций и компонентов и повышению их возможности повторного использования.

За последние несколько лет мы были благословлены в мире React возможностью работать с TypeScript и Redux Slices. TypeScript дает нам типы (очевидно 🙂), в то время как срезы Redux делают наш код управления состоянием менее подробным, чем традиционный Redux. Вместе они служат мощным инструментом для вашего приложения React. В этой статье будет использоваться пример списка задач, чтобы показать пользователям этих инструментов, как эффективно проводить модульное тестирование своего кода и создавать пуленепробиваемые приложения.

Начало работы

Чтобы построить наш список задач, мы собираемся использовать красно-зеленую модель рефакторинга модульного тестирования. Это означает, что для каждой функции мы выполним следующие шаги:

  1. Напишите функцию с достаточным количеством кода для компиляции (пока без логики 🙅)
  2. Напишите модульный тест, содержащий желаемый результат для этой функции.
  3. Запустите тест и посмотрите, как он провалится
  4. Напишите функцию, которая, по нашему мнению, пройдет модульный тест.
  5. Запустите функцию и наблюдайте за прохождением теста

Для начала мы создадим стандартный редуктор и модульный тест, чтобы убедиться, что все работает. Мы не пишем никакой логики, достаточно кода для компиляции приложения. 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.