Hi. I'm Jon Jagger, director of software at Kosli.
I built cyber-dojo, the place teams practice programming.
Showing posts with label tdd. Show all posts
Showing posts with label tdd. Show all posts
Atlantec conference
It was a pleasure and an honour to speak at the first Atlantec conference held in Galway, Ireland on May 15th.
I talked about cyber-dojo and showed some statistics from a random sample of its 30,000+ cyber-dojos,
together with a few examples of code/tests typically submitted, a few dashboard patterns, and wrapped up linking testing to Le Chatelier's Law and some of my favourite Systems Thinking quotes from Bradford Keeney.
print "squashed-circle" diamond
There's been a bit of a buzz about the Print-Diamond practice recently.
I recall doing this a couple of years ago with Johannes Brodwall.
In cyber-dojo naturally.
We took a wrong turn and were making a thorough mess of it.
I vividly recall Johannes saying:
Viewed like this you can think of the Diamond as a sort of squashed circle with the A,B,C characters all lying on the circumference. From here it was a short step to this (Ruby):
which, when puts'd gives:
And we knew we were on our way.
Let's hear it for Curry Driven Development!
This is too difficult. We're doing it wrong.I love that. If it's difficult you're probably doing it wrong. We gave up and took a break. We got a really nice Indian take away. Then we went back to Print-Diamond. Very quickly we came up with a new idea. We imagined the diamond lying in the center of an x,y axis. The Print-Diamond of 'C' would therefore look like this:
-2 -1 0 +1 +2
-2 - - A - -
-1 - B - B -
0 C - - - C
+1 - B - B -
+2 - - A - -
Viewed like this you can think of the Diamond as a sort of squashed circle with the A,B,C characters all lying on the circumference. From here it was a short step to this (Ruby):
(-2..+2).map{|row|
(-2..+2).map{|col|
row.abs + col.abs == 2 ? 'X' : '-'
}.join
}
which, when puts'd gives:
--X-- -X-X- X---X -X-X- --X--
And we knew we were on our way.
Let's hear it for Curry Driven Development!
another interesting TDD episode
The classic TDD cycle
says that you should start with a test for new functionality and see it fail.
There is real value in not skipping this step; not jumping straight to writing code to try to make it pass.
I started by writing my first test, like this:
I made this fail by writing the initial code as follows (the
which gave me the diagnostic:
I made this pass with the following slime
Next, I returned to the test and added a test for 6:
I ran the test, fully expecting it to fail, but it passed!
Can you see the problem?
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
The problem is in
Here's what's happening:
My mistake was in the test;
I ran the test again and this time it failed :-)
I made the test pass:
Let's hear it for starting with a test for new functionality and seeing it fail.
There is real value in not skipping this step; not jumping straight to writing code to try to make it pass.
- One reason is improving the diagnostic. Without care and attention diagnostics are unlikely to diagnose much.
- A second reason is to be sure the test is actually running! Suppose for example, you're using JUnit and you forget its @Test annotation? Or the public specifier?
- A third reason is because sometimes, as we saw last time, you get an unexpected green! Here's another nice example of exactly this which happened to me during a cyber-dojo demo today.
I started by writing my first test, like this:
static void assert_fizz_buzz(const char * expected, int n)
{
char actual[16];
fizz_buzz(actual, sizeof actual, n);
if (strcmp(expected, actual) != 0)
{
printf("fizz_buzz(%d)\n", n);
printf("expected: \"%s\"\n", expected);
printf(" actual: \"%s\"\n", actual);
assert(false);
}
}
static void numbers_divisible_by_three_are_Fizz(void)
{
assert_fizz_buzz("Fizz", 3);
}
I made this fail by writing the initial code as follows (the
(void)n is to momentarily avoid the
"n is unused" warning which my makefile promotes to an error
using the -Werror option):
void fizz_buzz(char * result, size_t size, int n)
{
(void)n;
strncpy(result, "Hello", size);
}
which gave me the diagnostic:
...: assert_fizz_buzz: Assertion `0' failed. fizz_buzz(3) expected: "Fizz" actual: "Hello"
I made this pass with the following slime
void fizz_buzz(char * result, size_t size, int n)
{
if (n == 3)
strncpy(result, "Fizz", size);
}
Next, I returned to the test and added a test for 6:
static void numbers_divisible_by_three_are_Fizz(void)
{
assert_fizz_buzz("Fizz", 3);
assert_fizz_buzz("Fizz", 6);
}
I ran the test, fully expecting it to fail, but it passed!
Can you see the problem?
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
The problem is in
assert_fizz_buzz which starts like this:
static void assert_fizz_buzz(const char * expected, int n)
{
char actual[16];
...
}
Here's what's happening:
assert_fizz_buzz("Fizz", 3)is calledchar actual[16]is definedfizz_buzz(actual, sizeof actual, 3)is calledif (n == 3)istrue"Fizz"isstrncpy'd intoactualfizz_buzz(actual, sizeof actual, 3)returnsstrcmpsays thatexpectedequalsactual- ...
assert_fizz_buzz("Fizz", 6)is calledchar actual[16]is definedactualexactly overlays its previous location so its first 5 bytes are still'F','i','z','z','\0'fizz_buzz(actual, sizeof actual, 6)is calledif (n == 3)isfalsefizz_buzz(actual, sizeof actual, 6)returnsstrcmpsays thatexpectedequalsactual
My mistake was in the test;
actual has automatic
storage duration so does not get initialized.
It's initial value is indeterminate.
The first call to assert_fizz_buzz is accidentally interfering
with the second call.
Tests should be isolated from each other.
I tweaked the test as follows:
static void assert_fizz_buzz(const char * expected, int n)
{
char actual[16] = { '\0' };
...
}
I ran the test again and this time it failed :-)
...: assert_fizz_buzz: Assertion `0' failed. fizz_buzz(6) expected: "Fizz" actual: ""
I made the test pass:
void fizz_buzz(char * result, size_t size, int n)
{
if (n % 3 == 0)
strncpy(result, "Fizz", size);
}
Let's hear it for starting with a test for new functionality and seeing it fail.
an interesting TDD episode
I'm doing the roman-numerals kata in C.
I write a test as follows:
I write a do-nothing implementation of
I run the tests and I get (I kid you not) this:
So I work towards improving the diagnostic with a custom assert, as follows:
I run this and my diagnostic is as follows:
Much better :-)
Now I start to implement
And I'm at green.
I refactor to this:
I refactor to this:
Remembering that in my test, n is one-hundred-and-eleven, I refactor to this:
I refactor to this:
And I'm still at green. Now I add a new test:
I run it and am amazed to see it pass.
It takes me a little while to figure out what is going on.
I'll take it line by line.
When
is this
and
which is this:
And
which is this:
And yet again
I edit the code to this:
And I'm still at green.
So now I'm wondering if there are any lessons I can learn from this episode. It was not a good idea to run the tests (to try and get an initial red) when doing so would knowingly cause the (unfinished) program to exhihibit undefined behaviour. In cyber-dojo terms an amber traffic-light is not the same as a red traffic-light. After adding the second test I should have edited
Then I would have got a proper red:
I write a test as follows:
#include "to_roman.hpp"
#include <assert.h>
#include <string.h>
int main(void)
{
char actual[32] = { '\0' };
to_roman(actual, 111);
assert(strcmp("CXI", actual) == 0);
}
I write a do-nothing implementation of
to_roman.I run the tests and I get (I kid you not) this:
...
test: to_roman.tests.c:26: main: Assertion `__extension__ ({ size_t __s1_len, __s2_len; (__builtin_constant_p ("CXI") && __builtin_constant_p (actual) && (__s1_len = __builtin_strlen ("CXI"), __s2_len = __builtin_strlen (actual), (!((size_t)(const void *)(("CXI") + 1) - (size_t)(const void *)("CXI") == 1) || __s1_len >= 4) && (!((size_t)(const void *)((actual) + 1) - (size_t)(const void *)(actual) == 1) || __s2_len >= 4)) ? __builtin_strcmp ("CXI", actual) : (__builtin_constant_p ("CXI") && ((size_t)(const void *)(("CXI") + 1) - (size_t)(const void *)("CXI") == 1) && (__s1_len = __builtin_strlen ("CXI"), __s1_len < 4) ? (__builtin_constant_p (actual) && ((size_t)(const void *)((actual) + 1) - (size_t)(const void *)(actual) == 1) ? __builtin_strcmp ("CXI", actual) : (__extension__ ({ const unsigned char *__s2 = (const unsigned char *) (const char *) (actual); register int __result = (((const unsigned char *) (const char *) ("CXI"))[0] - __s2[0]); if (__s1_len > 0 && __result == 0) { __result = (((const unsigned char *) (const char *) ("CXI"))[1] - __s2[1]); if (__s1_len > 1 && __result == 0) { __result = (((const unsigned char *) (const char *) ("CXI"))[2] - __s2[2]); if (__s1_len > 2 && __result == 0) __result = (((const unsigned char *) (const char *) ("CXI"))[3] - __s2[3]); } } __result; }))) : (__builtin_constant_p (actual) && ((size_t)(const void *)((actual) + 1) - (size_t)(const void *)(actual) == 1) && (__s2_len = __builtin_strlen (actual), __s2_len < 4) ? (__builtin_constant_p ("CXI") && ((size_t)(const void *)(("CXI") + 1) - (size_t)(const void *)("CXI") == 1) ? __builtin_strcmp ("CXI", actual) : (__extension__ ({ const unsigned char *__s1 = (const unsigned char *) (const char *) ("CXI"); register int __result = __s1[0] - ((const unsigned char *) (const char *) (actual))[0]; if (__s2_len > 0 && __result == 0) { __result = (__s1[1] - ((const unsigned char *) (const char *) (actual))[1]); if (__s2_len > 1 && __result == 0) { __result = (__s1[2] - ((const unsigned char *) (const char *) (actual))[2]); if (__s2_len > 2 && __result == 0) __result = (__s1[3] - ((const unsigned char *) (const char *) (actual))[3]); } } __result; }))) : __builtin_strcmp ("CXI", actual)))); }) == 0' failed.
...
So I work towards improving the diagnostic with a custom assert, as follows:
#include "to_roman.h"
#include <assert.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
static void assert_roman(const char * expected, int n)
{
char actual[32] = { '\0' };
to_roman(actual, n);
if (strcmp(expected, actual) != 0)
{
printf("to_roman(%d)\n", n);
printf("expected: \"%s\"\n", expected);
printf(" actual: \"%s\"\n", actual);
assert(false);
}
}
int main(void)
{
assert_roman("CXI", 111);
}
I run this and my diagnostic is as follows:
test: to_roman.tests.c:16: assert_roman: Assertion `0' failed. to_roman(111) expected: "CXI" actual: "" ...
Much better :-)
Now I start to implement
to_roman
#include "to_roman.h"
#include <string.h>
void to_roman(char * roman, int n)
{
roman[0] = '\0';
strcat(roman, "CXI");
}
And I'm at green.
I refactor to this:
#include "to_roman.h"
#include <string.h>
void to_roman(char * roman, int n)
{
roman[0] = '\0';
strcat(roman, "C");
strcat(roman, "X");
strcat(roman, "I");
}
I refactor to this:
#include "to_roman.h"
#include <string.h>
void to_roman(char * roman, int n)
{
const char * hundreds[] = { "C" };
const char * tens[] = { "X" };
const char * units[] = { "I" };
roman[0] = '\0';
strcat(roman, hundreds[0]);
strcat(roman, tens[0]);
strcat(roman, units[0]);
}
Remembering that in my test, n is one-hundred-and-eleven, I refactor to this:
#include "to_roman.h"
#include <string.h>
void to_roman(char * roman, int n)
{
const char * hundreds[] = { "", "C" };
const char * tens[] = { "", "X" };
const char * units[] = { "", "I" };
roman[0] = '\0';
strcat(roman, hundreds[1]);
strcat(roman, tens[1]);
strcat(roman, units[1]);
}
I refactor to this:
#include "to_roman.h"
#include <string.h>
void to_roman(char * roman, int n)
{
const char * hundreds[] = { "", "C" };
const char * tens[] = { "", "X" };
const char * units[] = { "", "I" };
roman[0] = '\0';
strcat(roman, hundreds[n / 100]);
n %= 100;
strcat(roman, tens[n / 10]);
n %= 10;
strcat(roman, units[n]);
}
And I'm still at green. Now I add a new test:
int main(void)
{
assert_roman("CXI", 111);
assert_roman("CCXXII", 222);
}
I run it and am amazed to see it pass.
It takes me a little while to figure out what is going on.
I'll take it line by line.
When
n == 222 this line:
strcat(roman, hundreds[n / 100]);
is this
strcat(roman, hundreds[2]);
and
hundreds[2] is an out-of-bounds index.
However, hundreds[2] just happens to evaluate to the same as
tens[0] which is the empty string. So at this point
roman is still the empty string.
The next lines are these:
n %= 100;
strcat(roman, tens[n / 10]);
which is this:
strcat(roman, tens[2]);
And
tens[2] is also an out-of-bounds index.
And tens[2] just happens to evaluate to the same as units[0]
which is also the empty string. So at this point
roman is still the empty string.
The next lines are these:
n %= 10;
strcat(roman, units[n]);
which is this:
strcat(roman, units[2]);
And yet again
units[2] is an out-of-bounds index.
This time units[2] just happens to evaluate to
"CCXXII" from the test!
So after this roman is "CCXXII" and the test passes!
Amazing!
I edit the code to this:
void to_roman(char * roman, int n)
{
const char * hundreds[] = { "", "C", "CC" };
const char * tens[] = { "", "X", "XX" };
const char * units[] = { "", "I", "II" };
roman[0] = '\0';
strcat(roman, hundreds[n / 100]);
n %= 100;
strcat(roman, tens[n / 10]);
n %= 10;
strcat(roman, units[n]);
}
And I'm still at green.
So now I'm wondering if there are any lessons I can learn from this episode. It was not a good idea to run the tests (to try and get an initial red) when doing so would knowingly cause the (unfinished) program to exhihibit undefined behaviour. In cyber-dojo terms an amber traffic-light is not the same as a red traffic-light. After adding the second test I should have edited
to_roman as follows:
void to_roman(char * roman, int n)
{
const char * hundreds[] = { "", "C", "" };
const char * tens[] = { "", "X", "" };
const char * units[] = { "", "I", "" };
roman[0] = '\0';
strcat(roman, hundreds[n / 100]);
n %= 100;
strcat(roman, tens[n / 10]);
n %= 10;
strcat(roman, units[n]);
}
Then I would have got a proper red:
test: to_roman.tests.c:17: assert_roman: Assertion `0' failed. to_roman(222) expected: "CCXXII" actual: "" ...
lessons from testing
I have run hundreds of test-driven coding dojos using cyber-dojo.
I see the same test anti-patterns time after time after time.
Do some of your tests exhibit the same same anti-patterns?
I see the same test anti-patterns time after time after time.
Do some of your tests exhibit the same same anti-patterns?
Subscribe to:
Posts (Atom)