This kata aims to help you become familiar with test-first design, also commonly called test-driven development, or TDD. TDD often results in stronger software design and significantly reduced complexity, as well as increased test coverage! For these reasons, many teams prefer writing tests first.
More about installation
- Clone this repository
npm install
-
npm install -D vitest
- Change the
test
script inpackage.json
to"vitest"
More about the test script
You'll see a line in package.json
that says
"test": "echo \"Error: no test specified\" && exit 1"
This is what will happen when you run npm test
or npm t
. Instead of echoing an error, replace it with "vitest"
so it reads
"test": "vitest"
-
Create a test that will always pass
More about the sample test
This help us check that our setup is working. Create
score.test.js
with these contents.import { test, expect } from 'vitest' test('test setup working', () => { expect(true).toBeTruthy() })
-
Run the tests
More about running tests
Run tests on the command line with
npm test
and celebrate if we're getting a passing test. Next, we will commit the working test. -
Commit our work
More about committing
git add -A git commit -m "Test setup working"
-
import a new file,
./score
, fromscore.test.js
More about importing
At the top of
score.test.js
, add a reference to the code we intend to test.import * as score from './score' // this is the line to add
Now you'll notice we are getting an error because it can't find the
score
reference (because it doesn't exist yet). -
Create
score.js
which exports an empty objectMore about
score.js
Create a new
score.js
file in the top level of your project. For now, it will contain the minimum amount of code required to get the test to pass.export {}
After saving these changes, the single test should once again run and pass.
Now, we will test our least complicated frame.
-
Create a test for returning the score of a gutterball frame. It will fail
More about the gutterball test
test('scores a gutterball frame', () => { const frame = [0, 0] const expected = 0 const actual = score.scoreFrame(frame) expect(actual).toBe(expected) })
Now our test is failing because it can't find the
scoreFrame
method. So let's add it. -
Add the
scoreFrame
method to./score.js
and export itMore about the
scoreFrame
methodTo
./score.js
, modify the exports and add thescoreFrame
methodexport { scoreFrame } function scoreFrame (frame) { }
Now our test is failing because it returned the wrong value (
undefined
) instead of what it was expecting (0
). So let's return what it wants. -
Cause
scoreFrame
to return the value expected by our testMore about the
scoreFrame
return valueThe quickest way to get our tests passing again is for
scoreFrame
to return0
.function scoreFrame (frame) { return 0 }
Sweet! Our tests are passing again. Let's commit it.
-
Commit our work
More about committing
git add -A git commit -m "Scoring gutterball frames"
Now let's add a feature that can score a normal frame (one without a spare or a strike).
-
Write a test to score a normal frame
More about the normal frame test
Our test might look like this:
test('scores a normal frame', () => { const frame = [2, 3] const expected = 5 const actual = score.scoreFrame(frame) expect(actual).toBe(expected) })
This new test is failing because we were expecting a
5
and0
was returned. Apparently ourscoreFrame
method needs to do something more thanreturn 0
. -
Update the
scoreFrame
function to pass the testMore about updating
scoreFrame
Instead of returning zero, the
scoreFrame
function should accept a frame and use its two values to return the score. Complete thescoreFrame
function to pass the test.function scoreFrame (frame) { //? }
But remember, the cycle is RED -> GREEN -> REFACTOR. Is there anything about our code that we could improve to make it more readable or DRY?
-
Commit our work
More about committing
git add -A git commit -m "Scoring normal frames"
From this point forward you will be writing the tests yourself!
-
Add a test for scoring a spare, and update
scoreFrame
to passMore about scoring spares
To do this, we're going to need the next frame as well. You'll need to pass two arguments when calling the
scoreFrame
function.test('scores a spare frame', () => { })
-
Complete any refactoring, re-run the tests, and make a commit
-
Add a test for scoring a single strike, and update
scoreFrame
to passMore about scoring single strikes
Because a strike uses the next 2 rolls, if the first is another strike (called a double), we'll need yet another frame. Let's tackle the double scenario later. For now, let's handle the single-strike scenario.test('scores a single strike frame', () => { })
-
Complete any refactoring, re-run the tests, and make a commit (again)
-
Add a test for scoring a double strike, and update
scoreFrame
to passMore about scoring double strikes
Now let's implement that other strike scenario where we have 2 strikes in a row and need a third frame. First, a new test.
test('scores a double strike frame', () => { })
-
Complete any refactoring, re-run the tests, and make a commit (yet again)
Tips
Once again, look for opportunities to refactor. Do you have a
scoreStrikes
function? MaybeisStrike
andisSpare
functions would be useful too. Run the tests and make sure they still pass. Cool, then commit our changes.
-
Add a feature to score a whole game of ten frames
More about scoring an entire game
Now that we can score many types of frames, let's add a feature to score a whole game of 10 frames. Because the 10th frame has special behaviour if there is a strike or a spare in it, we'll leave that scenario out of this test and test it separately later. But we can still add normal, spare, single strike and double strike frames.test('scores a game', () => { })
-
Complete any refactoring, re-run the tests, and make a commit
-
Add a feature to score a game with a strike or spare in the last frame
More about scoring special 10th frames
Now let's add a feature that calculates the 10th frame when it contains a strike or a spare. You guessed it, a test first.test('scores a spare in the 10th frame', () => { })
Maybe we could create an
isTenth
variable and pass it when callingscoreFrame
?
- Add a test that scores a perfect game
More about a perfect game
A perfect game should result in a score of 300.
const frames = [
[10, 0], [10, 0], [10, 0], [10, 0], [10, 0], [10, 0], [10, 0], [10, 0], [10, 0], [10, 10, 10]
]
You may find that scoring a perfect game reveals some flaws in your logic (maybe when scoring the 9th frame?). Make adjustments until the tests pass!
Name it index.js
so we can run it with npm start
.
Perhaps have a look at the prompt package.