Presented at .NET South West (2024-03-26)
https://www.meetup.com/dotnetsouthwest/events/299766807/
One of the greatest shifts in modern programming practices has been how programmers across many different domains, languages and environments have embraced unit testing. Good unit testing, however, is more than waving NUnit at your C# source. Tests help to make long-term product development cost effective rather than a cost centre, they underpin the effective flow of CI/CD and reduce failure demand on a team.
But the discussion of unit testing goes further than simply writing tests: what makes a good unit test? It is not enough to have tests; poor quality tests can hold back development just as good tests can streamline it. This session provides a perspective on what good unit tests (GUTs) can look like with a couple of examples.
4. So you’re writing unit tests?
Great!
Are they any good?
Kevlin Henney
“Program with GUTs”
medium.com/97-things/program-with-guts-828e69dd8e15
5. Do you have GUTs?
Kevlin Henney
“Program with GUTs”
medium.com/97-things/program-with-guts-828e69dd8e15
6. 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”
8. 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
9. What do I mean by good?
Kevlin Henney
“Program with GUTs”
medium.com/97-things/program-with-guts-828e69dd8e15
10. We think in generalities,
but we live in detail.
Alfred North Whitehead
29. 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
30. [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()
48. 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
49. 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
50. 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
51. 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
52. 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()
}
}
53. 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()
}
}
54. 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()
}
}
55. 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.
57. 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()
}
}
58. 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()
}
}
59. 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()
}
}
60. 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()
}
}
61. 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)
}
}
66. 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));
}
}
}
67. 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));
}
}
}
68. 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));
}
}
}
69. 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));
}
}
}
70. 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
}
71. 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
}
73. 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()
74. 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()
77. 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) ...
}
78. 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.
79. public class QueueTests
{
[Test]
public void TestConstructor() ...
[Test]
public void TestCapacity() ...
[Test]
public void TestLength() ...
[Test]
public void TestEnqueue() ...
[Test]
public void TestDequeue() ...
}
80. public class QueueTests
{
[Test]
public void Constructor() ...
[Test]
public void Capacity() ...
[Test]
public void Length() ...
[Test]
public void Enqueue() ...
[Test]
public void Dequeue() ...
}
81. public class QueueTests
{
[Test]
public void CanBeConstructed() ...
[Test]
public void HasCapacity() ...
[Test]
public void HasLength() ...
[Test]
public void CanBeEnqueuedOn() ...
[Test]
public void CanBeDequeuedFrom() ...
}
82. public class QueueTests
{
[Test]
public void CanSometimesBeConstructed() ...
[Test]
public void HasCapacity() ...
[Test]
public void HasLength() ...
[Test]
public void CanSometimesBeEnqueuedOn() ...
[Test]
public void CanSometimesBeDequeuedFrom() ...
}
84. 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()
85. 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()
86. 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);
}
87. 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);
}
89. 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);
}
90. 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);
}
91. 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);
}
92. 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
95. 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()
96. 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()
98. Given can be used to group
tests for operations with
respect to common
initial state
99. When can be used to group
tests by operation,
differentiated by initial
state or outcome
100. Then can be used to group
tests by common
outcome, regardless of
initial state
101. 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