No True TDD Professional!
Someone on LinkedIn is complaining that this:
…is not something a “true TDD professional would do”.
Well… yes and no :)
That is, indeed, a perfect green-phase code for the changes required by the exercise.
I would start exactly with a test like that:
class Test(TestCase):
def test_encode_run_lengths(self):
assert (encode_run_lengths("aaaabbbcca") ==
[("a", 4), ("b", 3), ("c", 2), ("a", 1)])
It would exercise an empty function so it would fail (red phase):
def encode_run_lengths(s):
pass
Then I would do exactly what the person in that screenshot suggests:
def encode_run_lengths(s):
return [("a", 4), ("b", 3), ("c", 2), ("a", 1)])
And that point, I would stop and think about what I really need to solve the exercise.
All I need is a function that takes an input string and a start index, and returns the longest sequence of the same character, as well as the last index where it stopped.
So let’s write a test for it:
class Test(TestCase):
def test_encode_run_length(self):
assert encode_run_length("aaaaa",0) == ("a", 5, 5)
The code, at this point, is just:
def encode_run_length(s, start_index):
pass
The test will fail.
Let’s make the test pass:
def encode_run_length(s, start_index):
return "a", 5, 5
We can now refactor:
def encode_run_length(s, start_index):
i = start_index
while i < len(s):
if i == start_index or s[i] == s[i-1]:
i += 1
else:
break
return s[start_index], i-start_index, i
Note that I’m not interested in creating optimal code at this stage nor evaluate all edge cases. I just want to implement the granular behaviour I’m laser-focusing on right now.
The test is now passing.
We can now add more cases:
class Test(TestCase):
def test_encode_run_length(self):
assert encode_run_length("aaaaa",0) == ("a", 5, 5)
assert encode_run_length("aaaaabbb",5) == ("b", 3, 8)
assert encode_run_length("aaaaa",4) == ("a", 1, 5)
assert encode_run_length("a", 0) == ("a", 1, 1)
How about edge/invalid cases though? If we add them:
assert not encode_run_length("aaaaa", 5)
assert not encode_run_length("aaaaa", -1)
assert not encode_run_length("", 0)
assert not encode_run_length(None, 0)
The test fails! We did not consider them?
Let’s guard against those scenarios:
def encode_run_length(s, start_index):
if s is None or not (0 <= start_index < len(s)):
return None
...
Now the test passes.
Now that we are confident our building block works, let’s go back to the initial green phase and refactor that code:
def encode_run_lengths(s):
groups = []
index = 0
while True:
group = encode_run_length(s, index:q)
if not group:
break
groups.append((group[0], group[1]))
index = group[2]
return groups
Again, I’m not interested in having huge quality code at this stage. I just want to validate the behaviour.
The test passes!
I can now refactor the code (maybe with a little help of AI) for a bit of elegance:
def encode_run_length(s, start_index):
if not (0 <= start_index < len(s)):
return None
char = s[start_index]
count = 1
i = start_index + 1
while i < len(s) and s[i] == char:
count += 1
i += 1
return char, count, i
def encode_run_lengths(s):
groups = []
index = 0
while index < len(s):
result = encode_run_length(s, index)
if result is None:
break
char, count, next_index = result
groups.append((char, count))
index = next_index
return groups
We obviously want to check that the test still pass–and they do!
So, yes, a TDD professional would use the code in the screenshot, but maybe no true TDD professional? :)