Test-Driven Development with “( Test && Commit ) || TestCodeOnly || Revert”

I learned through JB Rainsberger’s “The World’s Shortest Article on (test && commit) || revert” blog article that Kent Beck and Oddmund Strømme came up with a new workflow for developing software, inspired by the “Limbo strategy” for concurrent development with a distributed team, summarized quite succinctly as “test && commit || revert“.  A number of people have been talking about it.  Some have been trying it out to see how it works for them, rather than just engaging in armchair speculation.  And there have been some questions about how well the “test && commit || revert” process can work, given that it does not allow for the “Red” step of the traditional “Red, Green, Refactor” cycle of Test-Driven Development.

I find that adding one step, making it “( Test && Commit ) || TestCodeOnly || Revert“, enables using the well-known “Red, Green, Refactor” Test-Driven Development process, in a style that has been recommended by some for quite a few years.  Automating the process, as suggested by these expressions, would most likely improve our rigor and discipline in the TDD process.

So how does it work?

Kent’s “Test, Commit, Revert” expression, and mine, assume that software development is done in very short cycles, by writing a very small amount code, maybe just one line, “evaluating the expression,” and doing so again.

Each of the “words” in the expressions above are scripts or subroutines that do something and return a Boolean true/false result.

The “Test” action runs all the automated regression xUnit tests, and returns “true” if they all pass.  If one or more tests fail, it returns “false.”

The “Test && Commit” expression implies, due to “short circuit evaluation” of the logical “and” operation (“&&”) that the “Commit” script will only be run if “Test” returns true.

The “Commit” operation should save all the code changes you’ve made (since the last Commit), and return “true.”  This would write your changes to a shared source code repository, if you’re using one.

The next operation and script in Kent Beck’s version is “|| Revert”.  With short-circuit evaluation of this logical “or” operation, the “Revert” script will not be run unless the “Test” operation fails.

So Kent is suggesting that whenever any xUnit test fails, the system should automatically discard all the changes you’ve made to the code since the last time Test returned true.  That is, your code would revert to what it was the last time “Commit” was executed.

I happen to agree with Kent that this is fundamentally a good idea, in spite of the frustrations it may cause.  And this idea was proposed quite seriously by others quite a few years before this current conversation started.

It occurs to me that we can restore the traditional “Red, Green, Refactor” cycle, and improve the automation and rigor of recognized good Test-Driven Development process by adding only one more step to the expression — the TestCodeOnly step.

The “TestCodeOnly” step checks to see if you have changed only xUnit test code, or if your changes include the “Code Under Test.”  If you have only changed code in source directory holding the xUnit tests, then the “TestCodeOnly” step returns “true.”  But if you have changed any code at all in the “Code Under Test,” it returns “false.”  The “Code Under Test” is the “production code” that you intend to deliver in the working system.  The xUnit test code should be kept separate and not deployed into production.  This separation of “test” and “production” code is a widely used “best practice” that ensures a smaller, simpler, more well-focused production code release by avoiding the distraction and risk test-only code being deployed into production.

“( Test && Commit ) || TestCodeOnly || Revert”

So this is how this process would generally work:

You need to start with an environment where the xUnit tests pass.  For the first step in an empty environment, you’d write a single test that does nothing by pass.  Then evaluating the “( Test && Commit ) …” expression will get “true” for the “Test” running step, and “Commit” your code changes.

Write a very simple test, in typical TDD style, and evaluate the “( Test && Commit ) || TestCodeOnly || Revert” expression.  Because we have not written code to make the test pass, we should expect the test to fail, so “Test” will return “false.”  This skips the “Commit” operation and checks “TestCodeOnly.”  The “TestCodeOnly” step should return “true,” as we only changed test code, and this prevents the execution of the “Revert” step, which would throw away our changes.

This is the “Red” step of the “Red, Green, Refactor” cycle.  We have one failing test.

Now, as typical of TDD style, we write the minimum amount of code needed to make the test pass.  Then we evaluate the “( Test && Commit ) || TestCodeOnly || Revert” expression again.  We certainly hope that the xUnit “Test” run succeeds, which will result in an automatic “Commit” of our changes.

But if the xUnit “Test” run still fails, then “Test” will return “false.”  Because of this failure, the system will skip the “Commit” step and evaluate the “TestCodeOnly” check.  But this will return “false,” as we did change non-test code, in an attempt to make the test pass.  So the system goes on to run the “Revert” script, which throws away the failing test that we just wrote, and our quick simple attempt to make it pass.  At this point, if we run all the tests again, we will find that they all pass.

It can be frustrating to have the system automatically discard all your changes with a “Revert.”  But doing this does encourage us to “take smaller steps” in developing software, so that we do not risk losing too much.  And this approach has been shown, in practice, to be quite effective.

Example

Let’s suppose that we would like to implement a Fibonacci function in Java using Test-Driven Development, with the “( Test && Commit ) || TestCodeOnly || Revert” expression workflow.

Let’s start with a very simple JUnit test:

import junit.framework.TestCase;

public class FibonacciTest extends TestCase {

    public void test0() {
        assertEquals(0, Maths.fib(0));
    }

}

It doesn’t compile, as there is no “Maths” class, so let’s not run the expression yet.  I’ll let my IDE help me generate the “Maths” class and the “fib” function.  And I’ll put in the simplest possible implementation that will pass the JUnit test above.  I want the test to pass, to get the “Commit” instead of a “Revert” of the code I’ve just written.

public class Maths {
    
    public static long fib(final int index) {
        return 0;
    }

}

I run the “( Test && Commit ) || TestCodeOnly || Revert” expression, and because all the JUnit tests pass, “Test” returns “true,” and “Commit” saves my changes.

Let’s add another test, in the FibonacciTest class:

    public void test1() {
        assertEquals(1, Maths.fib(1));
    }

Evaluating the “( Test && Commit ) || TestCodeOnly || Revert” expression, I find that “Test” returns “false,” because this new test fails, but “TestCodeOnly” returns “true,” because I only made changes to the FibonacciTest class.  I did not change even a single character in the “Maths” class.

Now I need to make the simplest change possible in the “fib” function that will make all of the tests pass.  And I’d better do it right, or I’ll lose my change and my new test!  So, admitting that I’m kind of evil, I “take the lazy way out” and make a one word change to this:

    public static long fib(final int index) {
        return index;
    }

Evaluating the “( Test && Commit ) || TestCodeOnly || Revert” expression, I find that the tests pass, so it commits my changes.

Here’s another test:

    public void test2() {
        assertEquals(1, Maths.fib(2));
    }

Evaluating the “( Test && Commit ) || TestCodeOnly || Revert” expression, I see that the new test fails, but the “TestCodeOnly” check prevents the “Revert.”

I’ll fix it with something that returns the two possible values:

    public static long fib(final int index) {
        return (index == 0) ? 0 : 1;
    }

Evaluating the “( Test && Commit ) || …” commits.

Add a test:

    public void test3() {
        assertEquals(2, Maths.fib(3));
    }

This test fails, but does not cause a Revert.

I’ll do something crazy to make it pass:

    public static long fib(final int index) {
        return index + ((index < 2) ? 0 : -1);
    }

I don’t like how this code “looks,” but it does pass the tests, so I get a Commit.  So this would probably be a good time to refactor.  Something closer to the definition of the Fibonacci sequence would probably be easier to read and maintain.  So I’ll add this:

    public static long fib(final int index) {
        switch (index) {
            case 0:
                return 0;
            case 1:
                return 1;
        }
        return index + ((index < 2) ? 0 : -1);
    }

This passes and commits, so I’ll simplify it to this:

    public static long fib(final int index) {
        switch (index) {
            case 0:
                return 0;
            case 1:
                return 1;
            default:
                return index - 1;
        }
    }

I’m going in the direction of making it recursive, for the “default” case, but let’s add a test to justify adding that complexity:

    public void test4() {
        assertEquals(3, Maths.fib(4));
    }

Oh; it passes and commits.

Let’s try adding another test:

    public void test5() {
        assertEquals(5, Maths.fib(5));
    }

This test fails.

I still want to cheat a bit, like I did before:

    public static long fib(final int index) {
        switch (index) {
            case 0:
                return 0;
            case 1:
                return 1;
            default:
                return index - ((index < 5) ? 1 : 0);
        }
    }

This passes, but has a confusing and annoyingly complex expression.  Is “return index – ((index < 5) ? 1 : 0);” more or less complex than “return fib(index – 1) + fib(index – 2);”?  I’m going to say that the latter is easier to understand, so I refactor to it:

    public static long fib(final int index) {
        switch (index) {
            case 0:
                return 0;
            case 1:
                return 1;
            default:
                return fib(index - 1) + fib(index - 2);
        }
    }

Tests still pass, so it commits.  And I have a simple and easy to understand implementation.  It’s slow, but we can deal with that with a bit more refactoring, should we decide that this is important.

Advertisements

2 Responses to Test-Driven Development with “( Test && Commit ) || TestCodeOnly || Revert”

  1. Do not post on forums which are years old. Instead, do what i do: first, pick
    your personal style of piece. Will this article make contact with you circuitously? http://pafane.Mihanblog.com/post/comment/new/217/fromtype/postone/fid/15385185045bb3ede8d198c/atrty/1538518504/avrvy/0/key/34d53b3e63c80054701d2d623169976e/

  2. Jane says:

    Hi there! This post couldn’t be written much better! Reading
    through this article reminds me of my previous roommate!

    He always kept talking about this. I most certainly will send this information to him.
    Pretty sure he’s going to have a good read. I appreciate you
    for sharing! I’ve been surfing online more than 3
    hours today, yet I never found any interesting article like yours.
    It is pretty worth enough for me. In my opinion, if all webmasters and bloggers made good content
    as you did, the internet will be a lot more useful than ever before.

    I’ll right away clutch your rss as I can’t find your e-mail subscription hyperlink or newsletter service.
    Do you’ve any? Kindly allow me know so that I could subscribe.
    Thanks. http://foxnews.Co.uk/

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: