In brief, it’s this: whenever you are doing a bugfix, do it in two commits.
- Write a passing test that exemplifies the buggy behaviour.
- Fix the bug and also fix the test we just wrote.
For example,
def add(a, b):
return a + b + 1
and through your keen eye you observe that this addition function is buggy. You therefore write a test that records the bad behaviour,
Commit 1
diff --git a/test_arith.py b/test_arith.py
--- a/test_arith.py
+++ b/test_arith.py
@@ -0,0 +1,2 @@
+def test_add(a, b):
+ assert add(2, 2) == 5
Then comes the bugfix,
Commit 2
diff --git a/arith.py b/arith.py
--- a/arith.py
+++ b/arith.py
@@ -1,2 +1,2 @@
def add(a, b):
- return a + b + 1
+ return a + b
diff --git a/test_arith.py b/test_arith.py
--- a/test_arith.py
+++ b/test_arith.py
@@ -1,2 +1,2 @@
def test_add(a, b):
- assert add(2, 2) == 5
+ assert add(2, 2) == 4
BUT Y THO
Two reasons:
- Demonstrable proof that the bugfix actually fixed the bug
- CI still passes at every commit
The traditional way to do this is to do a bugfix and add a test in the same commit. But what if the test accidentally already passed before the alleged bugfix and the alleged bugfix didn’t actually work? In that case the added test gives false confidence that the bugfix worked.
Alright, show me
Here’s a less obvious example.
Suppose we want to make sure users are not underage,
def age_in_years(birthday, today):
return today.year - birthday.year
def can_access(birthday, today):
return 18 <= age_in_years(
birthday,
today
)
You notice that this age check is buggy. It’s only considering the years. If it’s October 2026, but you were born on December 2008, you are not 18 yet.
To fix it, the problem is just an off-by-one.
diff --git a/dates.py b/dates.py
--- a/dates.py
+++ b/dates.py
@@ -4,7 +4,7 @@ def age_in_years(birthday, today):
return today.year - birthday.year
def can_access(birthday, today):
- return 18 <= age_in_years(
+ return 18 < age_in_years(
birthday,
today
)
diff --git a/test_date.py b/test_date.py
--- a/test_date.py
+++ b/test_date.py
@@ -0,0 +1,5 @@
+def test_can_access():
+ assert can_access(
+ date(2007, 1, 1),
+ date(2026, 1, 1)
+ )
The astute reader will observe the problem isn’t in the can_access function but in the age calculation. The bugfix isn’t an actual bugfix and the proposed test actually passes both before and after the proposed fix.
The Kluever Maneuver would have prevented this.
We wrongly check that someone born on New Year’s Eve in 2008 is eighteen on New Year’s Day in 2026.
commit 1
diff --git a/test_date.py b/test_date.py
--- a/test_date.py
+++ b/test_date.py
@@ -0,0 +1,5 @@
+def test_can_access():
+ assert can_access(
+ date(2008, 12, 31),
+ date(2026, 1, 1)
+ )
Then we fix the age calculation and flip the test.
commit 2
diff --git a/dates.py b/dates.py
--- a/dates.py
+++ b/dates.py
@@ -1,7 +1,10 @@
+from dateutil.relativedelta import relativedelta
def age_in_years(birthday, today):
- return today.year - birthday.year
+ return relativedelta(
+ today,
+ birthday
+ ).years
def can_access(birthday, today):
return 18 <= age_in_years(
diff --git a/test_date.py b/test_date.py
--- a/test_date.py
+++ b/test_date.py
@@ -1,5 +1,5 @@
def test_can_access():
- assert test_can_access(
+ assert not test_can_access(
date(2008, 12, 31),
date(2026, 1, 1)
)
What about bug tracking?
Ah, yes, it’s also a good idea to log a bug in your tracker.
Reference the bug in your first commit.
add a test showing the bug
diff --git a/test_arith.py b/test_arith.py
--- a/test_arith.py
+++ b/test_arith.py
@@ -0,0 +1,2 @@
+FIXME: bug #1729
+def test_add(a, b):
+ assert add(2, 2) == 5
Record the fix in the second commit:
remove off-by-one error
fixes: #1729
diff --git a/arith.py b/arith.py
--- a/arith.py
+++ b/arith.py
@@ -1,2 +1,2 @@
def add(a, b):
- return a + b + 1
+ return a + b
diff --git a/test_arith.py b/test_arith.py
--- a/test_arith.py
+++ b/test_arith.py
@@ -1,2 +1,2 @@
-FIXME: bug #1729
def test_add(a, b):
- assert add(2, 2) == 5
+ assert add(2, 2) == 4
Okay but why “Kluever Maneuver”?
Oh. Because my colleague Kurt Kluever taught it to me, and I immediately think back to him when I think of this technique. He usualy calls it “terrible tests”, as in “Jordi, can you please write a terrible test for this?” and off I go to write a test asserting something I know to be wrong.
Leave a Reply