Тестируем ход игрока 🔗

Теперь напишем метод для обработки хода игрока.

Плюс подхода TDD в том, что мы продумываем API метода во время написания теста. То есть мы сразу используем нашу функцию, а значит стараемся сделать метод более удобным в применении.

Обрабатывать ход игрока будет метод acceptUserMove. Он будет принимать координаты клетки, в которую игрок поставит крестик, и менять состояние игры в зависимости от выбора игрока.

Пока что игрок всегда будет ставить крестик в левую верхнюю клетку доски. Позже мы к этому вернёмся.

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

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

test/game.js
+ test('Writes user\'s symbol in top left cell', () => { + const x = 0, y = 0 + + game.acceptUserMove(x, y) + const board = game.getState() + + expect(board[x][y]).toEqual('×') + })
src/Game.js
+ acceptUserMove(x, y) { + + }

Обрабатываем ход игрока 🔗

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

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

Также изменим getState, чтобы он возвращал не начальное состояние, а текущее.

Для того, чтобы тесты проходили, в acceptUserMove нам достаточно указать изменение только левой верхней клетки.

Проверяем тесты и переходим к рефакторингу.

src/Game.js
export default class Game { + constructor() { + this._board = [ + ['', '', ''], + ['', '', ''], + ['', '', ''] + ] + } + getState() { - return [ - ['', '', ''], - ['', '', ''], - ['', '', ''] - ] + return this._board } + acceptUserMove(x, y) { + this._board[0][0] = '×' } }

Снова рефакторинг 🔗

Первым делом вынесем '×' в константы как в тестах, так и в коде класса.

Фаза рефакторинга относится не только к продакшн‑коду, который мы пишем, но и к коду тестов. С тестами придётся работать: их нужно дописывать и обновлять при изменении требований. Поэтому код тестов должен быть чистым и понятным.

Дальше обратим внимание на acceptUserMove: в нём this._board[0][0] явно относится к внутренней реализации класса. Вынесем это действие во внутренний метод _updateBoard и вызовем его внутри acceptUserMove.

Проверяем, не сломалось ли что‑то по дороге.

test/game.js
import Game from '../src/Game' const userMoveSymbol = '×' const initialGameBoard = [ ['', '', ''], ['', '', ''], ['', '', ''] ] let game beforeEach(() => { game = new Game() }) describe('Game', () => { // ... test('Writes user\'s symbol in top left cell', () => { const x = 0, y = 0 game.acceptUserMove(x, y) const board = game.getState() expect(board[x][y]).toEqual(userMoveSymbol) }) })
src/Game.js
export default class Game { constructor() { this._userMoveSymbol = '×' // ... } // ... acceptUserMove(x, y) { this._updateBoard(0, 0) } _updateBoard(x, y) { this._board[x][y] = this._userMoveSymbol } }

Обобщаем ход игрока 🔗

Сейчас метод acceptUserMove умеет обрабатывать только левую верхнюю клетку доски. Обобщим его и добавим обработку других клеток.

Пишем новый тест для хода игрока по остальным клеткам. Он падает, так как acceptUserMove поставит крестик в левую верхнюю клетку.

test/game.js
test('Writes user\'s symbol in cell with given coordinates', () => { const x = 1, y = 1 game.acceptUserMove(x, y) const board = game.getState() expect(board[x][y]).toEqual(userMoveSymbol) })

Расширяем метод 🔗

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

Если мы запустим тесты, то увидим, что все три теста пройдены. TDD делает работу с уже написанным кодом безопасной, вы можете менять его и переписывать. Если что‑то сломается, тесты об этом скажут.

src/Game.js
- this._updateBoard(0, 0) + this._updateBoard(x, y)

Один тест вымещает другой 🔗

Теперь посмотрим на код тестов и увидим, что третий тест включает в себя случай, описанный во втором. Поэтому второй можно удалить, он больше не понадобится.

Это нормальная практика: тесты эволюционируют, а какие‑то из них постепенно отмирают.

test/game.js
- test('Writes user\'s symbol in top left cell', () => { - const x = 0, y = 0 - - game.acceptUserMove(x, y) - const board = game.getState() - - expect(board[x][y]).toEqual(userMoveSymbol) - }) test('Writes user\'s symbol in cell with given coordinates', () => { const x = 1, y = 1 game.acceptUserMove(x, y) const board = game.getState() expect(board[x][y]).toEequal(userMoveSymbol) })