In the previous article we have setup our TDD environment in JS, so we are ready to dip our toes into the pool of test driven development and have a bit of fun with developing a tennis scoring system.
Firstly we need to know how tennis scoring system works, which is described in great detail here. Please read it if you are not familiar with how it works.
To begin let’s make our lives easier and modify our package.json test script to this:
"test": "npm run transpile && jest ./dist/index.spec.js",
Which just runs the transpile command and once that’s done it automatically runs the jest command for testing our code.
Defining our first test
In TDD we always start by defining a test and we see it fail, so let’s do that:
describe("Tennis Scoring Tests", () => {
test("Server scores", () => {
startTennisMatch();
expect(getScore()).toBe("15 - Love");
});
});
We created a test called ‘Server scores’ and expect the score to be ’15 – Love’.
The test is failing, because we still don’t have our functions implemented.
How to store score
To begin we have to decide, how to store the score. In tennis there are usually 2 people playing. We are going to distinguish between them as server – the one that is serving the ball and the opponent.
To store the score for both players we use an array with the first value being the server’s score and the second value being the opponent’s score.
Defining core functionalities
We create an initialization function called startTennisGame, which sets the score for both players to 0.
export function startTennisMatch {
let score = [0, 0]
}
Then we are going to add functions for each player scoring, where we just increment their score value:
export function serverScores() {
score[0]++;
}
export function opponentScores() {
score[1]++;
}
We also need a utils function for returning the players score in text, 0 is Love, 1 is 15, 2 is 30, etc..:
export function getScoreInText(score) {
switch (score) {
case 0:
return "Love";
case 1:
return "15";
case 2:
return "30";
case 3:
return "40";
case 4:
return "Game";
}
}
And a function to return us the score of both players in text, which uses our already defined function getScoreInText:
export function getScore() {
// return text score
return `${getScoreInText(score[0])} - ${getScoreInText(score[1])}`;
}
Passing our first test
Now if we run the test with command:
npm run test
Our test is still failing, why is that? When we look at the response of the test we see that it says it Expected: “15 – Love” which is correct, but received “Love -Love”.
That’s because we forgot to add the server scoring function into the test. So we have a bug in our test, the server never scored. We just initialized our tennis game and ran the getScore function, expecting the server already scored.
To fix this let’s add the server scoring function to our test, like so:
test("Server scores", () => {
startTennisMatch();
serverScores();
expect(getScore()).toBe("15 - Love");
});
Now when we run the test command we should see our first test pass. The server scores and the score is ’15 – Love’.
Adding a bunch of more tests
After our success of passing the first test, we can create a bunch of more tests, see them fail and try to satisfy them. We’ve created these test, everything works and we pass all of them:
test("Server scores 2 times", () => {
startTennisMatch();
serverScores();
serverScores();
expect(getScore()).toBe("30 - Love");
});
test("Opponent scores 3 times", () => {
startTennisMatch();
opponentScores();
opponentScores();
opponentScores();
expect(getScore()).toBe("Love - 40");
});
test("Server scores 3 times and Opponent scores 2 times.", () => {
startTennisMatch();
serverScores();
serverScores();
serverScores();
opponentScores();
opponentScores();
expect(getScore()).toBe("40 - 30");
});
Game over test
Tennis games have an end, usually when one player scores 4 times. Let’s create a test where the server scores 4 times and let’s expect the following “Game – Server”.
test("Server scores 4 times", () => {
startTennisMatch();
serverScores();
serverScores();
serverScores();
serverScores();
expect(getScore()).toBe("Game - Server");
});
Oh-uh! Our test is failing. That’s great news! Because we didn’t code our previous solution to include the end of the game scenario.
Let’s modify our getScore function, so it encompasses the end game case.
export function getScore() {
const serverScore = getScoreInText(score[0]);
const opponentScore = getScoreInText(score[1]);
if (serverScore === "Game") {
return "Game - Server";
}
if (opponentScore === "Game") {
return "Game - Opponent";
}
return `${serverScore} - ${opponentScore}`;
}
Here we firstly store the server score and opponent score from the result of function getScoreInText. Then we check if any of them are equal to “Game”, if yes we return “Game – Server” or “Game – Opponent”. If no then we still as before return the “serverScore – opponentScore”.
Now when we run our end game test, it passes with flying colors.
If you want, you can create a similar end game scenario test for the opponent, where he scores 4 times.
Retrospective
We should look back at our code and see if we have missed some edge cases, for example what if the server scores 5 times, one more time when the game is already over? Our test fails, it returns ‘undefined – Love’. When it should return an error.
We have to change our scoring functions as well as getScore again:
export function serverScores() {
if (gameOver) {
error = "Game is already over. You can't score anymore.";
return;
}
score[0]++;
getScore();
}
export function opponentScores() {
if (gameOver) {
error = "Game is already over. You can't score anymore.";
return;
}
score[1]++;
getScore();
}
export function getScore() {
if (error) return error;
const serverScore = getScoreInText(score[0]);
const opponentScore = getScoreInText(score[1]);
if (serverScore === "Game") {
gameOver = true;
return "Game - Server";
}
if (opponentScore === "Game") {
gameOver = true;
return "Game - Opponent";
}
return `${serverScore} - ${opponentScore}`;
}
So essentially what we have to do is add a new global variable error and gameOver and in functions serverScores and opponentScores add the check for if the game is over then we assign the error with text “Game is already over. You can’t score anymore.”.
Also we run getScore function after each scoring. This way we check if the game is over already after each scoring.
And we have to check for error in getScore and if we find one then return it. Also when the serverScore or opponentScore is equal to “Game” then we set our global variable gameOver to true.
Now we also have to update our initialization function, to set the new global variables:
export function startTennisMatch() {
score = [0, 0];
gameOver = false;
error = false;
}
After that even the test with the edge case – when a server/opponent scores more than 5 times passes.
Conclusion
In this article we have looked at how to create code the test driven way. We have created a basic application for tennis scoring system, where a server or opponent can score and we’ve also implemented an end game scenario. In part 2 we will look at how to implement tennis’s deuce and advantage into our score system application.
Here’s the code on github.
Have a wonderful day Guildsmen! And remember when doing TDD, always start with a failing test. 🙃