Trying out test commit or revert

· January 11, 2019

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:

  1. mkdir fizzbuzz-tcr && cd fizzbuzz-tcr to create the directory and jump into it

  2. npm init -y to create an empty package.json file

  3. npm I -D mocha chai standard to install the tools I need

  4. touch index.js index.test.js to setup the two files we will work in

  5. I wrote scripts for test, lint and pretest

    "scripts": {
        "lint": "standard",
        "pretest": "standard",
        "test": "mocha -D bdd -R list ."
    }
    
    1. I’m linting my code with standard js
    2. The testing is done using mocha
    3. And the pretest script is automatically running the linting before the tests are executed
  6. 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);
      });
    });
    
  7. By running npm t I linted and ran the first test

  8. I created a .gitignore from the excellent https://www.gitignore.io/

  9. 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
  • 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

Twitter, Facebook