build with purpose

TDD Chess

I was working on a learning project to create a chess game with Javascript and figured out a way to get stuff done in a fun way with TDD. Just wanted to throw together a post of what I’ve found.

First off my Spellbook of choice for Unit Test Wizardry is Jasmine. I think it had pretty good syntax about a year ago so it’s what I seem to gravitate toward. Pick your own if you’re not like me. I’m also using WallabyJS that I mentioned in a previous post.

I start with a pretty simple test file. We’re building a chess game so here’s my test file for a Rook:

Rook.spec.js

let Rook = require('../Rook')

describe('Rook >', () => {
  describe('Initialization >', () => {
    it('should correctly initialize a rook', () => {
      let myRook = Rook();
    })
  })
})

It’s pretty basic right now because I’m just testing that I can create a rook in the first place. That’s the simplest test I could come up with so that’s where I started.

I like having the > characters in my descriptions so my test failures look like this:

tests/Rook.spec.js Rook > Initialization > should correctly initialize a rook

I’m currently reading Programming Javascript Applications by Eric Elliott where he covers some of the basics of large scale Javascript applications. He talks about creating objects in Chapter 3 and it lead me to start expirimenting with a referenced library stampit. It gives an interface for creating reusable objects with a very small module overhead. Let’s take a really quick look at where the rest of the code is at using this library:

Rook.js

const stampit = require('stampit')
const Piece = require('./Piece')

const State = stampit({
  methods: {
    canMove({board, pos1, pos2}) {
      return true;
    }
  },
  properties: {}
})

const Rook = Piece.compose(State)

module.exports = Rook

Rook is a module that defines one method and no properties. These are defined on the Rook object and can be accessed when an instance is created. Rook also composes, or extends, itself onto a basic module, Piece (shown below). It’s helpful to compose the two because Rook can rely on the functionality that Piece handles generically while calculating moves for itself.

Piece.js

const stampit = require('stampit')

const State = stampit({
  methods: {
    canMove({board, pos1, pos2}) {}
  },
  properties: {}
})

const Defaults = stampit({
  init({owner}) {
    this.owner = owner || this.owner;
  },
  properties: {
    owner: 'WHITE',
    name: '',
    renderChar: ''
  }
})

const Piece = stampit(Defaults, State);

module.exports = Piece;

I personally like Piece defining methods, like canMove(), that extending methods will inherit even though it should always be overwritten by composition. This gives us a simple interface to program against where we know that the function should always be callable. This is also true of the defined properties that now exist on the Piece.

Test Driven Development Begins Here

Now that you’re all caught up, lets start thinking about how to start testing for things that a Rook should store. Two basic things I can think of are a name and a display character. The name because we might want to print it out for events and the display character for printing the current board state out in the console.

Updating Rook.spec.js:

it('should correctly initialize a rook', () => {
  let myRook = Rook();

  expect(myRook.name).toEqual('ROOK') // Expected '' to equal 'ROOK'
  expect(myRook.renderChar).toEqual('R') // Expected '' to equal 'R'
})

If you’re running WallabyJS, these two immediately start to fail. This is incredibly nice because it immediately gives you something that can be improved upon. It’s really helpful when refactoring because those tests are always running and you’ll know immediately rather than when you try to build or commit later.

Lets fix those tests and update Rook.js!

properties: {
  name: 'ROOK',
  renderChar: 'R'
}

A simple update to the state properties and we’re back in business!

Let’s make it move!

Making sure that an object initialized is pretty boring. Important… but boring. Let’s try to make our Rook move horizontally and vertically as long as the space it’s moving to is empty.

I think it’s important to set a very simple test case. We could make sure that every space in the path is empty, but that would complicate our first simple test. I like starting with the basics and improving upon those after.

First, more tests in Rook.spec.js. Let’s describe some movement of the Rook:

describe('Movement >', () => {
  it('should be able to move into an empty space vertically', () => {
    let myRook = Rook();
    let positions = {'A': {1: {piece: myRook, row: 1, column: 'A'}, 3: {row: 3, column: 'A'}}}
    let pos1 = {column: 'A', row: 1}
    let pos2 = {column: 'A', row: 3}

    expect(myRook.canMove({board: positions, pos1: pos1, pos2: pos2})).toEqual(true) // Already passing!
  })

  it('should be able to move into an empty space horizontally', () => {
    let myRook = Rook();
    let positions = {'A': {1: {piece: myRook, row: 1, column: 'A'}}, 'C': {1: {row: 1, column: 'C'}}}
    let pos1 = {column: 'A', row: 1}
    let pos2 = {column: 'C', row: 1}

    expect(myRook.canMove({board: positions, pos1: pos1, pos2: pos2})).toEqual(true) // Already passing!
  })

  it('should not be able to move into an empty space diagonally', () => {
    let myRook = Rook();
    let positions = {'A': {1: {piece: myRook, row: 1, column: 'A'}}, 'C': {3: {row: 3, column: 'C'}}}
    let pos1 = {column: 'A', row: 1}
    let pos2 = {column: 'C', row: 3}

    expect(myRook.canMove({board: positions, pos1: pos1, pos2: pos2})).toEqual(false) // Expected true to equal false
  })
})

Pretty awesome! We were able to write two passing unit tests before we were even able to define a failing unit test! This is because we were defaulting to true in the Rook.canMove() method previously. I like that because it allows us to keep writing definitions of how Rooks move until we were able to find a case that fails. Now we have some really great tests in place when we start trying to write this functionality.

Solving the failing test cases

Now it’s time to start trying to solve these breakages. I like the idea of checking row/column offsets. If we have a row offset and a column offset, we know we’re in trouble. I’ll redefined the Rook.js methods to reflect that:

methods: {
  canMove({board, pos1, pos2}) {
    let vDist = Math.abs(pos2.row - pos1.row)
    let hDist = Math.abs(pos2.column.charCodeAt(0) - pos1.column.charCodeAt(0))
    if (vDist > 0 && hDist > 0) {
      return false
    }
    return true;
  }
},

And everything is passing again! Great!

It’s a bit difficult to see through the madness of TDD, but I’ve found that it’s really the best way to get me to write Unit Tests which can be invaluable later. My results are better because I’ve already tested everything and I’m a lot more confident in my solutions going forwards. I think it’s really great to be able to code this way and I’m excited to see how my Chess game turns out.

Expect to see some more posts as I keep working through it!

Make my day and share this post:

Other posts to peak your interest:

  • Understanding Visual Testing
  • The Redux Saga Black Box
  • What my college degree gave me
  • Technical Leaders Enabling Stronger Teams
  • Finding Your Gateway to Learning Vue
  • Why Write Server Rendered Frontend Apps
  • comments powered by Disqus
    © 2019. All rights reserved.