• 20 hours
  • Medium

Free online content available in this course.

course.header.alt.is_video

course.header.alt.is_certifying

Got it!

Last updated on 8/22/18

Add a feature in TDD

Log in or subscribe for free to enjoy all this course has to offer!

Catching and killing bugs can be fun!

However, if the project is a living creature and is not destined for termination in the foreseeable future, it will require new features!

You’ve done this before - plain and simple - by writing more code! But now, we’ve got to take advantage of the test driven development technique!

Let’s see how we can enhance our project in a sustainable way!

Introducing the new feature

The basics are all covered in our app. Now it's time to make it more fun! Let's encourage the players to stay focused and motivated by rewarding the consecutive wins! 🏆

Each additional consecutive time a player wins, instead of adding a single point to their score, we'll add as many points as there are uninterrupted consecutive wins!

A draw will be considered an interruption of a sequence and the score rewards get reset back to 1.

Here's an example of game states:

  • Player 1 - score 1 (+ 1) : 0

  • Player 1 - score 3 (+ 2) : 0

  • Player 1 - score 6 (+ 3) : 0

  • Player 2 - score 6 : (+ 1) 1

  • Player 2 - score 6 : (+ 2) 3

  • Draw - score 6 : 3

  • Player 2 - score 6 : (+ 1) 4

  • etc...

And finally, to make it extra fun: in case of 3 consecutive draws, the scores for both players will reset to 0! 😈 A player who advanced a lot would have to make sure this doesn't happen, when a player who got behind may want to stroll a little to facilitate this event.

While working on this final touch for the app, we'll follow the Red, Green, Refactor methodology in TDD.

Let the battle begin!

Cycle 1. Progressive rewards

The first addition we can implement is to make sure we are progressively increasing the score for players who win consecutively.

Cycle 1. Red - create a test

We'll start by writing a test against our new addition: if the first player wins 3 times in a row from the start of the tournament, the expected score will be 6:0, as shown below:

    func testGivenGameWonByPlayerOneForThirdTime_WhenAdded_ThenScoreShouldBe6x0() {
        tournament.addGame(withWinner: .one)
        tournament.addGame(withWinner: .one)
        tournament.addGame(withWinner: .one)
        
        XCTAssertEqual(tournament.score(forPlayer: .one), 6)
        XCTAssertEqual(tournament.score(forPlayer: .two), 0)
    }

Once we type that in and execute the test, we can observe our new test fail - and rightly so, as this is not how the score is calculated:

Cycle 1. Green - implement the code

To implement the newly required functionality we need to add 2 variables to collect the winning history:

private var lastWinner: Player?
private var gamesWon = 0

And alter the addGame method from this...

    func addGame(withWinner winner: Player?) {
        if let winner = winner {
            scores[winner]! += 1
        }
    }

...to this:

    func addGame(withWinner winner: Player?) {
        if let winner = winner {
            
            if lastWinner == winner {
                gamesWon += 1
                scores[winner]! += gamesWon
            }
            else {
                lastWinner = winner
                gamesWon = 1
                scores[winner]! += gamesWon                
            }
        }
    }

Now run the test again and observe it going green:

We are almost through the first cycle!

Cycle 1. Refactor - clean up and improve

What is there to improve here?

The production code seems fine for the moment. And the test can be improved. We can anticipate needing to add games a number of times, repeating the same lines of code. Other than create extra work, this would increase the chances of us making a mistake miscalculating the number of lines needed for the test. So, let's create a helper function that will add a number of games for a player:

    func addGames(_ count: Int, withWinner winner: Player?) {
        for _ in 0 ..< count {
            tournament.addGame(withWinner: winner)
        }
    }

And then replace advancing the tournament with a single line:

    func testGivenGameWonByPlayerOneForThirdTime_WhenAdded_ThenScoreShouldBe6x0() {
        addGames(3, withWinner: .one)
        
        XCTAssertEqual(tournament.score(forPlayer: .one), 6)
        XCTAssertEqual(tournament.score(forPlayer: .two), 0)
    }

We are through the first cycle! 👍

Cycle 2. Draw interruption

The next circumstance we need to address is the occurrence of a draw. When it happens, we must reset the latest winner information; otherwise, if the previous winner continues winning after the draw, they will gain unearned rewards. 😱

Cycle 2. Red - create a test

We'll start this iteration by writing a test for the new condition: the first player wins 3 times in a row from the start of the tournament, then a games ends with a draw, and then the first player wins again. This time, the expected score will be 7:0, as shown below:

    func testGivenGameWonByPlayerOneFollowing3WinsPlayerOneAndDraw_WhenAdded_ThenScoreShouldBe7x0() {
        addGames(3, withWinner: .one)
        addGames(1, withWinner: nil)
        addGames(1, withWinner: .one)
        
        XCTAssertEqual(tournament.score(forPlayer: .one), 7)
        XCTAssertEqual(tournament.score(forPlayer: .two), 0)
    }

And it fails of course!

Cycle 2. Green - implement the code

To satisfy the new requirement, we need to add the code to the addGame method handling the event when the winner parameter is nil:

        if let winner = winner {
            // ...
        }
        else {
            lastWinner = nil
            gamesWon = 0
        }

Run the test and observe it pass!

Cycle 2. Refactor - clean up and improve

We are so good at producing perfectly perfect code. 😎 So, let's move on!

Cycle 3. Draw interruption

The last component of the new feature to address is accounting for 3 draws in a row, which needs to reset the tournament!

Cycle 3. Red - create a test

As usual, we'll write a test for the remaining condition, the tournament will advance with a winner 3 times, then we'll let the other player win 3 times, and finally we'll require a draw to happen 3 times. In the end, we'll expect the score to be back to square one: 0:0.

    func testGivenDrawGameFollowing3WinsPlayer13WinsPlayer2And2Draws_WhenAdded_ThenScoreShouldBe0x0() {
        addGames(3, withWinner: .one)
        addGames(3, withWinner: .two)
        addGames(3, withWinner: nil)
        
        XCTAssertEqual(tournament.score(forPlayer: .one), 0)
        XCTAssertEqual(tournament.score(forPlayer: .two), 0)
    }

The test fails as expected.

Cycle 3. Green - implement the code

To handle this situation, we need to add a new tracking variable to hold the number of consecutive draws:

private var draws = 0

...and alter the addGame method:

    if let winner = winner {
            // ...
        }
        else {
            lastWinner = nil
            gamesWon = 0
            
            draws += 1
            if draws == 3 {
                scores[.one]! = 0
                scores[.two]! = 0
                draws = 0
            }
        }

Test it now... And it passed!

Cycle 3. Refactor - clean up and improve

We are awesome at writing great code! However, there's still space for improvement! Instead of using a constant 3 in the code, let's move it out to a static constant. And, even though we have only 2 players, we might as well reset the whole dictionary of scores. Also, let's move it out to a function:

// ...
// create a static constant
private static let maxOfDraws = 3

// ...
// create reset fucntion
private func resetScores() {
    for (player, _) in scores {
        scores[player] = 0
    }
}

// ...
// alter handling the draws
draws += 1
if draws == Tournament.maxOfDraws {
    resetScores()
    draws = 0
}

Let's run the test again to make sure nothing is broken... Still passed!

Now we are truly awesome! 😇

Here's the final code of the Tournament class:

import Foundation

class Tournament {
    private static let maxOfDraws = 3
    private var scores = [Player.one: 0, Player.two: 0]
    
    private var lastWinner: Player?
    private var gamesWon = 0
    private var draws = 0
    
    func score(forPlayer player: Player) -> Int {
        return scores[player]!
    }
    
    func addGame(withWinner winner: Player?) {
        if let winner = winner {
            if lastWinner == winner {
                gamesWon += 1
                scores[winner]! += gamesWon
            }
            else {
                lastWinner = winner
                gamesWon = 1
                scores[winner]! += gamesWon
            }
        }
        else {
            lastWinner = nil
            gamesWon = 0
            
            draws += 1
            if draws == Tournament.maxOfDraws {
                resetScores()
                draws = 0
            }
        }
    }
    
    private func resetScores() {
        for (player, _) in scores {
            scores[player] = 0
        }
    }
}

Are we still covered?

We need to check that we are still covering 100% of our model. Click Cmd + u to execute all the tests and switch to the code coverage page:

Still looking good!

Now you can play this fancy variation of the Tic-Tac-Toe game! 🤗

Let's Recap!

I hope you enjoyed this demonstration and can appreciate the power of Test Driven Development! Remember to repeat the Red, Green, Refactor cycle without skipping phases. Each time, the goal is to find the smallest possible code adjustment to test to allow us to progress.

It's a big change in the way you develop. I encourage you to practice Test Driven Development as much as possible. After a while, you'll end up wondering how you lived without it! 🤩

Example of certificate of achievement
Example of certificate of achievement