Теперь напишем метод для обработки хода игрока.
Плюс подхода 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.jsexport 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.jsimport 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.jsexport 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.jstest('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) })