Guidance Cards - Code

From School of CS Student Wiki
Jump to navigation Jump to search

Part of Red-Green-Go!
Good collection of testing anti-patterns

Code Card 1

Requires Review/Re-wording

Take the simplest route to making the current suite of tests pass.
Code 1.jpg

TThis idea is one of the cornerstones of TDD, and lies at the heart of what we are aiming to achieve during the middle ("code") step of the red-green-green cycle. Once we have our failing test, describing the small increment of behaviour we are going to focus on in this increment, our next goal is to get the code back to a "green" state as quickly and as directly as possible. We are not concerned with the beauty and elegance of the code in this step, nor with considerations or efficiency or maintainability. We'll worry about those things in the refactoring step. In the code step, we are aiming to find the smallest simplest change to the code that makes the new test pass, while not breaking any of the existing tests.

Kent Beck explains this idea well, in his book "Test Driven Development by Example" (p.11):

The goal is clean code that works (thanks to Ron Jeffries for this pithy summary). Clean code that works is out of the reach of even the best programmers some of the time, and out of the reach of most programmers (like me) most of the time. Divide and conquer, baby. First we'll solve the "that works" part of the problem. Then we'll solve the "clean code" part. This is the opposite of architecture-driven development, where you solve "clean code" first, then scramble around trying to integrate into the design the things you learn as you solve the "that works" problem.

So, in the code step, we want to write production code that does exactly what the new test asks for, no more than that and no less. We want to match the specification, as given by the test suite, as closely as we can.


What Does this Mean in Practice?

In practice, this means that the code we write during this step can look very strange to someone not familiar with the TDD approach. For example, suppose we are writing code that filters all the negative numbers from an array of integers. (Apologies for the Noddy example, but a more realistic scenario would just obscure the message we're trying to get across.) Suppose further that we have reached the point in the development where we are ready to consider arrays of size greater than one, and so write the following test:

int[] twoNegatives = { -1, -1 };
int[] emptyCollection = {}
assertThat(allPositiveNumbersIn(twoNegatives), is(emptyCollection));

When we come to write the production code to make this test pass, we should put aside our awareness that we are eventually aiming to write code to filter arrays of arbitrary size and contents, and focus only on this one test case. The thing which distinguishes the array in this test case from the ones we have worked with so far is that it is longer; this array has two items, while previously all our test cases have dealt with arrays of length 0 or 1. So, we can quickly make the test pass by using this condition to work out what result we have to return:

     public int[] allPositiveNumbersIn(int[] numberCollection) {
          if (numberCollection.length == 2)
               return new int[0];

          ... // The code that we have written so far
     }

So I Have to Write Bad Code?

But, but, but --- you might be saying right now --- that's horrible! And nothing like the eventual solution! Why do all this work we'll just have to delete?

In conventional programming terms, you would be right to say this. But, TDD works differently.

Remember that we are aiming to write only the code that is needed to satisfy the specification we have (that is, the test cases we have written so far). If we write anything more than this simple condition, we will be jumping ahead of ourselves, writing the general solution (in a big step) rather than dealing just with the new functionality required by the new test case. Yes, more tests will probably come along soon that will cause us to change these two lines of code, but deleting code we have written is not the only way to waste time on a software project. Suppose we had jumped ahead and written the more general solution to the problem in one step, and then the project was cancelled or changed, so that the more general solution was actually never needed. That is another form of waste, that TDD guards against.

And how much of the code we wrote for this TDD step is actually wasted? We do need to create a new array, in which to store the filtered integers, which will be returned. And in some cases, the value returned will be an empty array. There will also have to be some kind of check on the length of the array in the completed code, so that part of our thinking (and, to a lesser extent, our typing) is not actually wasted at all.


This change of mindset can be hard for some programmers (perhaps especially good ones) to make at first. These techniques contradict many of the notions of "good" code and design that we have been teaching for decades. It is only when we see some of the benefits, and the way that writing the simplest, most direct code pushes out the correct functionality we need, that we can begin to understand that writing the simplest code, and keeping close to the tests, actually require skills not very different from the design and coding principles we have been taught. In fact, TDD does not break these design rules, it is only a very different expression of these basic principles, applied in a somewhat different way.


But I Could Write the Solution to this Example Correctly in One Step

It's true that there are many circumstances in which applying full-blown TDD is overkill. When we are writing code that does not need to be reliable and correct, or code that will be used once or twice and then thrown away. If we are writing experimental code, just to learn a new technique or technology, then we would not need to use TDD. Similarly, if the code we need to write is very trivial or familiar, and (if tests are needed) you have the discipline to put good tests in place without the scaffolding of TDD, then you don't need to apply it.

But, if the code has a real customer, if its robustness is important to you, if the code will live for a long time and be maintained over a period of years, if the requirements are highly volatile or are complex so that no one really understands them, if any of these conditions apply to you, then TDD may deliver the results you are looking for.


http://goo.gl/Etmtc6 | QR Code

Code Card 2

Requires Review/Re-wording

Use if-statements in the production code to allow several hard-coded results to be returned, satisfying several tests.
Code 2.jpg
Suppose we are writing code to play the game FizzBuzz. The first test we might write would be:

assertThat(theFizzBuzzResponseFor(1), is("1"));

We quickly produce the following production code to make this test pass:

public String theFizzBuzzResponseFor(int number) {
     return "1";
}

Next we write the test:

assertThat(theFizzBuzzResponseFor(2), is("2"));

This fails, so we can move on to write the production code that will make it pass. Now that we have two test cases, we clearly can't just return the value expected by this new test:

public String theFizzBuzzResponseFor(int number) {
     return "2";
}

Now our new test will pass, but our first test will fail. We need to find a quick and simple way to make both tests pass.

If we can find some property of the two numbers that will allow us to tell which has been passed as a parameter, then we can instruct our method to return the appropriate one:

public String theFizzBuzzResponseFor(int number) {
     if (number == 1)
          return "1";
     return "2";
}

Get the idea? Of course, this code will not do for our final implementation of the full method. But, for now, this gets our tests passing quickly. We'll wait until later tests come along to drive out the more complex behaviour that the final method will need. To do any more at this stage would be a violation of the YAGNI principle ("you ain't gonna need it"), and be adding unnecessary gold plating.

We can keep going with this approach indefinitely. For example, if we next add the test:

assertThat(theFizzBuzzResponseFor(3), is("Fizz"));

we can make all our tests pass by writing:

public String theFizzBuzzResponseFor(int number) {
     if (number == 1)
          return "1";
     if (number == 2)
          return "2";
     return "Fizz";
}

And so on, and so on.

In practice, of course, we will mop up these conditional statements periodically in our refactoring steps, as they reveal the underlying patterns needed to implement the functionality in a more general way. Check out the FizzBuzz example to see several instances of this growth of if-statements plus their subsequent mopping up in the refactoring steps.


= Further Resources

For a fuller discussion, see Kent Beck's description of his "Fake It (‘Til You Make It)" approach:

http://gmarik.info/notes/programming/test-driven-development-kent-beck


http://goo.gl/B5TDyW | QR Code

Code Card 3

Requires Review/Re-wording

The simplest way to make one test case pass is often to hard-code its expected result as the return value.'
Code 3.jpg
When we have just one test case to pass (in our first red-green-green cycle), all we know about the requirements for the system is that it should return the value (or objects) expected by the test. For example, if we have the following test:

Product fridge = new Fridge();
fridge.setPurchasePrice(new Pounds(100,00));
assertThat(amountOfPurchaseTaxDueOn(fridge), is(new Pounds(17, 50)));
What is the simplest possible implementation of
amountOfPurchaseDueOn()
that will make this single test pass?

The answer is the following:

public Money amountOfPurchaseTaxDueOn(Product product) {
     return new Pounds(17, 50);
}

If this implementation seems overly simplistic, you are right --- compared with the specification for the full system, it is indeed far too simplistic and incomplete. But, compared with the specification as it currently stands (containing just this one test), this implementation does everything that is required.

But, it does indicate a problem with the code we have written. If a simple implementation like this can satisfy our test suite, then the problem (from a TDD point of view) is not with the production code but with the test suite. We need to add more tests, to force us to write the more complex implementation that our understanding of the domain tells us is needed.


http://goo.gl/RtMSde | QR Code

Compile-to-Compile

Requires Review/Re-wording

When writing code work in steps that get you back to compiling code as soon as possible.

Definitely need a concrete example - code change that requires several steps. Breaks it down in compiling steps.

• This applies to refactoring as well
We will make mistakes. If you leave a long gap between writing code and compiling you may lose your progress. Compiling code means you can run the tests. Code which does not compile has no degree of confidence in the changes made since the last compile.


recompiling consideration

Green-to-Green

Requires Review/Re-wording

When writing code work in steps that get you back to passing test suites as soon as possible. This also applies to refactoring. We want to minimise the ammount of code changes between passing states of the codes so we can find errors quickly. Project that shows all the red crosses along the project structure appears as we type new code.

Authors

  • gravatar Mbasssme [userPHRhYmxlIGNsYXNzPSJ0d3BvcHVwIj48dHI+PHRkIGNsYXNzPSJ0d3BvcHVwLWVudHJ5dGl0bGUiPkdyb3Vwczo8L3RkPjx0ZD51c2VyPGJyIC8+PC90ZD48L3RyPjwvdGFibGU+] ·
  • gravatar Mbax9mb5 [userPHRhYmxlIGNsYXNzPSJ0d3BvcHVwIj48dHI+PHRkIGNsYXNzPSJ0d3BvcHVwLWVudHJ5dGl0bGUiPkdyb3Vwczo8L3RkPjx0ZD51c2VyPGJyIC8+PC90ZD48L3RyPjwvdGFibGU+]