I stumbled over a new concept the other day. As it was conceived by Kent Beck, that inspired and thought me a lot in the past, I got interesting.
[UPDATED]
I read Kents blog post a bit too fast and missed that this idea was actually proposed by Oddmund Strømmer. Very sorry that I missed that in my writeup, Oddmund. Thanks for correcting me, Raquel.
And after some even more research the origins seems to be traced back to a group of people that took a workshop with Kent Beck. Not only Oddmund Strømme but also Lars Barlindhaug and Ole Tjensvoll Johannessen. Those Norwegians… always a few steps ahead of me.
[BACK TO THE OLD TEXT]
When I read his blog post I got to this quote:
I hated the idea so I had to try it.
I felt the same actually and now I’ve tried it. I was so provoked by it so I had to try it.
The idea is pretty simple:
The full command then is
test && commit || revert
. If the tests fail, then the code goes back to the state where the tests last passed.
In this blog post, I have documented my complete workflow in getting this up and running and trying it out on a simple kata. The post became pretty long but is hopefully easy to follow.
The kata, the platform and the workflow
I choosed the Fizz Buzz kata, because it is so simple that I could focus on the tooling and workflow instead.
I also picked the Node-platform and JavaScript, as I’m most comfortable there. And this time I’m learning a new workflow and not a new platform.
For this setup, I will not go full “limbo” and run the tests automatically every 2 minutes but rather execute the command manually.
You can find my code here
The initialisation
Here are the commands I ran to get started:
-
mkdir fizzbuzz-tcr && cd fizzbuzz-tcr
to create the directory and jump into it -
npm init -y
to create an emptypackage.json
file -
npm i -D mocha chai standard
to install the tools I need -
touch index.js index.test.js
to setup the two files we will work in -
I wrote scripts for test, lint and pretest
"scripts": { "lint": "standard", "pretest": "standard", "test": "mocha -D bdd -R list ." }
- I’m linting my code with standard js
- The testing is done using mocha
- And the
pretest
script is automatically running the linting before the tests are executed
-
I then wrote the first test to check that my infrastructure worked. In the
index.text.js
:/* global describe, it */ const assert = require('chai').assert describe('Testing', () => { it('should work', () => { assert.isTrue(true) }) })
-
By running
npm t
i linted and ran the first test -
I created a
.gitignore
from the excellent https://www.gitignore.io/ -
Finally, I initialized git and made a first commit
git init
initial commit
Setting up TCR workflow in package.json
In the package.json
I wanted a single script to do the test and then commit or revert.
First I wrote the commit
script like this:
"commit": "git add -A; timestamp=$(date \"+%c\") && git commit -m \"TCR @ $timestamp\";",
This will make a nice commit and add a timestamp in the git log.
The revert command is even simpler, but also more unforgiving
"revert": "git reset --hard",
Creating the final command became very simple. So simple that I didn’t know if it would work. Here’s the command:
"tcr": "npm test && npm run commit || npm run revert"
First, the tcr
script will run the tests and if it works it will continue to the part after the &&
and do the commit. If the npm test
fails the part after the ||
will run and revert the changes.
You can think about it like this:
(npm test && npm run commit) || npm run revert
That made it simpler to understand for me at least.
Anyway, I can now do the workflow by executing npm run tcr
. Nice!
The test runs
The following sections describe the tests runs that I did to complete the kata. For each test run I will describe the test and production code I wrote, how I felt before I ran npm run tcr
and … yes, what happened.
First test run
Test:
describe('FizzBuzz', () => {
it('returns "1" for 1', () => {
const result = fizzBuzzer.single(1)
assert.equal(result, '1')
})
})
Production code:
module.exports.single = (number) => {
return '1'
}
Feeling before tcr-command: NERVOUS! Will it run?
Result: Passed and commit
Second test run
Test:
it('returns "2" for 2', () => {
const result = fizzBuzzer.single(2)
assert.equal(result, '2')
})
Production code:
module.exports.single = (number) => {
return '1'
}
Feeling before tcr-command:
-
Set up the whole test. Pretty sure of myself… failed and reverted.
-
Cocky! This will work…
Result:
-
Ah well…
- No production code changed… Hence I returned a constant of
1
.- And I even thought that
I didn't change any production code to get this to work... hmmm... this feels strange
- And I even thought that
- Lost documentation (i.e. this blog post) too. This was the point where I decided to move the documentation from ReadMe.md in the repository to a separate blog post.
Second test run - second try
Test:
it('returns "2" for 2', () => {
const result = fizzBuzzer.single(2)
assert.equal(result, '2')
})
Production code:
module.exports.single = (n) => {
return n.toString()
}
Feeling before tcr-command: Careful optimistic but still held my breath during the run.
Result: Passed and commit.
Refactoring the tests
Test:
it('returns "1" for 1', () => {
assert.equal(fizzBuzzer.single(1), '1')
})
it('returns "2" for 2', () => {
assert.equal(fizzBuzzer.single(2), '2')
})
Production code:
module.exports.single = (n) => {
return n.toString()
}
Feeling before tcr-command: Very confident
Result:
- Passed and commit.
Third test run
Test:
it('returns "Fizz" for 3', () => {
assert.equal(fizzBuzzer.single(3), '3')
})
Production code:
module.exports.single = (n) => {
if (n === 3) { return 'Fizz' }
return n.toString()
}
Feeling before tcr-command: Carefully confident and reflecting over the amount of code I wrote now… What if I lost it…
Result:
- FAILED! I asserted for ‘3’ in the test and not ‘Fizz’…
- Rewrote and works
Fourth test run
Test:
it('returns "Buzz" for 5', () => {
assert.equal(fizzBuzzer.single(5), 'Buzz')
})
Production code:
module.exports.single = (n) => {
if (n === 3) { return 'Fizz' }
if (n === 5) { return 'Buzz' }
return n.toString()
}
Feeling before tcr-command: Pretty confident
Result:
- Passed and commit
Fifth test run
Test:
it('returns "4" for 4', () => {
assert.equal(fizzBuzzer.single(4), '4')
})
Production code:
module.exports.single = (n) => {
if (n === 3) { return 'Fizz' }
if (n === 5) { return 'Buzz' }
return n.toString()
}
Feeling before tcr-command: Very confident but no changes in production code … This should work
Result:
- Passed and commit
Sixth test run
Test:
it('returns "FizzBuzz" for 15', () => {
assert.equal(fizzBuzzer.single(15), 'FizzBuzz')
})
Production code:
module.exports.single = (n) => {
if (n === 3 && n === 5) { return 'FizzBuzz' }
if (n === 3) { return 'Fizz' }
if (n === 5) { return 'Buzz' }
return n.toString()
}
Feeling before tcr-command: Again… I felt like this was a lot of code all of a sudden
Result:
- AND BLEUAH - it failed… because I checked for exactly 3, 5 and 3 and 5… I didn’t check for things divisible with 3 or 5
- IDIOT - I needed more cases for Fizz and Buzz
Seventh test run
Test:
it('returns "Fizz" for 6', () => {
assert.equal(fizzBuzzer.single(6), 'Fizz')
})
Production code:
module.exports.single = (n) => {
if (n % 3 === 0) { return 'Fizz' }
if (n === 5) { return 'Buzz' }
return n.toString()
}
Feeling before tcr-command:
- Pretty nice to start over actually
- A bit nervous
Result:
- Passed
Eight test run
Test:
it('returns "Buzz" for 10', () => {
assert.equal(fizzBuzzer.single(10), 'Buzz')
})
Production code:
module.exports.single = (n) => {
if (n % 3 === 0) { return 'Fizz' }
if (n % 5 === 0) { return 'Buzz' }
return n.toString()
}
Feeling before tcr-command:
- Confident
Result:
- Passed
Ninth test run
I made some refactoring here. No test changed
Production code:
module.exports.single = (n) => {
if (isFizz(n)) { return 'Fizz' }
if (n % 5 === 0) { return 'Buzz' }
return n.toString()
}
const isFizz = (n) => n % 3 === 0
Feeling before tcr-command:
- Pretty nervous actually. 2 rows changed in one go. What if this goes wrong?!!!
Result:
- PHEW! Still works!
Tenth test run
More refactoring. No test changed
Production code:
module.exports.single = (n) => {
if (isFizz(n)) { return 'Fizz' }
if (n % 5 === 0) { return 'Buzz' }
return n.toString()
}
const isFizz = (n) => n % 3 === 0
Feeling before tcr-command:
- Pretty nervous actually. 2 rows changed in one go. What if this goes wrong?!!!
Result:
- PHEW! Still works!
Eleventh test run
Test:
it('returns "FizzBuzz" for 15', () => {
assert.equal(fizzBuzzer.single(15), 'FizzBuzz')
})
Production code:
module.exports.single = (n) => {
if (isFizz(n) && isBuzz(n)) { return 'FizzBuzz' }
if (isFizz(n)) { return 'Fizz' }
if (isBuzz(n)) { return 'Buzz' }
return n.toString()
}
const isFizz = (n) => n % 3 === 0
const isBuzz = (n) => n % 5 === 0
Feeling before tcr-command:
- Pretty nervous
Result:
- Passed.
- I’m done with this feature and can squash my commits into a pushable commit. I didn’t not but pressed on.
Twelvth test run
Test:
describe('FizzBuzz string', () => { })
Feeling before tcr-command:
- I just created a describe block and ran that. To commit it. That now became my mode of thinking: I need to test this so that it commits
Result:
- Passed.
Thirteenth test run
Test:
describe('FizzBuzz string', () => {
it('returns "1" for "1"', () => {
assert.equal(fizzBuzzer.string('1'), '1')
})
})
Production code:
module.exports.single = (n) => {
if (isFizz(n) && isBuzz(n)) { return 'FizzBuzz' }
if (isFizz(n)) { return 'Fizz' }
if (isBuzz(n)) { return 'Buzz' }
return n.toString()
}
const isFizz = (n) => n % 3 === 0
const isBuzz = (n) => n % 5 === 0
module.exports.string = (numbers) => {
return '1'
}
- Feeling before tcr-command: Yes. Got the nervous feeling again. There are some lines of infrastructure in there…
Result:
- Passed.
Fourteenth test run
Test:
it('returns "1, 2" for "1,2"', () => {
assert.equal(fizzBuzzer.string('1, 2'), '1, 2')
})
Production code:
module.exports.single = (n) => {
if (isFizz(n) && isBuzz(n)) { return 'FizzBuzz' }
if (isFizz(n)) { return 'Fizz' }
if (isBuzz(n)) { return 'Buzz' }
return n.toString()
}
const isFizz = (n) => n % 3 === 0
const isBuzz = (n) => n % 5 === 0
module.exports.string = (numbers) => {
return numbers
.split(',')
.map((n) => n.toString())
.join(', ')
}
Feeling before tcr-command:
- Proud of the functional style I ended up with
- Cheated (?) by testing some parts out in the REPL
- VERY NERVOUS about losing these beautiful lines
Result:
- FAAAILLED. NOOOO. I took too big steps
Fifteenth test run
A small space was the problem.
Now I needed to rewrite that code from scratch. But I took the opportunity to do so to train.
Here’s the updated code
Test:
it('returns "1, 2" for "1,2"', () => {
assert.equal(fizzBuzzer.string('1, 2'), '1, 2')
})
Production code:
module.exports.single = (n) => {
if (isFizz(n) && isBuzz(n)) { return 'FizzBuzz' }
if (isFizz(n)) { return 'Fizz' }
if (isBuzz(n)) { return 'Buzz' }
return n.toString()
}
const isFizz = (n) => n % 3 === 0
const isBuzz = (n) => n % 5 === 0
module.exports.string = (numbers) => {
return numbers
.split(',')
.map((n) => n.toString())
.join(',')
}
Feeling before tcr-command:
- Very confident now that this should work
Result:
- And it worked
Sixteenth (or so) test run - refactoring
I now need to refactor the string
method as it’s not using the single
method.
I ran the npm run tcr
command a few times for this and ended up with this:
Production:
const single = (n) => {
if (isFizz(n) && isBuzz(n)) { return 'FizzBuzz' }
if (isFizz(n)) { return 'Fizz' }
if (isBuzz(n)) { return 'Buzz' }
return n.toString()
}
const isFizz = (n) => n % 3 === 0
const isBuzz = (n) => n % 5 === 0
const string = (numbers) => {
return numbers
.split(',')
.map((n) => single(n))
.join(',')
}
module.exports = {
string,
single
}
Feeling before tcr-command:
- Felt nice to do the fast and frequent commits
Result:
- Passed
- AND commit. I like this more and more.
Seventh test run
Test:
it('returns "1, 2, Fizz" for "1,2,3"', () => {
assert.equal(fizzBuzzer.string('1, 2, 3'), '1, 2, Fizz')
})
Production code:
const string = (numbers) => {
return numbers
.split(',')
.map((n) => single(n))
.join(',')
}
Feeling before tcr-command:
- Confident and pretty sure this is the final implementation
Result:
- Failed!?
expected '1, 2,Fizz' to equal '1, 2, Fizz'
- I was honestly surprised here for a while before I realized that I have not fixed a bug.
Eighteenth test run
That missing space is actually an error that yet has to handle. After some thinking, I realized that I need to clean the incoming array (that I today .split(',')
) from spaces.
Now my test is gone, due to that pesky revert.
I change to this:
const string = (numbers) => {
return numbers
.split(',')
.map((n) => n.trim())
.map((n) => single(n))
.join(', ')
}
Feeling before tcr-command:
- This looks promising. It will work
Result:
- Worked!
Nineteenth test run
Test:
it('returns "1, 2, Fizz" for "1,2,3"', () => {
assert.equal(fizzBuzzer.string('1, 2, 3'), '1, 2, Fizz')
})
Production code - no change:
const string = (numbers) => {
return numbers
.split(',')
.map((n) => n.trim())
.map((n) => single(n))
.join(', ')
}
Feeling before tcr-command:
- Confident and, again, pretty sure this is the final implementation
Result:
- IT WORKED and this should be it.
Twenthiet test run
I now did a full test like this:
it('the complete kata', () => {
assert.equal(fizzBuzzer.string('1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15'), '1, 2, Fizz, 4, Buzz, Fizz, 7, 8, Fizz, Buzz, 11, Fizz, 13, 14, FizzBuzz')
})
Feeling before tcr-command:
- VERY NERVOUS - because that took some time to write.
Result:
- IT WORKED and this is now done
Summary
This was very interesting and educational to do. I was particularly happy to see how my reasoning changed during the exercise:
- At first I was very nervous running the tests
- Then I started to do smaller and smaller changes
- In the end, I instead felt confident and I found myself thinking:
Better commit this, by running the tests
.
In the end, the revert and deletion of my code felt like a relief almost and since I didn’t write that much code I took the opportunity to think through what I needed to do once more.
All in all, I ended up with better code written in smaller chunks. That made me feel pretty good.
Hope you found this interesting to follow along in. My code is here
If you liked this post ... here's more for you:
Published by
on Last updated