KEMBAR78
Program with GUTs | PDF
Program
with GUTs
Kevlin Henney
@KevlinHenney
@kevlin@mastodon.social
@kevlin.bsky.social
threads.net/@kevlin.henney
instagram.com/kevlin.henney
about.me/kevlin
linkedin.com/in/kevlin
kevlinhenney.medium.com
kevlin@curbralan.com
slideshare.net/Kevlin
So you’re writing unit tests?
Great!
Are they any good?
Kevlin Henney
“Program with GUTs”
medium.com/97-things/program-with-guts-828e69dd8e15
Do you have GUTs?
Kevlin Henney
“Program with GUTs”
medium.com/97-things/program-with-guts-828e69dd8e15
Very many people say “TDD”
when they really mean,
“I have good unit tests”
(“I have GUTs”?)
Alistair Cockburn
“The modern programming professional has GUTs”
(“I have GUTs
Or have you landed someone
(future you?) with interest-
accumulating technical debt
in their testbase?
Kevlin Henney
“Program with GUTs”
medium.com/97-things/program-with-guts-828e69dd8e15
What do I mean by good?
Kevlin Henney
“Program with GUTs”
medium.com/97-things/program-with-guts-828e69dd8e15
We think in generalities,
but we live in detail.
Alfred North Whitehead
mastodon.social/deck/@0xabad1dea@infosec.exchange/112015426639751687
public static bool IsLeapYear(int year) 
[Test]
public void Test() 
[Test]
public void TestIsLeapYear() 
[Test]
public void TestIsLeapYearIsOK() 
[Test]
public void TestIsLeapYearIsCorrect() 
[Test]
public void TestIsLeapYearWorks() 
[Test]
public void TestIsLeapYearWorksAsExpected() 
[Test]
public void Test1() 
[Test]
public void Test2() 
[Test]
public void TestLeapYears() 
[Test]
public void TestNonLeapYears() 
[Test]
public void TestLeapYears()
{
Assert.IsTrue(IsLeapYear(2024));
Assert.IsTrue(IsLeapYear(2000));
}
[Test]
public void TestNonLeapYears() 
[Test]
public void Test2024IsALeapYear() 
[Test]
public void Test2000IsALeapYear() 
[Test]
public void Test2023IsNotALeapYear() 
[Test]
public void Test1900IsNotALeapYear() 
[Test]
public void Test2016IsALeapYear() 
[Test]
public void Test2400IsALeapYear() 
[Test]
public void Test2022IsNotALeapYear() 
[Test]
public void Test2100IsNotALeapYear() 
Write Tests
for People
Gerard Meszaros
97-things-every-x-should-know.gitbooks.io/97-things-every-programmer-should-know/content/en/thing_95
Gerard Meszaros
97-things-every-x-should-know.gitbooks.io/97-things-every-programmer-should-know/content/en/thing_95
So who should you be
writing the tests for?
For the person trying to
understand your code.
Gerard Meszaros
97-things-every-x-should-know.gitbooks.io/97-things-every-programmer-should-know/content/en/thing_95
Good tests act as
documentation for the
code they are testing.
A year divisible by 4 is a leap year
A year divisible by 400 is a leap year
A year not divisible by 4 is not a leap year
A year divisible by 100 is not a leap year
[Test] public void
A_year_divisible_by_4_is_a_leap_year() 
[Test] public void
A_year_divisible_by_400_is_a_leap_year() 
[Test] public void
A_year_not_divisible_by_4_is_not_a_leap_year() 
[Test] public void
A_year_divisible_by_100_is_not_a_leap_year() 
A_year_divisible_by_4_is_a_leap_year
A_year_divisible_by_400_is_a_leap_year
A_year_not_divisible_by_4_is_not_a_leap_year
A_year_divisible_by_100_is_not_a_leap_year
A_year_not_divisible_by_4_is_not_a_leap_year
A_year_divisible_by_4_is_a_leap_year
A_year_divisible_by_100_is_not_a_leap_year
A_year_divisible_by_400_is_a_leap_year
A_year_not_divisible_by_4_is_not_a_leap_year
A_year_divisible_by_4_but_not_by_100_is_a_leap_ye
A_year_divisible_by_100_but_not_by_400_is_not_a_l
A_year_divisible_by_400_is_a_leap_year
A_year_not_divisible_by_4_is_not_a_leap_year
A_year_divisible_by_4_but_not_by_100_is_a_leap_year
A_year_divisible_by_100_but_not_by_400_is_not_a_leap_year
A_year_divisible_by_400_is_a_leap_year
Not leap years
Leap years
Not leap years
Leap years
Not divisible by 4 Divisible by 4 Divisible by 100 Divisible by 400
IsLeapYear_YearNotDivisibleBy4_ReturnsFalse
IsLeapYear_YearDivisibleBy4ButNotBy100_ReturnsTrue
IsLeapYear_YearDivisibleBy100ButNotBy400_ReturnsFalse
IsLeapYear_YearDivisibleBy400_ReturnsTrue
A_year_not_divisible_by_4_should_not_be_a_leap_year
A_year_divisible_by_4_but_not_by_100_should_not_be_a_leap_y
A_year_divisible_by_100_but_not_by_400_should_not_be_a_leap
A_year_divisible_by_400_should_not_be_a_leap_year
A_year_not_divisible_by_4_must_not_be_a_leap_year
A_year_divisible_by_4_but_not_by_100_must_not_be_a_leap_yea
A_year_divisible_by_100_but_not_by_400_must_not_be_a_leap_y
A_year_divisible_by_400_must_not_be_a_leap_year
A_year_not_divisible_by_4_is_not_a_leap_year
A_year_divisible_by_4_but_not_by_100_is_a_leap_year
A_year_divisible_by_100_but_not_by_400_is_not_a_leap_year
A_year_divisible_by_400_is_a_leap_year
Propositions
are vehicles
for stating
how things are
or might be.
Thus only indicative
sentences which it
makes sense to think
of as being true or as
being false are
capable of expressing
propositions.
public static bool IsLeapYear(int year)
{
return year % 4 == 0 &&
year % 100 != 0 ||
year % 400 == 0;
}
A_year_not_divisible_by_4_is_not_a_leap_year
A_year_divisible_by_4_but_not_by_100_is_a_leap_year
A_year_divisible_by_100_but_not_by_400_is_not_a_leap_year
A_year_divisible_by_400_is_a_leap_year
public static bool IsLeapYear(int year)
{
return year % 4 == 0;
}
A_year_not_divisible_by_4_is_not_a_leap_year
A_year_divisible_by_4_but_not_by_100_is_a_leap_year
A_year_divisible_by_100_but_not_by_400_is_not_a_leap_year
A_year_divisible_by_400_is_a_leap_year
A failing test should tell you exactly what is
wrong quickly, without you having to spend a lot
of time analyzing the failure.
This means...
Marit van Dijk
“Use Testing to Develop Better Software Faster”
medium.com/97-things/use-testing-to-develop-better-software-faster-9dd2616543d3
Each test should test one thing.
Marit van Dijk
“Use Testing to Develop Better Software Faster”
medium.com/97-things/use-testing-to-develop-better-software-faster-9dd2616543d3
Use meaningful, descriptive names.
Don’t just describe what the test does either (we
can read the code), tell us why it does this. This
can help decide whether a test should be
updated in line with changed functionality or
whether an actual failure that should be fixed
has been found.
Marit van Dijk
“Use Testing to Develop Better Software Faster”
medium.com/97-things/use-testing-to-develop-better-software-faster-9dd2616543d3
Never trust a test you haven’t seen fail.
Marit van Dijk
“Use Testing to Develop Better Software Faster”
medium.com/97-things/use-testing-to-develop-better-software-faster-9dd2616543d3
public class Leap_year_spec
{
public class A_year_is_a_leap_year
{
[Test]
public void if_it_is_divisible_by_4_but_not_by_100() 
[Test]
public void if_it_is_divisible_by_400() 
}
public class A_year_is_not_a_leap_year
{
[Test]
public void if_it_is_not_divisible_by_4() 
[Test]
public void if_it_is_divisible_by_100_but_not_by_400() 
}
}
namespace Leap_year_spec
{
public class A_year_is_a_leap_year
{
[Test]
public void if_it_is_divisible_by_4_but_not_by_100() 
[Test]
public void if_it_is_divisible_by_400() 
}
public class A_year_is_not_a_leap_year
{
[Test]
public void if_it_is_not_divisible_by_4() 
[Test]
public void if_it_is_divisible_by_100_but_not_by_400() 
}
}
namespace Leap_year_spec
{
public class A_year_is_a_leap_year
{
[Test]
public void if_it_is_divisible_by_4_but_not_by_100() 
[Test]
public void if_it_is_divisible_by_400() 
}
public class A_year_is_not_a_leap_year
{
[Test]
public void if_it_is_not_divisible_by_4() 
[Test]
public void if_it_is_divisible_by_100_but_not_by_400() 
}
}
Nat Pryce & Steve Freeman
Are your tests really driving your development?
For tests to drive development they must do
more than just test that code performs its
required functionality: they must clearly express
that required functionality to the reader.
That is, they must be clear specifications of the
required functionality.
namespace Leap_year_spec
{
public class A_year_is_a_leap_year 
public class A_year_is_not_a_leap_year 
}
namespace Leap_year_spec
{
public class A_year_is_a_leap_year 
public class A_year_is_not_a_leap_year
{
[Test]
public void if_it_is_not_divisible_by_4()
{
Assert.IsFalse(IsLeapYear(2023));
}
[Test]
public void if_it_is_divisible_by_100_but_not_by_400() 
}
}
namespace Leap_year_spec
{
public class A_year_is_a_leap_year 
public class A_year_is_not_a_leap_year
{
[Test]
public void if_it_is_not_divisible_by_4()
{
Assert.IsFalse(IsLeapYear(2023));
Assert.IsFalse(IsLeapYear(2022));
Assert.IsFalse(IsLeapYear(1999));
Assert.IsFalse(IsLeapYear(3));
}
[Test]
public void if_it_is_divisible_by_100_but_not_by_400() 
}
}
namespace Leap_year_spec
{
public class A_year_is_a_leap_year 
public class A_year_is_not_a_leap_year
{
[TestCase(2023)]
[TestCase(2022)]
[TestCase(1999)]
[TestCase(3)]
public void if_it_is_not_divisible_by_4(int year)
{
Assert.IsFalse(IsLeapYear(year));
}
[Test]
public void if_it_is_divisible_by_100_but_not_by_400() 
}
}
namespace Leap_year_spec
{
public class A_year_is_a_leap_year 
public class A_year_is_not_a_leap_year
{
[Test]
public void if_it_is_not_divisible_by_4(
[Values(2023, 2022, 1999, 3)] int year)
{
Assert.IsFalse(IsLeapYear(year));
}
[Test]
public void if_it_is_divisible_by_100_but_not_by_400() 
}
}
namespace Leap_year_spec
{
public class A_year_is_a_leap_year
{
[Test]
public void if_it_is_divisible_by_4_but_not_by_100(
[Values(2024, 2016, 1984, 4) int year) 
[Test]
public void if_it_is_divisible_by_400(
[Range(400, 4000, 400)] int year) 
}
public class A_year_is_not_a_leap_year
{
[Test]
public void if_it_is_not_divisible_by_4(
[Values(2023, 2022, 1999, 3)] int year) 
[Test]
public void if_it_is_divisible_by_100_but_not_by_400(
[Values(2100, 1900, 1800, 100)] int year) 
}
}
Simple
cases
Common
cases
Error
cases
Boundary
cases
Simple
cases
Common
cases
Error
cases
Boundary
cases ?
namespace Leap_year_spec
{
public class A_year_is_a_leap_year 
public class A_year_is_not_a_leap_year 
public class Error_cases 
}
namespace Leap_year_spec
{
public class A_year_is_a_leap_year 
public class A_year_is_not_a_leap_year 
public class A_year_is_not_supported 
}
namespace Leap_year_spec
{
public class A_year_is_a_leap_year 
public class A_year_is_not_a_leap_year 
public class A_year_is_not_supported
{
[Test]
public void if_it_is_0()
{
Assert.Throws<ArgumentOutOfRangeException>(() => IsLeapYear(0));
}
}
}
namespace Leap_year_spec
{
public class A_year_is_a_leap_year 
public class A_year_is_not_a_leap_year 
public class A_year_is_not_supported
{
[Test]
public void if_it_is_0()
{
Assert.Catch<ArgumentOutOfRangeException>(() => IsLeapYear(0));
}
}
}
namespace Leap_year_spec
{
public class A_year_is_a_leap_year 
public class A_year_is_not_a_leap_year 
public class A_year_is_not_supported
{
[Test]
public void if_it_is_0()
{
Assert.Catch<ArgumentOutOfRangeException>(() => IsLeapYear(0));
}
[Test]
public void if_it_is_negative(
[Values(-1, -4, -100, -400)] int year)
{
Assert.Catch<ArgumentOutOfRangeException>(() => IsLeapYear(year));
}
}
}
namespace Leap_year_spec
{
public class A_year_is_a_leap_year 
public class A_year_is_not_a_leap_year 
public class A_year_is_not_supported
{
[Test]
public void if_it_is_0()
{
Assert.Catch<ArgumentOutOfRangeException>(() => IsLeapYear(0));
}
[Test]
public void if_it_is_negative(
[Values(-1, -4, -100, -400, int.MinValue)] int year)
{
Assert.Catch<ArgumentOutOfRangeException>(() => IsLeapYear(year));
}
}
}
namespace Leap_year_spec
{
public class A_year_is_a_leap_year 
public class A_year_is_not_a_leap_year 
public class A_year_is_supported 
public class A_year_is_not_supported 
}
namespace Leap_year_spec
{
public class A_year_is_a_leap_year 
public class A_year_is_not_a_leap_year 
public class A_year_is_supported
{
[Test]
public void if_it_is_positive(
[Values(1, 10000, int.MaxValue)] int year)
{
Assert.DoesNotThrow(() => IsLeapYear(year));
}
}
public class A_year_is_not_supported 
}
Leo Tolstoy
Anna Karenina
All happy families are alike;
each unhappy family is
unhappy in its own way.
namespace Leap_year_spec 
public class A_year_is_a_leap_year 
public void if_it_is_divisible_by_4_but_not_by_400() 
public void if_it_is_divisible_by_400() 
public class A_year_is_not_a_leap_year 
public void if_is_not_divisible_by_4() 
public void if_it_is_divisible_by_100_but_not_by_400() 
public class A_year_is_supported 
public void if_it_is_positive() 
public class A_year_is_not_supported 
public void if_it_is_0() 
public void if_it_is_negative() 
namespace Leap_year_spec 
public class A_year_is_a_leap_year 
public void if_it_is_divisible_by_4_but_not_by_400() 
public void if_it_is_divisible_by_400() 
public class A_year_is_not_a_leap_year 
public void if_is_not_divisible_by_4() 
public void if_it_is_divisible_by_100_but_not_by_400() 
public class A_year_is_supported 
public void if_it_is_positive() 
public class A_year_is_not_supported 
public void if_it_is_0() 
public void if_it_is_negative() 
Producer Consumer
Queue
Length
Capacity
Enqueue ⮞ ⮜ Dequeue
public class Queue<T>
{
...
public Queue(int capacity) ...
public int Capacity => ...
public int Length => ...
public bool Enqueue(T toBack) ...
public bool Dequeue(out T fromFront) ...
}
Nat Pryce & Steve Freeman
Are your tests really driving your development?
Tests that are not written with their role as
specifications in mind can be very confusing to
read. The difficulty in understanding what they
are testing can greatly reduce the velocity at
which a codebase can be changed.
public class QueueTests
{
[Test]
public void TestConstructor() ...
[Test]
public void TestCapacity() ...
[Test]
public void TestLength() ...
[Test]
public void TestEnqueue() ...
[Test]
public void TestDequeue() ...
}
public class QueueTests
{
[Test]
public void Constructor() ...
[Test]
public void Capacity() ...
[Test]
public void Length() ...
[Test]
public void Enqueue() ...
[Test]
public void Dequeue() ...
}
public class QueueTests
{
[Test]
public void CanBeConstructed() ...
[Test]
public void HasCapacity() ...
[Test]
public void HasLength() ...
[Test]
public void CanBeEnqueuedOn() ...
[Test]
public void CanBeDequeuedFrom() ...
}
public class QueueTests
{
[Test]
public void CanSometimesBeConstructed() ...
[Test]
public void HasCapacity() ...
[Test]
public void HasLength() ...
[Test]
public void CanSometimesBeEnqueuedOn() ...
[Test]
public void CanSometimesBeDequeuedFrom() ...
}
85
twitter.com/venkat_s/status/1633131055635890179
namespace Queue_spec 
public class Creating_a_queue 
public void leaves_it_empty() 
public void preserves_positive_bounding_capacity() 
public void fails_with_non_positive_bounding_capacity() 
public class Enqueuing_on 
public void an_empty_queue_makes_it_longer() 
public void a_non_empty_queue_makes_it_longer() 
public void a_non_full_queue_up_to_capacity_makes_it_full() 
public void a_full_queue_is_ignored() 
public class Dequeuing_from 
public void an_empty_queue_is_ignored_with_default_value() 
public void a_non_empty_queue_gives_values_in_order_enqueued() 
public void a_full_queue_makes_it_non_full() 
namespace Queue_spec 
public class Creating_a_queue 
public void leaves_it_empty() 
public void preserves_positive_bounding_capacity() 
public void fails_with_non_positive_bounding_capacity() 
public class Enqueuing_on 
public void an_empty_queue_makes_it_longer() 
public void a_non_empty_queue_makes_it_longer() 
public void a_non_full_queue_up_to_capacity_makes_it_full() 
public void a_full_queue_is_ignored() 
public class Dequeuing_from 
public void an_empty_queue_is_ignored_with_default_value() 
public void a_non_empty_queue_gives_values_in_order_enqueued() 
public void a_full_queue_makes_it_non_full() 
public class Enqueuing_on 
public void an_empty_queue_makes_it_longer(string value)
{
var queue = new Queue<string>(2);
var enqueued = queue.Enqueue(value);
Assert.IsTrue(enqueued);
Assert.AreEqual(1, queue.Length);
}
public class Enqueuing_on 
public void an_empty_queue_makes_it_longer(string value)
{
var queue = new Queue<string>(2);
var enqueued = queue.Enqueue(value);
Assert.IsTrue(enqueued);
Assert.AreEqual(1, queue.Length);
}
Gerard Meszaros
97-things-every-x-should-know.gitbooks.io/97-things-every-programmer-should-know/content/en/thing_95
For each usage scenario, the test(s):
▪ Describe the context, starting point, or
preconditions that must be satisfied
▪ Illustrate how the software is invoked
▪ Describe the expected results or
postconditions to be verified
public class Enqueuing_on 
public void an_empty_queue_makes_it_longer(string value)
{
// Arrange:
var queue = new Queue<string>(2);
// Act:
var enqueued = queue.Enqueue(value);
// Assert:
Assert.IsTrue(enqueued);
Assert.AreEqual(1, queue.Length);
}
public class Enqueuing_on 
public void an_empty_queue_makes_it_longer(string value)
{
// Establish precondition for operation:
var queue = new Queue<string>(2);
// Perform operation of interest:
var enqueued = queue.Enqueue(value);
// Confirm postcondition of operation:
Assert.IsTrue(enqueued);
Assert.AreEqual(1, queue.Length);
}
public class Enqueuing_on 
public void an_empty_queue_makes_it_longer(string value)
{
// Given:
var queue = new Queue<string>(2);
// When:
var enqueued = queue.Enqueue(value);
// Then:
Assert.IsTrue(enqueued);
Assert.AreEqual(1, queue.Length);
}
Thinking in States
In most real-world situations, people’s
relaxed attitude to state is not an issue.
Unfortunately, however, many
programmers are quite vague about
state too — and that is a problem.
Niclas Nilsson
97-things-every-x-should-know.gitbooks.io/97-things-every-programmer-should-know/content/en/thing_84
Non-Empty
Empty
Dequeue Enqueue
Dequeue [Length > 1]
Enqueue
Dequeue [Length == 1]
new [Capacity > 0]
Non-Full
Empty
Dequeue
Enqueue
Dequeue [Length > 1]
Enqueue
Dequeue [Length == 1]
new [Capacity > 0]
Full
Non-Empty
[Length = Capacity]
[Length < Capacity]
namespace Queue_spec 
public class A_new_queue 
public void is_empty() 
public void preserves_positive_bounding_capacity() 
public void fails_with_non_positive_bounding_capacity() 
public class An_empty_queue 
public void ignores_dequeuing_with_default_value() 
public void becomes_non_empty_when_value_enqueued() 
public class A_non_empty_queue 
public class that_is_not_full 
public void becomes_longer_when_value_enqueued() 
public void becomes_full_when_enqueued_up_to_capacity() 
public class that_is_full 
public void ignores_further_enqueued_values() 
public void becomes_non_full_when_dequeued() 
public void dequeues_values_in_order_enqueued() 
namespace Queue_spec 
public class A_new_queue 
public void is_empty() 
public void preserves_positive_bounding_capacity() 
public void fails_with_non_positive_bounding_capacity() 
public class An_empty_queue 
public void ignores_dequeuing_with_default_value() 
public void becomes_non_empty_when_value_enqueued() 
public class A_non_empty_queue 
public class that_is_not_full 
public void becomes_longer_when_value_enqueued() 
public void becomes_full_when_enqueued_up_to_capacity() 
public class that_is_full 
public void ignores_further_enqueued_values() 
public void becomes_non_full_when_dequeued() 
public void dequeues_values_in_order_enqueued() 
Given
When
Then
Given can be used to group
tests for operations with
respect to common
initial state
When can be used to group
tests by operation,
differentiated by initial
state or outcome
Then can be used to group
tests by common
outcome, regardless of
initial state
I hope that’s been useful.
You’re off to do revisit some
tests?
OK, catch you later.
Kevlin Henney
“Program with GUTs”
medium.com/97-things/program-with-guts-828e69dd8e15

Program with GUTs