TDD/BDD by example: the basics

11 minute read

TDD/BDD by example

  1. The basics

You know what? Testing your software with one command is awesome. Being able to run a set of tests that can tell you whether you’ve broken some functionality while you were updating your systems is a great tool. The sooner you detect bugs, the better. Not to mention the pleasure of seeing all them pass!

Testing is a very wide field in Computer Science and there’s a whole set of jobs related to it. Have you ever heard of Unit Tests, Integration Tests, Test-Driven Development, Behavior-Driven Development, Continuous Integration, …? Those are all related to this field.

However, you can’t test everything in a simple or a cheap way. It might take more time to implement your systems so they can be tested than actually making the systems work. There are some functionalities with special testing needs like those related to net communication or real-time interaction.

Okay, yeah, there are some tools you can use in those scenarios like using mocks or fake UI interactions. The reality is that you will end up having some kind of QA department who will be responsible of finding all of the bugs you’ve introduced in the application.

Let me tell you something: automatic testing in videogames is hard and rarely widespreaded (that’s a generalization, and generalizations are evil!). Of course you could test some of the systems separately. Maybe you can test your data structures, or your weapons system, or the puzzles in the game, or even your movement system. But the amount of work it requires to be able to create a meaningful test suite is huge. And yet, you could only use it for a subset of the whole project. At the end of the day, you still rely on your QA team to help you. Give some love to your QA team from time to time.

I’m not very used to creating automatic testing myself, but I’ve used it sparingly over the years. I’ll try to explain the process I’d use to build a simple game system with tests to validate it.

TDD and BDD

Test-Driven Development (TDD) stands for the development process in which developers follow these general steps:

  • The developer understands the feature that needs to be implemented.
  • A new test is created to define that feature, creating the minimum code required to make it run.
  • All tests are ran, and this one must fail.
  • Only the minimum code that makes the test pass is added.
  • All tests must pass now, including this one.
  • The developer refactors the code to improve it (remove duplication, clean it up, …).

On the other hand, Behavior-Driven Development (BDD) is a methodology built on top of TDD. While TDD focuses on individual tests that check working functionalities and inputs/outputs, BDD focuses on the behavior of a testing unit (a collection of tests related to one logic construct).

Let’s be honest: I don’t mind what TDD or BDD is, or what you should use. I just want my code tested with a readable format. I’m not being picky with terminology in this post. Let’s just develop some cool stuff!

And by some cool stuff I mean learning by example.

Triple Triad

Back in 1999 a very remarkable videogame was released for the PlayStation: Final Fantasy VIII. I could describe the game or talk about why I like it so much, but it might take some more time than the one we have for this post!

In the videogame there was a card game called Triple Triad. It had relatively simple rules (described here) but you could spend hours playing it! The main reason to do so was to earn cards from your opponents and then mod them into items (some of those were unique to this system).

We’ll try to build the logic for this card game in Scala using TDD/BDD.

Rules

Triple Triad featured some base rules that applied throughout the game and some situational ones related to some events (i.e. the region in which you played). We’ll stick to the base ones for now.

Let’s have a look at a screenshot of a typical game:

Triple Triad example game

This would be our design diagram, instead of drawing one ourselves.

Keep the screenshot in mind as I outline the rules:

  • The Board is a 3x3 square grid.
  • All Cells in the grid start empty.
  • Each Cell has a Color.
  • A Card has four Ranks, each one assigned to one of its sides (Top, Left, Bottom, Right).
  • A Rank is a number in the range [1, 10] (the game uses A for the number 10).
  • A Card can be placed in an empty Cell.
  • When a Card is placed, its Cell’s Color changes to the Player’s that owns the placed card.
  • Each Player has a Color.
  • Each Player has a Hand of 5 Cards.
  • When a Card is placed, all horizontally and vertically neighbouring Cards are taken.
    • The Ranks of the Cards that are in contact with each other are compared.
    • If the Rank in the just-placed Card is higher, the neighbouring Card flips over.
    • When a Card flips over, the Color in its Cell changes to the one in the Player that placed the Card.
    • This continues until no cards can be flipped over.
  • The Game ends when all Cells have a Card.
  • The winner is the Player whose Color is most repeated throughout the Cells, including the number of Cards still in its Hand.

Have you noticed? By describing the game we’ve already found some nouns and some verbs that look like stuff we’ll use to create our system. That’s a great sign!

Take a look at the screenshot again. You can see there are two players: red and blue. Their cards are colored instead of cells as we mentioned (which we can’t see because cards are on top of them!). You can see the ranks of the cards on their top left side. There’s also an icon on some cards’s top right side that we’ll skip for now (spoiler: it’s the element of the card).

So, now that we mostly know how the system works, let’s start creating the logic in a TDD/BDD way!

Language and libraries

This time, we’ll be using Scala to illustrate the concepts in this post. It’s is a JVM-based language that mixes Object-Oriented Programming and Functional Programming in a very nice way. I’m not a professional Scala developer (not even close!) but I like its readability and the benefits of being immutable-by-default. If you want to follow along, head to the official Scala site to learn how to set it up on your computer.

We’ll be using ScalaTest to help us with our TDD/BDD implementation, so you should also go to its site to know how to configure it in case you’re following along. Also, you can read this interesting post on how to use ScalaTest more in depth than we’ll do.

Game logic

So, let’s go item by item through the list of rules.

An empty Board

Let’s take a look back at the feature list. It started as:

  • The Board is a 3x3 square grid.

So that’s the first thing we’ll build.

TDD checklist: create a test, make it run

Let’s start by creating this test specification:

class DefaultBoardSpec extends FlatSpec with Matchers {
  behavior of "A Board"
  
  it should "start empty" in {
    Board().isEmpty should be (true)
  }
}

Doesn’t it read like an open book? That’s the magic of ScalaTest’s FlatSpec and Matchers! Let’s go through the test explaining what’s going on.

First of all we’ve got the definition of our DefaultBoardSpec, which is a testing unit for our Board.
The behavior of "A Board" line defines a title for all of the tests; it’s just like a name for our testing unit.
Each it should "..." in line will define a test in our unit, so we can check individual features.

If we were to run this code, it wouldn’t compile. What’s a Board? If we remember the TDD checklist we mentioned before, we now have to make the test compile with the minimal needed code.

case class Board() {
  def isEmpty: Boolean = ???
}

Now it compiles. And what’s ???, you ask? It’s a method accessible from all compilation units in Scala that just throws a NotImplementedError exception when invoked. It’s very useful to stub methods like this!

TDD checklist: ensure new test fails

Let’s test it, then!

sbt test

This is the output:

[info] DefaultBoardSpec:
[info] A Board
[info] - should start empty *** FAILED ***
[info]   scala.NotImplementedError: an implementation is missing
[info]   ...
[info] Run completed in 297 milliseconds.
[info] Total number of tests run: 1
[info] Suites: completed 1, aborted 0
[info] Tests: succeeded 0, failed 1, canceled 0, ignored 0, pending 0
[info] *** 1 TEST FAILED ***

Sure enough, it fails! We’re on the right track. What’s next?

TDD checklist: make it pass, minimum code

Alright, let’s update our isEmpty method so it makes the test pass:

case class Board() {
  def isEmpty: Boolean = true
}

And now let’s ensure this code makes the test pass. Now, the output is:

[info] DefaultBoardSpec:
[info] A Board
[info] - should start empty
[info] Run completed in 284 milliseconds.
[info] Total number of tests run: 1
[info] Suites: completed 1, aborted 0
[info] Tests: succeeded 1, failed 0, canceled 0, ignored 0, pending 0
[info] All tests passed.

Awesome! It passes!

TDD checklist: refactor code

Let’s make use of a Scala’s companion object to define a Default Board, like so:

object Board {
  val Default = Board()
}

So now we can use Board.Default to always refer to the same instance with the default configuration. We can now rewrite our test as:

behavior of "A Default Board"

it should "start empty" in {
  Board.Default.isEmpty should be (true)
}

Good job! We’ve now implemented the first feature with a full TDD approach!

Board as a square grid

Okay, now that we’ve seen how we define our Board we’ll model it as a 3x3 square grid. This time I won’t be listing all the steps in the TDD checklist and may skip some of them.

Here are our tests:

it should "have 3 rows" in {
  Board.default.rows should be (3)
}

it should "have 3 columns" in {
  Board.default.columns should be (3)
}

it should "have 9 Cells" in {
  Board.default.cellCount should be (9)
}

Let’s model our Board to have a List[Cell] to model the grid. And what’s a Cell, you say? For now, it’s just:

case class Cell()

Now we can update our Board definition to be:

object Board {
  val Default = Board(3, 3)
}

case class Board(rows: Int, columns: Int) {
  private val cells: List[Cell] = List.fill(rows * columns)(Cell())
  
  lazy val cellCount: Int = cells.size
  def isEmpty: Boolean = true
}

Which makes all tests pass:

[info] DefaultBoardSpec:
[info] A Default Board
[info] - should start empty
[info] - should have 3 rows
[info] - should have 3 columns
[info] - should have 9 Cells
[info] Run completed in 340 milliseconds.
[info] Total number of tests run: 4
[info] Suites: completed 1, aborted 0
[info] Tests: succeeded 4, failed 0, canceled 0, ignored 0, pending 0
[info] All tests passed.

Empty Cells

The next item in our rules list is:

  • All Cells in the grid start empty.

Let’s start with the test:

class DefaultCellSpec extends FlatSpec with Matchers {
  behavior of "A Default Cell"
  
  it should "start empty" in {
    Cell.Default.isEmpty should be (true)
  }
}

As you can see, we’ve created a separate testing unit for our Cell. Apart from that, it’s basically analogous to the DefaultBoardSpec. Now, it must compile.

object Cell {
  val Default = Cell() 
}

case class Cell() {
  def isEmpty: Boolean = ???
}

Does it fail?

[info] DefaultCellSpec:
[info] A Default Cell
[info] - should start empty *** FAILED ***
[info]   scala.NotImplementedError: an implementation is missing
[info]   ...
[info] DefaultBoardSpec:
[info] A Default Board
[info] - should start empty
[info] - should have 3 rows
[info] - should have 3 columns
[info] - should have 9 Cells
[info] Run completed in 541 milliseconds.
[info] Total number of tests run: 5
[info] Suites: completed 3, aborted 0
[info] Tests: succeeded 4, failed 1, canceled 0, ignored 0, pending 0
[info] *** 1 TEST FAILED ***

Yes, it does. Can we fix it?

case class Cell() {
  def isEmpty: Boolean = true
}
[info] DefaultCellSpec:
[info] A Default Cell
[info] - should start empty
[info] DefaultBoardSpec:
[info] A Default Board
[info] - should start empty
[info] - should have 3 rows
[info] - should have 3 columns
[info] - should have 9 Cells
[info] Run completed in 529 milliseconds.
[info] Total number of tests run: 5
[info] Suites: completed 3, aborted 0
[info] Tests: succeeded 5, failed 0, canceled 0, ignored 0, pending 0
[info] All tests passed.

Yes, we did it! Great job! Now, for the refactor.

Have you noticed we’ve defined Board.isEmpty and Cell.isEmpty but they aren’t related yet? We’re going to do it now. Let’s refactor Board.isEmpty as:

def isEmpty: Boolean = cells.forall(_.isEmpty)

Oh, and now that we’ve got Cell.Default, let’s also refactor Board.cells as:

private val cells: List[Cell] = List.fill(rows * columns)(Cell.Default)

All tests pass with this refactor, so we’re very happy!


You’re getting the glimpse of TDD, aren’t you? :)

I know you’re thinking: I like the concept, but it looks so cumbersome to create a simple feature. That’s mostly because we’ve been illustrating the concept behind it step by step with very simple features that wouldn’t require this whole process most of the times.

Still, having these tests will prove helpful in the future even if we didn’t follow the whole TDD checklist for each one of them.

In the next post of the series we’ll continue building the logic of Triple Triad but we’ll reduce the explanation to implement each feature so we can build the rest of them!

You can find the code we’ve been building here.

Thanks for reading!

TDD/BDD by example

  1. The basics