Programming with GUTs

Preview:

Citation preview

info@neueda.com01 498 0028

Programming with GUTsWriting Good Unit Tests

Kevlin Henneykevlin@curbralan.com

IntentClarify the practice(s) of unit testing

ContentKinds of testsTesting approachGood unit testsListening to your tests

But first...

Kinds of Tests

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.

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

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!

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

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

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"

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

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

Testing Approach

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

Test Early.Test Often.Test Automatically.

Andrew Hunt and David Thomas

The Pragmatic Programmer

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)

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"

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

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"

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

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

Less unit testing dogma.More unit testing karma.

Alberto Savoia"The Way of Testivus"

Good Unit Tests

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"

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

[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");

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

[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

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);

}

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?"

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

[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

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

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)

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:

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:

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:

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

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;

}

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

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.

It is better to be roughly right than precisely wrong.

John Maynard Keynes

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:

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

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>();

}

Functional

Operational

Developmental

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;...

};

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;...

};

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;...

};

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

Listening to Your Tests

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?"

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.

Feature

Option A Option B

Client

Option A Option B

Client

AB

C

DE

F

⊗ ⊗⊗

⊗ ⊗⊗⊗⊗⊗

⊗⊗

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

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.

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

Root CoreCode

ExternalWrapper

Wrapped

CoreCode

UsageInterface

ExternalWrapper

Root

Decoupled

Name based on implementation

Implementation name

Client

Name based on client usage

Implementation name

Client

There can be only one.

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

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

It renders you powerless.

[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;

}

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) ......

}

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) ......

}

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;

}

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;

}

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

Lucius Cary

public class Date{

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

}

public class Date{

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

}

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

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

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

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