2

TCR: A pulverizer for coding tasks

 3 years ago
source link: https://flexport.engineering/tcr-a-pulverizer-for-coding-tasks-f059786451d6
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

TCR: A pulverizer for coding tasks

I’m a big fan of both Martin Fowler and Kent Beck. So I got excited when I saw this on Twitter:

(Kent’s past “crazy ideas” include Extreme Programming and JUnit.)

Kent Beck explains TCR

I went to one of Kent’s workshops, where he explained that TCR is a new approach to coding, like TDD. It stands for test && commit || revert. The concept is simple: every time you write some code, you run your tests. If the tests pass, the code is immediately committed—and if not, it is immediately reverted. Like, reset --hard reverted.

During the workshop, as we experimented with TCR ourselves, I used this Node script:

#!/bin/sh(node test.js && (git add . && git commit -am working)) || git reset --hard

Here’s a different one I used on a small Ruby project:

#!/bin/sh(git diff --name-only | grep Gemfile) && bundlegit add .dotenv rspec && (git add . && git commit -m tcr) || (git add . && git reset --hard)

Note: I’ve included multiple git add commands in case tests themselves generate output to be committed; for instance, when using a gem like vcr.

Run that after every change you make. If you’re really gutsy, like Kent, you set up your IDE to run it on save. (I didn’t quite do that, but I did end up making it my build script in Sublime Text 3, which was pretty much equivalent.)

And that’s the whole idea. With TCR, your code is constantly getting committed—or erased. You never build up uncommitted code. And your tests are always green.

1*Th-lHVMxBPTWxSs8AckB5Q.png?q=20
tcr-a-pulverizer-for-coding-tasks-f059786451d6
Writing toy functions in Node using TCR. There’s a bug here (see it?), so all the added lines (indicated by the gutter) are about to get deleted as soon as I build.

Staying close to reality

I was intrigued by TCR because it resonates with one of my deepest principles of making things: hew as close to reality as possible.

The best code is code that is actually working in production. But second-best is a green test suite. The more you drift away from a working test suite, the more you are probably setting yourself up for an enormous amount of pain. So I immediately liked the idea of anything that would force you to get your tests passing with every single commit.

But as I was about to discover, TCR makes failing tests very painful indeed.

Cheating

I quickly found out how annoying and frustrating it can be to fail tests in TCR. One little mistake, and you lose your work — everything you just did is deleted.

(By the way, you’re not supposed to hit undo in your editor to bring back the changes—Kent said that’s cheating. You’re also not supposed to run the test suite on its own, outside of the TCR invocation.)

My initial reaction to TCR was to start hating tests. Tests were the things that were causing my hard work to get erased! If I just didn’t write any tests, or didn’t extend them, I could save my work. So I would write a bunch of code, and then, very gingerly extend the test suite to cover it. That kept the code intact!

1*f1X5_IZ-c2B_iizP6V3tgQ.png?q=20
tcr-a-pulverizer-for-coding-tasks-f059786451d6
Whee! With no tests I can write as much code as I want! It probably works…

I noticed, however, that Kent wasn’t doing this when he demonstrated. I eventually decided that this too was a form of “cheating,” that it wasn’t getting the benefits that had initially intrigued me.

Coding two lines at a time

When you do TCR without “cheating”, what it actually pushes you to do is to write your code extremely incrementally—literally about two lines at a time.

Implementing a new method? First just define it, returning null. Test && commit || revert. Add a test that invokes the method and checks for a null return value. TCR. Then start on the method body. Does it have a loop? Write the body of the loop first. Test that it works on one element. Then wrap the loop around it and test that it works for multiple elements.

1*GYfcmTtlCmy01nXBtXyTwg.png?q=20
tcr-a-pulverizer-for-coding-tasks-f059786451d6
Building rot13 in a better way. Step 1, make it work for a 1-char string.
1*NQ6ZnOxLztD7vmgIXnL7Eg.png?q=20
tcr-a-pulverizer-for-coding-tasks-f059786451d6
Step 2, introduce an accumulator variable, even though there’s no loop yet.
1*GNyBBoWT20i7gy8hgS_XDg.png?q=20
tcr-a-pulverizer-for-coding-tasks-f059786451d6
Step 3, add a loop.
1*H8cyCoYMVP9Zigtn5ggtiQ.png?q=20
tcr-a-pulverizer-for-coding-tasks-f059786451d6
Step 4, add an if statement inside the loop.

Refactoring? One line at a time. Did you extract an expression into a local variable? TCR. Splitting a big method in two? Define an empty second method first. TCR. Then move one or two lines at a time from the original method into the new one, moving the boundary between them by just that much each time.

Even tests can be done incrementally. First just define the test, with no assertions. TCR. Then add one very simple assertion, like “does not error”. TCR. Narrow it a bit: assert the type of the result, but not its value. TCR. Then assert the value.

1*FP7vuurbINxeOn9yE-qofw.png?q=20
tcr-a-pulverizer-for-coding-tasks-f059786451d6
Three different levels of strictness in assertions

Do you have to do this? No. But wow, is it solid when you do.

Is it cheating to have any untested code, even a stub method that returns nil, or an empty class declaration? I think not, because you’re still learning something from each round of TCR here. You know you’re still learning because the TCR can fail: you could have a syntax error. Learning that “the two lines of code I added have no syntax errors” may seem trivial, but the whole point of TCR is to divide your work into trivial pieces. It’s like a pulverizer for coding tasks, atomizing big chunks of work into very fine particles.

What tests mean in TCR

It’s natural to compare TCR to TDD. I don’t do TDD consistently when I code, but I use it sometimes because I appreciate certain things that it gets you. One is that it pushes you to full test coverage. Another is that seeing the tests go from red to green helps you verify the test itself.

In TCR, I found myself changing my view of what tests are for and what they even mean. First, obviously, you never run a test you expect to fail (unless you’re prepared to type it in all over again). Second, I don’t necessarily write the test before the code (although I try to stick extremely close to full coverage).

What if you make a change to the way a method works, and it’s going to affect a lot of tests all at once? If there is anything that TCR encourages, it’s not making a lot of changes all at once to anything, including the tests.

One way to handle this would be:

  1. Make a copy of the method.
  2. Change its behavior.
  3. Begin copying the tests, one at a time, modifying each one.
  4. Delete the old method and its tests.

Another way to handle it is broaden-change-narrow:

  1. Broaden the tests by relaxing their assertions to cover both the old and the new behavior. (E.g., change result == 'foo-bar' to result.match(/foo.bar/)
  2. Change the behavior. (E.g., from outputting foo-bar to foo_bar)
  3. Tighten the assertions one at a time, fixing anything that doesn’t match your expectations. (E.g., result == 'foo_bar')

After spending some time with TCR, I decided that the meaning of tests, what they represent, is different than in TDD. In TDD, a test represents desired behavior. In TCR, a test represents your current understanding of behavior. Since tests always have to work, you always write them to match what you believe the system is doing right now.

Thus, TDD leads you to a cycle of red-green-refactor. In TCR, the cycle is more like: old-new-refactor. First, you write a test for the current behavior of the system, even if it’s wrong. Then you update to the new behavior, changing the test along with it.

If TCR gets better tooling, one thing I’d love to see is a test framework that lets you annotate tests to say which ones represent desired behavior versus not. This would let you write checks like “you cannot merge to master any tests that represent undesired behavior.”

TCR in the real world

Outside Kent’s workshop, so far I’ve only tried TCR on a microservice that I was writing from scratch. One big advantage of this is that the test suite ran in under 100ms. In a real-world, monolithic webapp, a test suite can take 10–20 minutes to run, even under heavy parallelization. This would make TCR unbearable.

I don’t know the solution to this yet, and neither did Kent Beck when we did the workshop. It could be a reason to drive towards microservices, or a reason to invest more in test performance. And/or maybe in practice you only run a subset of your tests on each iteration.

Should you use this for real?

Not all the time.

TCR is a tool, so put it in your toolbox and pull it out when it seems useful. Which is also my approach most other coding techniques, from TDD to pair programming.

Let’s finish by returning to the idea of “hewing close to reality.” If TDD is like galloping on a horse, touching the ground frequently but not constantly, then TCR is like driving a tank with treads that are in constant contact. Maybe that points out when to use it: when the going gets tough and you need to be unstoppable.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK