Первый тест 🔗

Вначале разработаем класс, отвечающий за логику игры. Пусть состояние игры хранится в виде двумерного массива, в котором на месте какой‑либо клетки будет находиться крестик или нолик.

Начнём с метода, который возвращает начальное состояние. Начальным состоянием будет массив размером 3×3 с пустыми строками в клетках.

test/game.js
describe('Game', () => { test('Should return empty game board', () => { const game = new Game() const board = game.getState() expect(board).toEqual([ ['', '', ''], ['', '', ''], ['', '', ''] ]) }) })

Мы ожидаем, что возвращаемое значение не будет совпадать с начальным состоянием игры, и тест упадёт.

Тест действительно упадёт, но не поэтому. Класса Game ещё не существует, и создание его экземпляра вызовет ошибку.

Нам нужно дополнить код, создав этот класс. Создадим его прямо в коде теста.

Теперь тест упадёт по правильной причине: null !== начальному состоянию игры.

test/game.js
+ class Game { + getState() { + return null + } + }

Идём к зелёной зоне 🔗

Когда мы создали отказной тест, мы оказались в красной зоне. Теперь наша задача — пройти этот тест с как можно меньшими затратами.

Чтобы метод getState прошёл тест, мы можем из него вернуть начальное состояние игры. Когда тест пройден, мы оказываемся в зелёной зоне.

На первый взгляд кажется, что мы сделали что‑то бесполезное. Но мы создали первый метод API, тесты к нему, и уже можем передать его команде. Теперь с созданным методом могут работать другие разработчики. Тесты также выступают в роли документации, описывая принцип работы метода.

Перед каждым комитом мы проверяем изменения на тестах, и, если что‑то сломалось, нам будет проще понять, в какой момент.

Пока что getState всегда возвращает начальное состояние. К этому методу мы вернёмся позже.

test/game.js
class Game { getState() { - return null + return [ + ['', '', ''], + ['', '', ''], + ['', '', ''] + ] } }

Рефакторинг 🔗

Теперь у нас есть тесты, и мы можем приступить к рефакторингу кода.

Вынесем класс в отдельный файл, импортируем его в файл теста.

Также вынесем создание экземпляра класса в beforeEach, который вызывается перед каждым тестом. Так возможное изменение конструктора затронет только одно место.

Начальное состояние игры тоже может понадобиться в нескольких тестах, поэтому вынесем его в константу.

Чтобы проверить, что ничего не сломалось, запускаем тесты. Если они зелёные, можем двигаться дальше.

src/Game.js
export default class Game { getState() { return [ ['', '', ''], ['', '', ''], ['', '', ''] ] } }
test/game.js
import Game from '../src/Game' const initialGameBoard = [ ['', '', ''], ['', '', ''], ['', '', ''] ] let game beforeEach(() => { game = new Game() }) describe('Game', () => { test('Should return empty game board', () => { const board = game.getState() expect(board).toEqual(initialGameBoard) }) })