82
[email protected] 01 498 0028

Programming with GUTs

Embed Size (px)

Citation preview

Page 1: Programming with GUTs

[email protected] 498 0028

Page 2: Programming with GUTs

Programming with GUTsWriting Good Unit Tests

Kevlin [email protected]

Page 3: Programming with GUTs
Page 4: Programming with GUTs

IntentClarify the practice(s) of unit testing

ContentKinds of testsTesting approachGood unit testsListening to your tests

But first...

Page 5: Programming with GUTs
Page 6: Programming with GUTs
Page 7: Programming with GUTs
Page 8: Programming with GUTs
Page 9: Programming with GUTs
Page 10: Programming with GUTs

Kinds of Tests

Page 11: Programming with GUTs

Testing is a way of showing that you care about something

If a particular quality has value for a system, there should be a concrete demonstration of that valueCode quality can be demonstrated through unit testing, static analysis, code reviews, analysing trends in defect and change history, etc.

Page 12: Programming with GUTs

Unit testing involves testing the individual classes and mechanisms; is the responsibility of the application engineer who implemented the structure. [...] System testing involves testing the system as a whole; is the responsibility of the quality assurance team.

Grady BoochObject-Oriented Analysis and Design with Applications, 2nd edition

Page 13: Programming with GUTs

System testing involves testing of the software as a whole product

System testing may be a distinct job or role, but not necessarily a group

Programmer testing involves testing of code by programmers

It is about code-facing tests, i.e., unit and integration testsIt is not the testing of programmers!

Page 14: Programming with GUTs

Teams do deliver successfully using manual tests, so this can't be considered a critical success factor. However, every programmer I've interviewed who once moved to automated tests swore never to work without them again. I find this nothing short of astonishing.Their reason has to do with improved quality of life. During the week, they revise sections of code knowing they can quickly check that they hadn't inadvertently broken something along the way. When they get code working on Friday, they go home knowing that they will be able on Monday to detect whether anyone had broken it over the weekend—they simply rerun the tests on Monday morning. The tests give them freedom of movement during the day and peace of mind at night.

Alistair Cockburn, Crystal Clear

Page 15: Programming with GUTs

Automated tests offer a way to reduce the waste of repetitive work

Write code that tests codeHowever, note that not all kinds of tests can be automated

Usability tests require observation and monitoring of real usageExploratory testing is necessarily a manual task

Page 16: Programming with GUTs

A test is not a unit test if:It talks to the databaseIt communicates across the networkIt touches the file systemIt can't run at the same time as any of your other unit testsYou have to do special things to your environment (such as editing config files) to run it.

Tests that do these things aren't bad. Often they are worth writing, and they can be written in a unit test harness. However, it is important to be able to separate them from true unit tests so that we can keep a set of tests that we can run fast whenever we make our changes.

Michael Feathers, "A Set of Unit Testing Rules"

Page 17: Programming with GUTs

Unit tests are on code that is isolated from external dependencies

The outcome is based on the code of the test and the code under test

Integration tests involve external dependencies

Some portion of a system is necessarily not unit testable; the portion that is not necessarily so is a measure of coupling

Page 18: Programming with GUTs

It is worth distinguishing between tests on functional behaviour...

Written in terms of asserting values and interactions in response to use, the result of which is either right or wrong

And tests on operational behaviourMore careful experimental design is needed because they typically require sampling and statistical interpretation

Page 19: Programming with GUTs

Testing Approach

Page 20: Programming with GUTs

If all you could make was a long-term argument for testing, you could forget about it. Some people would do it out of a sense of duty or because someone was watching over their shoulder. As soon as the attention wavered or the pressure increased, no new tests would get written, the tests that were written wouldn't be run, and the whole thing would fall apart.

Kent Beck

Extreme Programming Explained, 1st edition

Page 21: Programming with GUTs

Test Early.Test Often.Test Automatically.

Andrew Hunt and David Thomas

The Pragmatic Programmer

Page 22: Programming with GUTs

A passive approach looks at tests as a way of confirming code behaviour

This is Plain Ol' Unit Testing (POUTing)An active approach also uses testing to frame and review design

This is Test-Driven Development (TDD)A reactive approach introduces tests in response to defects

This is Defect-Driven Testing (DDT)

Page 23: Programming with GUTs

Very many people say "TDD" when they really mean, "I have good unit tests" ("I have GUTs"?) Ron Jeffries tried for years to explain what this was, but we never got a catch-phrase for it, and now TDD is being watered down to mean GUTs.

Alistair Cockburn"The modern programming professional has GUTs"

Page 24: Programming with GUTs

TDD complements other design, verification and validation activities

The emphasis is on defining a contract for a piece of code with behavioural examples that act as both specification and confirmationThe act of writing tests (i.e., specifying) is interleaved with the act of writing the target code, offering both qualitative and quantitative feedback

Page 25: Programming with GUTs

Because Unit Testing is the plain-Jane progenitor of Test Driven Development, it's kind of unfair that it doesn't have an acronym of its own. After all, it's hard to get programmer types to pay attention if they don't have some obscure jargon to bandy about. [...] I'll borrow from the telco folks and call unit testing Plain Old Unit Testing.

Jacob Proffitt, "TDD or POUT"

Page 26: Programming with GUTs

POUT employs testing as a code verification tool

Test writing follows code writing when the piece of target code is considered complete — effective POUTing assumes sooner rather than laterThe emphasis is quantitative and the focus is on defect discovery rather than design framing or problem clarification

Page 27: Programming with GUTs

DDT involves fixing a defect by first adding a test for the defect

DDT uses defects as an opportunity to improve coverage and embody learning from feedback in code formThis is a normal part of effective TDD and POUT practiceHowever, DDT can also be used on legacy code to grow the set of tests

Page 28: Programming with GUTs

Less unit testing dogma.More unit testing karma.

Alberto Savoia"The Way of Testivus"

Page 29: Programming with GUTs

Good Unit Tests

Page 30: Programming with GUTs

I was using xUnit. I was using mock objects. So why did my tests seem more like necessary baggage instead of this glorious, enabling approach?

In order to resolve this mismatch, I decided to look more closely at what was inspiring all the unit-testing euphoria. As I dug deeper, I found some major flaws that had been fundamentally undermining my approach to unit testing.

The first realization that jumped out at me was that my view of testing was simply too “flat.” I looked at the unit-testing landscape and saw tools and technologies. The programmer in me made unit testing more about applying and exercising frameworks. It was as though I had essentially reduced my concept of unit testing to the basic mechanics of exercising xUnit to verify the behavior of my classes. [...]

In general, my mindset had me thinking far too narrowly about what it meant to write good unit tests.

Tod Golding, "Tapping into Testing Nirvana"

Page 31: Programming with GUTs

Poor unit tests lead to poor unit-testing experiences

Effective testing requires more than mastering the syntax of an assertion

It is all too easy to end up with monolithic or ad hoc tests

Rambling stream-of-consciousness narratives intended for machine execution but not human consumption

Page 32: Programming with GUTs

[Test]public void Test(){

RecentlyUsedList list = new RecentlyUsedList();Assert.AreEqual(0, list.Count);list.Add("Aardvark");Assert.AreEqual(1, list.Count);Assert.AreEqual("Aardvark", list[0]);list.Add("Zebra");list.Add("Mongoose");Assert.AreEqual(3, list.Count);Assert.AreEqual("Mongoose", list[0]);Assert.AreEqual("Zebra", list[1]);Assert.AreEqual("Aardvark", list[2]);list.Add("Aardvark");Assert.AreEqual(3, list.Count);Assert.AreEqual("Aardvark", list[0]);Assert.AreEqual("Mongoose", list[1]);Assert.AreEqual("Zebra", list[2]);bool thrown;try{

string unreachable = list[3];thrown = false;

}catch (ArgumentOutOfRangeException){

thrown = true;}Assert.IsTrue(thrown);

}

[Test]public void Test1(){

RecentlyUsedList list = new RecentlyUsedList();Assert.AreEqual(0, list.Count);list.Add("Aardvark");Assert.AreEqual(1, list.Count);Assert.AreEqual("Aardvark", list[0]);list.Add("Zebra");list.Add("Mongoose");Assert.AreEqual(3, list.Count);Assert.AreEqual("Mongoose", list[0]);Assert.AreEqual("Zebra", list[1]);Assert.AreEqual("Aardvark", list[2]);

}[Test]public void Test2(){

RecentlyUsedList list = new RecentlyUsedList();Assert.AreEqual(0, list.Count);list.Add("Aardvark");Assert.AreEqual(1, list.Count);Assert.AreEqual("Aardvark", list[0]);list.Add("Zebra");list.Add("Mongoose");Assert.AreEqual(3, list.Count);Assert.AreEqual("Mongoose", list[0]);Assert.AreEqual("Zebra", list[1]);Assert.AreEqual("Aardvark", list[2]);list.Add("Aardvark");Assert.AreEqual(3, list.Count);Assert.AreEqual("Aardvark", list[0]);Assert.AreEqual("Mongoose", list[1]);Assert.AreEqual("Zebra", list[2]);

}[Test]public void Test3(){

RecentlyUsedList list = new RecentlyUsedList();Assert.AreEqual(0, list.Count);list Add("Aardvark");

Page 33: Programming with GUTs

A predominantly procedural test style is rarely effective

It is based on the notion that "I have a function foo, therefore I have one test function that tests foo", which does not really make sense for object usageAnd, in truth, procedural testing has never really made much sense for procedural code either

Page 34: Programming with GUTs

[Test]public void Constructor(){

RecentlyUsedList list = new RecentlyUsedList();Assert.AreEqual(0, list.Count);

}[Test]public void Add(){

RecentlyUsedList list = new RecentlyUsedList();list.Add("Aardvark");Assert.AreEqual(1, list.Count);list.Add("Zebra");list.Add("Mongoose");Assert.AreEqual(3, list.Count);list.Add("Aardvark");Assert.AreEqual(3, list.Count);

}[Test]public void Indexer(){

RecentlyUsedList list = new RecentlyUsedList();list.Add("Aardvark");list.Add("Zebra");list.Add("Mongoose");Assert.AreEqual("Mongoose", list[0]);Assert.AreEqual("Zebra", list[1]);Assert.AreEqual("Aardvark", list[2]);list.Add("Aardvark");Assert.AreEqual("Aardvark", list[0]);Assert.AreEqual("Mongoose", list[1]);Assert.AreEqual("Zebra", list[2]);bool thrown;try{

string unreachable = list[3];thrown = false;

}catch (ArgumentOutOfRangeException){

thrown = true;}Assert.IsTrue(thrown);

}

Constructor

Add

Indexer

Page 35: Programming with GUTs

void test_sort(){

int single[] = { 2 };quickersort(single, 1);assert(sorted(single, 1));

int identical[] = { 2, 2, 2 };quickersort(identical, 3);assert(sorted(identical, 3));

int ascending[] = { -1, 0, 1 };quickersort(ascending, 3);assert(sorted(ascending, 3));

int descending[] = { 1, 0, -1 };quickersort(descending, 3);assert(sorted(descending, 3));

int arbitrary[] = { 32, 12, 19, INT_MIN };quickersort(arbitrary, 4);assert(sorted(arbitrary, 4));

}

void test_sort(){

int empty[] = { 2, 1 };quickersort(empty + 1, 0);assert(empty[0] == 2);assert(empty[1] == 1);int single[] = { 3, 2, 1 };quickersort(single + 1, 1);assert(single[0] == 3);assert(single[1] == 2);assert(single[2] == 1);int identical[] = { 3, 2, 2, 2, 1 };quickersort(identical + 1, 3);assert(identical[0] == 3);assert(identical[1] == 2);assert(identical[2] == 2);assert(identical[3] == 2);assert(identical[4] == 1);int ascending[] = { 2, -1, 0, 1, -2 };quickersort(ascending + 1, 3);assert(ascending[0] == 2);assert(ascending[1] == -1);assert(ascending[2] == 0);assert(ascending[3] == 1);assert(ascending[4] == -2);int descending[] = { 2, 1, 0, -1, -2 };quickersort(descending + 1, 3);assert(descending[0] == 2);assert(descending[1] == -1);assert(descending[2] == 0);assert(descending[3] == 1);assert(descending[4] == -2);int arbitrary[] = { 100, 32, 12, 19, INT_MIN, 0 };quickersort(arbitrary + 1, 4);assert(arbitrary[0] == 100);assert(arbitrary[1] == INT_MIN);assert(arbitrary[2] == 12);assert(arbitrary[3] == 19);assert(arbitrary[4] == 32);assert(arbitrary[5] == 0);

}

Page 36: Programming with GUTs

Everybody knows that TDD stands for Test Driven Development. However, people too often concentrate on the words "Test" and "Development" and don't consider what the word "Driven" really implies. 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. 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.

Nat Pryce and Steve Freeman"Are Your Tests Really Driving Your Development?"

Page 37: Programming with GUTs

Behavioural tests are based on usage scenarios and outcomes

They are purposeful, cutting across individual operations and reflecting useTests are named in terms of requirements, emphasising intention and goals rather than mechanicsThis style correlates with the idea of use cases and user stories in the small

Page 38: Programming with GUTs

[Test]public void InitialListIsEmpty(){

RecentlyUsedList list = new RecentlyUsedList();

Assert.AreEqual(0, list.Count);}[Test]public void AdditionOfSingleItemToEmptyListIsRetained(){

RecentlyUsedList list = new RecentlyUsedList();list.Add("Aardvark");

Assert.AreEqual(1, list.Count);Assert.AreEqual("Aardvark", list[0]);

}[Test]public void AdditionOfDistinctItemsIsRetainedInStackOrder(){

RecentlyUsedList list = new RecentlyUsedList();list.Add("Aardvark");list.Add("Zebra");list.Add("Mongoose");

Assert.AreEqual(3, list.Count);Assert.AreEqual("Mongoose", list[0]);Assert.AreEqual("Zebra", list[1]);Assert.AreEqual("Aardvark", list[2]);

}[Test]public void DuplicateItemsAreMovedToFrontButNotAdded(){

RecentlyUsedList list = new RecentlyUsedList();list.Add("Aardvark");list.Add("Mongoose");list.Add("Aardvark");

Assert.AreEqual(2, list.Count);Assert.AreEqual("Aardvark", list[0]);Assert.AreEqual("Mongoose", list[1]);

}[Test, ExpectedException(ExceptionType = typeof(ArgumentOutOfRangeException))]public void OutOfRangeIndexThrowsException(){

RecentlyUsedList list = new RecentlyUsedList();list.Add("Aardvark");list.Add("Mongoose");list.Add("Aardvark");string unreachable = list[3];

}

Addition of single item to empty list is retained

Addition of distinct items is retained in stack order

Duplicate items are moved to front but not added

Out of range index throws exception

Initial list is empty

Page 39: Programming with GUTs

void require_that_sorting_nothing_does_not_overrun(){

int empty[] = { 2, 1 };quickersort(empty + 1, 0);assert(empty[0] == 2);assert(empty[1] == 1);

}void require_that_sorting_single_value_changes_nothing(){

int single[] = { 3, 2, 1 };quickersort(single + 1, 1);assert(single[0] == 3);assert(single[1] == 2);assert(single[2] == 1);

}void require_that_sorting_identical_value_changes_nothing(){

int identical[] = { 3, 2, 2, 2, 1 };quickersort(identical + 1, 3);assert(identical[0] == 3);assert(identical[1] == 2);assert(identical[2] == 2);assert(identical[3] == 2);assert(identical[4] == 1);

}void require_that_sorting_ascending_sequence_changes_nothing(){

int ascending[] = { 2, -1, 0, 1, -2 };quickersort(ascending + 1, 3);assert(ascending[0] == 2);assert(ascending[1] == -1);assert(ascending[2] == 0);assert(ascending[3] == 1);assert(ascending[4] == -2);

}void require_that_sorting_descending_sequence_reverses_it(){

int descending[] = { 2, 1, 0, -1, -2 };quickersort(descending + 1, 3);assert(descending[0] == 2);assert(descending[1] == -1);assert(descending[2] == 0);assert(descending[3] == 1);assert(descending[4] == -2);

}void require_that_sorting_mixed_sequence_orders_it(){

int arbitrary[] = { 100, 32, 12, 19, INT_MIN, 0 };quickersort(arbitrary + 1, 4);assert(arbitrary[0] == 100);assert(arbitrary[1] == INT_MIN);assert(arbitrary[2] == 12);assert(arbitrary[3] == 19);assert(arbitrary[4] == 32);assert(arbitrary[5] == 0);

}

Require that sorting nothing does not overrun

Require that sorting single value changes nothing

Require that sorting identical values changes nothing

Require that sorting ascending sequence changes nothing

Require that sorting descending sequence reverses it

Require that sorting mixed sequence orders it

Page 40: Programming with GUTs

testIsLeapYear

testNonLeapYearstestLeapYears

yearsNotDivisibleBy4AreNotLeapYearsyearsDivisibleBy4ButNotBy100AreLeapYearsyearsDivisibleBy100ButNotBy400AreNotLeapYearsyearsDivisibleBy400AreLeapYears

Procedural test structured in terms of the function being tested, but not in terms of its functionality:

Tests partitioned in terms of the result of the function being tested:

Behavioural tests reflecting requirements and partitioned in terms of the problem domain:

Increasingly expressive and

sustainable

public static boolean isLeapYear(int year)

Page 41: Programming with GUTs

testYearsNotDivisibleBy4testYearsDivisibleBy4testYearsDivisibleBy100ButNotBy400testYearsDivisibleBy400

It is common to name the characteristic or scenario being tested, but unless the outcome is also described it is not obvious to see what the requirement is:

Page 42: Programming with GUTs

yearsNotDivisibleBy4ShouldNotBeLeapYearsyearsDivisibleBy4ButNotBy100ShouldBeLeapYearsyearsDivisibleBy100ButNotBy400ShouldNotBeLeapYearsyearsDivisibleBy400ShouldBeLeapYears

A standard BDD practice is to use the word should in describing a behaviour. This ritual word can sometimes be helpful if requirement-based naming is not the norm, but be aware that it can sometimes look indecisive and uncommitted:

Page 43: Programming with GUTs

testThatYearsNotDivisibleBy4AreNotLeapYearstestThatYearsDivisibleBy4ButNotByAreLeapYearstestThatYearsDivisibleBy100ButNotBy400AreNotLeapYearstestThatYearsDivisibleBy400AreLeapYears

If using JUnit 3, or any other framework that expects a test prefix, consider prefixing with testThat as, grammatically, it forces a complete sentence:

Page 44: Programming with GUTs

Example-based test cases are easy to read and simple to write

They have a linear narrative with a low cyclomatic complexity, and are therefore less tedious and error proneCoverage of critical cases is explicitAdditional value coverage can be obtained using complementary and more exhaustive data-driven tests

Page 45: Programming with GUTs

public class CalculatorShellTest extends TestCase{

public void testAdditionBasedOperations(){

assertEquals("= 15", run("6\n9\nplus\n"));assertEquals("= 15", run("6\n9\n+\n"));assertEquals("= 10", run("27\n17\nminus\n"));assertEquals("= 10", run("27\n17\n-\n"));assertEquals("= 12", run("3\n4\nmultiply\n"));assertEquals("= 12", run("3\n4\n*\n"));assertEquals("= -4", run("4\nnegate\n"));assertEquals("= 16", run("4\nsquare\n"));assertEquals("= 8", run("7\n++\n"));assertEquals("= 6", run("7\n--\n"));

}public void testDivisionBasedOperations(){

assertEquals("= 1", run("3\n3\ndiv\n"));assertEquals("= 1", run("4\n3\ndiv\n"));assertEquals("= 0", run("3\n4\ndiv\n"));assertEquals("= 0", run("3\n0\ndiv\n"));assertEquals("= 2", run("6\n3\n/\n"));assertEquals("= 0", run("3\n3\nmod\n"));assertEquals("= 1", run("4\n3\nmod\n"));assertEquals("= 3", run("3\n4\nmod\n"));assertEquals("= 0", run("3\n0\nmod\n"));assertEquals("= 1", run("7\n3\n%\n"));assertEquals("= 1", run("7\n3\nmodulo\n"));

}public void testComparisonOperation(){

assertEquals("= 1", run("5\n3\ncompare\n"));assertEquals("= 0", run("3\n3\ncompare\n"));assertEquals("= -1", run("3\n5\ncompare\n"));

}public void testStackControlOperations(){

assertEquals("= 27 27", run("27\ndup\n"));assertEquals("= 17", run("17\n27\ndrop\n"));assertEquals("=", run("9\n6\n17\n27\nclear\n"));assertEquals("=", run("clear\n"));assertEquals("= 17 27", run("17\n27\nswap\n"));

}public void testStacking(){

assertEquals("=", run("\n"));assertEquals("= 4 3 2 1", run("1\n2\n3\n4\n\n"));assertEquals("= 3 4 2 1", run("1\n2\n3\n4\nswap\n"));assertEquals("= 7 2 1", run("1\n2\n3\n4\nplus\n"));

}public void testSpaceSeparation(){

assertEquals("= 4 3 2 1", run("1 2 3 4\n\n"));assertEquals("= 4 3 2 1", run(" 1 2 3\t4 \n\n"));assertEquals("= 3 4 2 1", run("1 2 3 4 swap\n"));assertEquals("= 3 4 2 1", run("1 2 3\n4 swap \n"));assertEquals("", run("1 2 3 exit \n"));

}public void testStackUnderflow(){

assertEquals("Stack underflow", run("drop\n"));assertEquals("Stack underflow", run("dup\n"));assertEquals("Stack underflow", run("negate\n"));assertEquals("Stack underflow", run("1\nswap\n"));assertEquals("Stack underflow", run("1\nplus\n"));

}public void testUnknownCommands(){

assertEquals("Unknown command", run("drip\n"));assertEquals("Unknown command", run("1\n2\ndroop\n"));

}public void testExit(){

assertEquals("", run("exit\n"));assertEquals("", run("6\n9\nexit\n"));assertEquals("", run("6\n9\nexit\n17\n"));assertEquals("", run(""));assertEquals("", run("6\n9\n"));

}private static String run(String input){

Reader source = new StringReader(input);Writer sink = new StringWriter();CalculatorShell shell = new CalculatorShell(source, sink);

try{

shell.run();}catch(IOException caught){

throw new RuntimeException(caught);}

return sink.toString().trim();}

}

public class CalculatorShell{

public CalculatorShell(Reader input, Writer output){

this.input = new Scanner(input);this.output = output;

commands.put("dup", "dup");commands.put("drop", "drop");commands.put("clear", "clear");commands.put("swap", "swap");commands.put("plus", "plus");commands.put("+", "plus");commands.put("minus", "minus");commands.put("-", "minus");commands.put("multiply", "multiply");commands.put("*", "multiply");commands.put("div", "div");commands.put("/", "div");commands.put("mod", "mod");commands.put("modulo", "mod");commands.put("%", "mod");commands.put("negate", "negate");commands.put("square", "square");commands.put("++", "increment");commands.put("--", "decrement");commands.put("compare", "compare");

}public void run() throws IOException{

try{

while(input.hasNextLine()){

String line = input.nextLine();if(line.length() == 0){

showStack();}else{

for(String token : line.split("[ \t]+"))if(token.length() != 0)

interpretToken(token);}

}}catch(Exit ignored){}

}private void interpretToken(String token) throws IOException{

try{

calculator.push(Integer.parseInt(token));}catch(NumberFormatException ignored){

if(token.equals("exit")){

throw new Exit();}else{

try{

executeCommand(commands.get(token));}catch(StackCalculator.UnderflowException caught){

output.write("Stack underflow\n");}catch(UnknownCommand caught){

output.write("Unknown command\n");}

}}

}private void executeCommand(String methodName) throws IOException{

if(methodName != null){

try{

calculator.getClass().getMethod(methodName).invoke(calculator);showStack();

}catch(InvocationTargetException caught){

throw (StackCalculator.UnderflowException) caught.getCause();}catch(IllegalAccessException caught){

throw new UnknownCommand();}catch(NoSuchMethodException caught){

throw new UnknownCommand();}

}else{

throw new UnknownCommand();}

}private void showStack() throws IOException{

String stack = "=";for(int index = 0; index != calculator.depth(); ++index)

stack += " " + calculator.get(index);output.write(stack + "\n");

}private StackCalculator calculator = new StackCalculator();

private Map<String, String> commands = new HashMap<String, String>();private Scanner input;private Writer output;

}

Page 46: Programming with GUTs

There are a number of things that should appear in tests

Simple cases, because you have to start somewhereCommon cases, using equivalence partitioning to identify representativesBoundary cases, testing at and aroundContractual error cases, i.e., test rainy-day as well as happy-day scenarios

Page 47: Programming with GUTs

There are a number of things that should not appear in tests

Do not assert on behaviours that are standard for the platform or language — the tests should be on your codeDo not assert on implementation specifics — a comparison may return 1but test for > 0 — or incidental presentation — spacing, spelling, etc.

Page 48: Programming with GUTs

It is better to be roughly right than precisely wrong.

John Maynard Keynes

Page 49: Programming with GUTs

assertEquals(-1, lower.compareTo(higher));assertEquals(0, value.compareTo(value));assertEquals(+1, higher.compareTo(lower));

assertNegative(lower.compareTo(higher));assertZero(value.compareTo(value));assertPositive(higher.compareTo(lower));

The following is overcommitted and unnecessarily specific, hardwiring incidental implementation details as requirements:

The following reflects the actual requirement, also making the intent clearer by introducing specifically named custom assertions:

Page 50: Programming with GUTs

Refactoring (noun): a change made to the internal structure of software to make it easier to understand and cheaper to modify without changing its observable behavior.

Refactor (verb): to restructure software by applying a series of refactorings without changing the observable behavior of the software.

Martin Fowler, Refactoring

Page 51: Programming with GUTs

public class RecentlyUsedList{

public RecentlyUsedList(){

list = new List<string>();}public void Add(string newItem){

if (list.Contains(newItem)){

int position = list.IndexOf(newItem);string existingItem = list[position];list.RemoveAt(position);list.Insert(0, existingItem);

}else{

list.Insert(0, newItem);}

}public int Count{

get{

int size = list.Count;return size;

}}public string this[int index]{

get{

int position = 0;foreach (string value in list){

if (position == index)return value;

++position;}throw new ArgumentOutOfRangeException();

}}private List<string> list;

}

public class RecentlyUsedList{

public void Add(string newItem){

list.Remove(newItem);list.Add(newItem);

}public int Count{

get{

return list.Count;}

}public string this[int index]{

get{

return list[Count - index - 1];}

}private List<string> list = new List<string>();

}

Page 52: Programming with GUTs

Functional

Operational

Developmental

Page 53: Programming with GUTs

class access_control{public:

bool is_locked(const std::basic_string<char> &key) const{

std::list<std::basic_string<char> >::const_iterator found = std::find(locked.begin(), locked.end(), key);return found != locked.end();

}bool lock(const std::basic_string<char> &key){

std::list<std::basic_string<char> >::iterator found = std::find(locked.begin(), locked.end(), key);if(found == locked.end()){

locked.insert(locked.end(), key);return true;

}return false;

}bool unlock(const std::basic_string<char> &key){

std::list<std::basic_string<char> >::iterator found = std::find(locked.begin(), locked.end(), key);if(found != locked.end()){

locked.erase(found);return true;

}return false;

}...

private:std::list<std::basic_string<char> > locked;...

};

Page 54: Programming with GUTs

class access_control{public:

bool is_locked(const std::string &key) const{

return std::count(locked.begin(), locked.end(), key) != 0;}bool lock(const std::string &key){

if(is_locked(key)){

return false;}else{

locked.push_back(key);return true;

}}bool unlock(const std::string &key){

const std::size_t old_size = locked.size();locked.remove(key);return locked.size() != old_size;

}...

private:std::list<std::string> locked;...

};

Page 55: Programming with GUTs

class access_control{public:

bool is_locked(const std::string &key) const{

return locked.count(key) != 0;}bool lock(const std::string &key){

return locked.insert(key).second;}bool unlock(const std::string &key){

return locked.erase(key);}...

private:std::set<std::string> locked;...

};

Page 56: Programming with GUTs

Ideally, unit tests are black-box testsThey should focus on interface contractThey should not touch object internals or assume control structure in functions

White-box tests can end up testing that the code does what it does

They sometimes end up silencing opportunities for refactoring, as well as undermine attempts at refactoring

Page 57: Programming with GUTs

Listening to Your Tests

Page 58: Programming with GUTs

Tests and testability offer many forms of feedback on code quality

But you need to take care to listen (not just hear) and understand what it means and how to respond to itDon't solve symptoms — "Unit testing is boring/difficult/impossible, therefore don't do unit testing" — identify and resolve root causes — "Why is unit testing boring/difficult/impossible?"

Page 59: Programming with GUTs

Dependency inversion allows a design's dependencies to be reversed, loosened and manipulated at will, which means that dependencies can be aligned with known or anticipated stability.

• •

• •••

••

Consider a number of possible change scenarios and mark affected components.

Page 60: Programming with GUTs

Feature

Option A Option B

Client

Option A Option B

Client

Page 61: Programming with GUTs

AB

C

DE

F

⊗ ⊗⊗

⊗ ⊗⊗⊗⊗⊗

⊗⊗

Page 62: Programming with GUTs

Unit testability is a property of loosely coupled code

Inability to unit test a significant portion of the code base is an indication of dependency problemsIf only a small portion of the code is unit testable, it is likely that unit testing will not appear to be pulling its weight in contrast to, say, integration testing

Page 63: Programming with GUTs

InfrastructurePlumbing and service foundations introduced in root layer of the hierarchy.

ServicesServices adapted and extended appropriately for use by the domain classes.

DomainApplication domain concepts modelled and represented with respect to extension of root infrastructure.

Page 64: Programming with GUTs

InfrastructurePlumbing and service foundations for use as self-contained plug-ins.

ServicesServices adapted appropriately for use by the domain classes.

DomainApplication domain concepts modelled and represented with respect to plug-in services.

concept

realisation

Page 65: Programming with GUTs

Root CoreCode

ExternalWrapper

Wrapped

CoreCode

UsageInterface

ExternalWrapper

Root

Decoupled

Page 66: Programming with GUTs

Name based on implementation

Implementation name

Client

Name based on client usage

Implementation name

Client

Page 67: Programming with GUTs

There can be only one.

Page 68: Programming with GUTs

Unit testing also offers feedback on code cohesion, and vice versa

Overly complex behavioural objects that are essentially large slabs of procedural codeAnaemic, underachieving objects that are plain ol' data structures in disguiseObjects that are incompletely constructed and in need of validation

Page 69: Programming with GUTs

Don't ever invite a vampire into your house, you silly boy.

It renders you powerless.

Page 70: Programming with GUTs

[Test]public void AdditionOfSingleItemToEmptyListIsRetained(){

RecentlyUsedList list = new RecentlyUsedList();list.List = new List<string>();list.Add("Aardvark");

Assert.AreEqual(1, list.List.Count);Assert.AreEqual("Aardvark", list.List[0]);

}[Test]public void AdditionOfDistinctItemsIsRetainedInStackOrder(){

RecentlyUsedList list = new RecentlyUsedList();list.List = new List<string>();list.Add("Aardvark");list.Add("Zebra");list.Add("Mongoose");

Assert.AreEqual(3, list.List.Count);Assert.AreEqual("Mongoose", list.List[0]);Assert.AreEqual("Zebra", list.List[1]);Assert.AreEqual("Aardvark", list.List[2]);

}[Test]public void DuplicateItemsAreMovedToFrontButNotAdded(){

RecentlyUsedList list = new RecentlyUsedList();list.List = new List<string>();list.Add("Aardvark");list.Add("Mongoose");list.Add("Aardvark");

Assert.AreEqual(2, list.List.Count);Assert.AreEqual("Aardvark", list.List[0]);Assert.AreEqual("Mongoose", list.List[1]);

}

public class RecentlyUsedList{

public void Add(string newItem){

list.Remove(newItem);list.Insert(0, newItem);

}public List<string> List{

get{

return list;}set{

list = value;}

}private List<string> list;

}

Page 71: Programming with GUTs
Page 72: Programming with GUTs

public class Date{

...public int getYear() ...public int getMonth() ...public int getDayInMonth() ...public void setYear(int newYear) ...public void setMonth(int newMonth) ...public void setDayInMonth(int newDayInMonth) ......

}

Page 73: Programming with GUTs

public class Date{

...public int getYear() ...public int getMonth() ...public int getWeekInYear() ...public int getDayInYear() ...public int getDayInMonth() ...public int getDayInWeek() ...public void setYear(int newYear) ...public void setMonth(int newMonth) ...public void setWeekInYear(int newWeek) ...public void setDayInYear(int newDayInYear) ...public void setDayInMonth(int newDayInMonth) ...public void setDayInWeek(int newDayInWeek) ......

}

Page 74: Programming with GUTs

public class Date{

...public int getYear() ...public int getMonth() ...public int getWeekInYear() ...public int getDayInYear() ...public int getDayInMonth() ...public int getDayInWeek() ...public void setYear(int newYear) ...public void setMonth(int newMonth) ...public void setWeekInYear(int newWeek) ...public void setDayInYear(int newDayInYear) ...public void setDayInMonth(int newDayInMonth) ...public void setDayInWeek(int newDayInWeek) ......private int year, month, dayInMonth;

}

Page 75: Programming with GUTs

public class Date{

...public int getYear() ...public int getMonth() ...public int getWeekInYear() ...public int getDayInYear() ...public int getDayInMonth() ...public int getDayInWeek() ...public void setYear(int newYear) ...public void setMonth(int newMonth) ...public void setWeekInYear(int newWeek) ...public void setDayInYear(int newDayInYear) ...public void setDayInMonth(int newDayInMonth) ...public void setDayInWeek(int newDayInWeek) ......private int daysSinceEpoch;

}

Page 76: Programming with GUTs

When it is not necessary to change, it is necessary not to change.

Lucius Cary

Page 77: Programming with GUTs

public class Date{

...public int getYear() ...public int getMonth() ...public int getWeekInYear() ...public int getDayInYear() ...public int getDayInMonth() ...public int getDayInWeek() ......

}

Page 78: Programming with GUTs

public class Date{

...public int year() ...public int month() ...public int weekInYear() ...public int dayInYear() ...public int dayInMonth() ...public int dayInWeek() ......

}

Page 79: Programming with GUTs

Be careful with coverage metricsLow coverage is always a warning signOtherwise, understanding what kind of coverage is being discussed is key — is coverage according to statement, condition, path or value?

And be careful with coverage mythsWithout a context of execution, plain assertions in code offer no coverage

Page 80: Programming with GUTs

Watch out for misattributionIf you have GUTs, but you fail to meet system requirements, the problem lies with system testing and managing requirements, not with unit testingIf you don't know what to test, then how do you know what to code? Not knowing how to test is a separate issue addressed by learning and practice

Page 81: Programming with GUTs

Don't shoot the messengerIf the bulk of testing occurs late in development, it is likely that unit testing will be difficult and will offer a poor ROIIf TDD or pre-check-in POUTing is adopted for new code, but fewer defects are logged than with late testing on the old code, that is (1) good news and (2) unsurprising

Page 82: Programming with GUTs

If practice is unprincipled there is no coordination and there is discord. When it is principled, there is balance, harmony and union. Perhaps all life aspires to the condition of music.

Aubrey Meyer