Вначале разработаем класс, отвечающий за логику игры. Пусть состояние игры хранится в виде двумерного массива, в котором на месте какой‑либо клетки будет находиться крестик или нолик.
Начнём с метода, который возвращает начальное состояние. Начальным состоянием будет массив размером 3×3 с пустыми строками в клетках.
test/game.jsdescribe('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.jsclass Game { getState() { - return null + return [ + ['', '', ''], + ['', '', ''], + ['', '', ''] + ] } }
Теперь у нас есть тесты, и мы можем приступить к рефакторингу кода.
Вынесем класс в отдельный файл, импортируем его в файл теста.
Также вынесем создание экземпляра класса в beforeEach, который вызывается перед каждым тестом. Так возможное изменение конструктора затронет только одно место.
Начальное состояние игры тоже может понадобиться в нескольких тестах, поэтому вынесем его в константу.
Чтобы проверить, что ничего не сломалось, запускаем тесты. Если они зелёные, можем двигаться дальше.
src/Game.jsexport default class Game { getState() { return [ ['', '', ''], ['', '', ''], ['', '', ''] ] } }
test/game.jsimport 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) }) })