Programming with GUTs
Upcoming SlideShare
Loading in...5
×
 

Programming with GUTs

on

  • 2,139 views

 

Statistics

Views

Total Views
2,139
Views on SlideShare
2,121
Embed Views
18

Actions

Likes
1
Downloads
15
Comments
0

3 Embeds 18

http://blog.itcork.ie 15
http://static.slidesharecdn.com 2
http://www.slideshare.net 1

Accessibility

Categories

Upload Details

Uploaded via as Adobe PDF

Usage Rights

© All Rights Reserved

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
    Processing…
Post Comment
Edit your comment

Programming with GUTs Programming with GUTs Presentation Transcript

  • info@neueda.com 01 498 0028
  • Programming with GUTs Writing Good Unit Tests Kevlin Henney kevlin@curbralan.com
  • Intent Clarify the practice(s) of unit testing Content Kinds of tests Testing approach Good unit tests Listening 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 value Code 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 Booch Object-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 tests It 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 code However, note that not all kinds of tests can be automated Usability tests require observation and monitoring of real usage Exploratory testing is necessarily a manual task
  • A test is not a unit test if: It talks to the database It communicates across the network It touches the file system It can't run at the same time as any of your other unit tests You 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, quot;A Set of Unit Testing Rulesquot;
  • 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 behaviour More 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 quot;TDDquot; when they really mean, quot;I have good unit testsquot; (quot;I have GUTsquot;?) 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 quot;The modern programming professional has GUTsquot;
  • 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 confirmation The 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, quot;TDD or POUTquot;
  • 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 later The 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 form This is a normal part of effective TDD and POUT practice However, DDT can also be used on legacy code to grow the set of tests
  • Less unit testing dogma. More unit testing karma. Alberto Savoia quot;The Way of Testivusquot;
  • 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, quot;Tapping into Testing Nirvanaquot;
  • 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] [Test] public void Test() public void Test1() { { RecentlyUsedList list = new RecentlyUsedList(); RecentlyUsedList list = new RecentlyUsedList(); Assert.AreEqual(0, list.Count); Assert.AreEqual(0, list.Count); list.Add(quot;Aardvarkquot;); list.Add(quot;Aardvarkquot;); Assert.AreEqual(1, list.Count); Assert.AreEqual(1, list.Count); Assert.AreEqual(quot;Aardvarkquot;, list[0]); Assert.AreEqual(quot;Aardvarkquot;, list[0]); list.Add(quot;Zebraquot;); list.Add(quot;Zebraquot;); list.Add(quot;Mongoosequot;); list.Add(quot;Mongoosequot;); Assert.AreEqual(3, list.Count); Assert.AreEqual(3, list.Count); Assert.AreEqual(quot;Mongoosequot;, list[0]); Assert.AreEqual(quot;Mongoosequot;, list[0]); Assert.AreEqual(quot;Zebraquot;, list[1]); Assert.AreEqual(quot;Zebraquot;, list[1]); Assert.AreEqual(quot;Aardvarkquot;, list[2]); Assert.AreEqual(quot;Aardvarkquot;, list[2]); list.Add(quot;Aardvarkquot;); } Assert.AreEqual(3, list.Count); [Test] Assert.AreEqual(quot;Aardvarkquot;, list[0]); public void Test2() Assert.AreEqual(quot;Mongoosequot;, list[1]); { Assert.AreEqual(quot;Zebraquot;, list[2]); RecentlyUsedList list = new RecentlyUsedList(); bool thrown; Assert.AreEqual(0, list.Count); try list.Add(quot;Aardvarkquot;); { Assert.AreEqual(1, list.Count); string unreachable = list[3]; Assert.AreEqual(quot;Aardvarkquot;, list[0]); thrown = false; list.Add(quot;Zebraquot;); } list.Add(quot;Mongoosequot;); catch (ArgumentOutOfRangeException) Assert.AreEqual(3, list.Count); { Assert.AreEqual(quot;Mongoosequot;, list[0]); thrown = true; Assert.AreEqual(quot;Zebraquot;, list[1]); } Assert.AreEqual(quot;Aardvarkquot;, list[2]); Assert.IsTrue(thrown); list.Add(quot;Aardvarkquot;); } Assert.AreEqual(3, list.Count); Assert.AreEqual(quot;Aardvarkquot;, list[0]); Assert.AreEqual(quot;Mongoosequot;, list[1]); Assert.AreEqual(quot;Zebraquot;, list[2]); } [Test] public void Test3() { RecentlyUsedList list = new RecentlyUsedList(); Assert.AreEqual(0, list.Count);
  • A predominantly procedural test style is rarely effective It is based on the notion that quot;I have a function foo, therefore I have one test function that tests fooquot;, which does not really make sense for object usage And, in truth, procedural testing has never really made much sense for procedural code either
  • [Test] public void Constructor() Constructor { RecentlyUsedList list = new RecentlyUsedList(); Assert.AreEqual(0, list.Count); } [Test] public void Add() { Add RecentlyUsedList list = new RecentlyUsedList(); list.Add(quot;Aardvarkquot;); Assert.AreEqual(1, list.Count); list.Add(quot;Zebraquot;); list.Add(quot;Mongoosequot;); Assert.AreEqual(3, list.Count); list.Add(quot;Aardvarkquot;); Assert.AreEqual(3, list.Count); } [Test] public void Indexer() { Indexer RecentlyUsedList list = new RecentlyUsedList(); list.Add(quot;Aardvarkquot;); list.Add(quot;Zebraquot;); list.Add(quot;Mongoosequot;); Assert.AreEqual(quot;Mongoosequot;, list[0]); Assert.AreEqual(quot;Zebraquot;, list[1]); Assert.AreEqual(quot;Aardvarkquot;, list[2]); list.Add(quot;Aardvarkquot;); Assert.AreEqual(quot;Aardvarkquot;, list[0]); Assert.AreEqual(quot;Mongoosequot;, list[1]); Assert.AreEqual(quot;Zebraquot;, list[2]); bool thrown; try { string unreachable = list[3]; thrown = false; } catch (ArgumentOutOfRangeException) { thrown = true; } Assert.IsTrue(thrown); }
  • void test_sort() void test_sort() { { int empty[] = { 2, 1 }; int single[] = { 2 }; quickersort(empty + 1, 0); quickersort(single, 1); assert(empty[0] == 2); assert(sorted(single, 1)); assert(empty[1] == 1); int single[] = { 3, 2, 1 }; int identical[] = { 2, 2, 2 }; quickersort(single + 1, 1); quickersort(identical, 3); assert(single[0] == 3); assert(sorted(identical, 3)); assert(single[1] == 2); assert(single[2] == 1); int ascending[] = { -1, 0, 1 }; int identical[] = { 3, 2, 2, 2, 1 }; quickersort(ascending, 3); quickersort(identical + 1, 3); assert(sorted(ascending, 3)); assert(identical[0] == 3); assert(identical[1] == 2); int descending[] = { 1, 0, -1 }; assert(identical[2] == 2); quickersort(descending, 3); assert(identical[3] == 2); assert(sorted(descending, 3)); assert(identical[4] == 1); int ascending[] = { 2, -1, 0, 1, -2 }; int arbitrary[] = { 32, 12, 19, INT_MIN }; quickersort(ascending + 1, 3); quickersort(arbitrary, 4); assert(ascending[0] == 2); assert(sorted(arbitrary, 4)); 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 quot;Testquot; and quot;Developmentquot; and don't consider what the word quot;Drivenquot; 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 quot;Are Your Tests Really Driving Your Development?quot;
  • Behavioural tests are based on usage scenarios and outcomes They are purposeful, cutting across individual operations and reflecting use Tests are named in terms of requirements, emphasising intention and goals rather than mechanics This style correlates with the idea of use cases and user stories in the small
  • [Test] public void InitialListIsEmpty() { Initial list is empty RecentlyUsedList list = new RecentlyUsedList(); Assert.AreEqual(0, list.Count); } [Test] public void AdditionOfSingleItemToEmptyListIsRetained() Addition of single item to { RecentlyUsedList list = new RecentlyUsedList(); empty list is retained list.Add(quot;Aardvarkquot;); Assert.AreEqual(1, list.Count); Assert.AreEqual(quot;Aardvarkquot;, list[0]); } [Test] public void AdditionOfDistinctItemsIsRetainedInStackOrder() Addition of distinct items is { RecentlyUsedList list = new RecentlyUsedList(); retained in stack order list.Add(quot;Aardvarkquot;); list.Add(quot;Zebraquot;); list.Add(quot;Mongoosequot;); Assert.AreEqual(3, list.Count); Assert.AreEqual(quot;Mongoosequot;, list[0]); Assert.AreEqual(quot;Zebraquot;, list[1]); Assert.AreEqual(quot;Aardvarkquot;, list[2]); } [Test] Duplicate items are moved to public void DuplicateItemsAreMovedToFrontButNotAdded() { RecentlyUsedList list = new RecentlyUsedList(); front but not added list.Add(quot;Aardvarkquot;); list.Add(quot;Mongoosequot;); list.Add(quot;Aardvarkquot;); Assert.AreEqual(2, list.Count); Assert.AreEqual(quot;Aardvarkquot;, list[0]); Assert.AreEqual(quot;Mongoosequot;, list[1]); } [Test, ExpectedException(ExceptionType = typeof(ArgumentOutOfRangeException))] Out of range index throws public void OutOfRangeIndexThrowsException() { RecentlyUsedList list = new RecentlyUsedList(); exception list.Add(quot;Aardvarkquot;); list.Add(quot;Mongoosequot;); list.Add(quot;Aardvarkquot;); string unreachable = list[3]; }
  • void require_that_sorting_nothing_does_not_overrun() { int empty[] = { 2, 1 }; Require that sorting nothing does not overrun 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 }; Require that sorting single value changes nothing 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 }; Require that sorting identical values changes nothing 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 }; Require that sorting ascending sequence changes nothing 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() { 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() { 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); }
  • public static boolean isLeapYear(int year) Procedural test structured in terms of the function being tested, but not in terms of its functionality: testIsLeapYear Tests partitioned in terms of the result of the function being tested: testNonLeapYears testLeapYears Behavioural tests reflecting requirements and partitioned in terms of the problem domain: yearsNotDivisibleBy4AreNotLeapYears Increasingly yearsDivisibleBy4ButNotBy100AreLeapYears expressive and yearsDivisibleBy100ButNotBy400AreNotLeapYears sustainable yearsDivisibleBy400AreLeapYears
  • 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: testYearsNotDivisibleBy4 testYearsDivisibleBy4 testYearsDivisibleBy100ButNotBy400 testYearsDivisibleBy400
  • 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: yearsNotDivisibleBy4ShouldNotBeLeapYears yearsDivisibleBy4ButNotBy100ShouldBeLeapYears yearsDivisibleBy100ButNotBy400ShouldNotBeLeapYears yearsDivisibleBy400ShouldBeLeapYears
  • If using JUnit 3, or any other framework that expects a test prefix, consider prefixing with testThat as, grammatically, it forces a complete sentence: testThatYearsNotDivisibleBy4AreNotLeapYears testThatYearsDivisibleBy4ButNotByAreLeapYears testThatYearsDivisibleBy100ButNotBy400AreNotLeapYears testThatYearsDivisibleBy400AreLeapYears
  • 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 prone Coverage of critical cases is explicit Additional value coverage can be obtained using complementary and more exhaustive data-driven tests
  • public class CalculatorShellTest extends TestCase public class CalculatorShell { { public void testAdditionBasedOperations() public CalculatorShell(Reader input, Writer output) { { assertEquals(quot;= 15quot;, run(quot;6n9nplusnquot;)); this.input = new Scanner(input); assertEquals(quot;= 15quot;, run(quot;6n9n+nquot;)); this.output = output; assertEquals(quot;= 10quot;, run(quot;27n17nminusnquot;)); assertEquals(quot;= 10quot;, run(quot;27n17n-nquot;)); commands.put(quot;dupquot;, quot;dupquot;); assertEquals(quot;= 12quot;, run(quot;3n4nmultiplynquot;)); commands.put(quot;dropquot;, quot;dropquot;); assertEquals(quot;= 12quot;, run(quot;3n4n*nquot;)); commands.put(quot;clearquot;, quot;clearquot;); assertEquals(quot;= -4quot;, run(quot;4nnegatenquot;)); commands.put(quot;swapquot;, quot;swapquot;); assertEquals(quot;= 16quot;, run(quot;4nsquarenquot;)); commands.put(quot;plusquot;, quot;plusquot;); assertEquals(quot;= 8quot;, run(quot;7n++nquot;)); commands.put(quot;+quot;, quot;plusquot;); assertEquals(quot;= 6quot;, run(quot;7n--nquot;)); commands.put(quot;minusquot;, quot;minusquot;); } commands.put(quot;-quot;, quot;minusquot;); public void testDivisionBasedOperations() commands.put(quot;multiplyquot;, quot;multiplyquot;); { commands.put(quot;*quot;, quot;multiplyquot;); assertEquals(quot;= 1quot;, run(quot;3n3ndivnquot;)); commands.put(quot;divquot;, quot;divquot;); assertEquals(quot;= 1quot;, run(quot;4n3ndivnquot;)); commands.put(quot;/quot;, quot;divquot;); assertEquals(quot;= 0quot;, run(quot;3n4ndivnquot;)); commands.put(quot;modquot;, quot;modquot;); assertEquals(quot;= 0quot;, run(quot;3n0ndivnquot;)); commands.put(quot;moduloquot;, quot;modquot;); assertEquals(quot;= 2quot;, run(quot;6n3n/nquot;)); commands.put(quot;%quot;, quot;modquot;); assertEquals(quot;= 0quot;, run(quot;3n3nmodnquot;)); commands.put(quot;negatequot;, quot;negatequot;); assertEquals(quot;= 1quot;, run(quot;4n3nmodnquot;)); commands.put(quot;squarequot;, quot;squarequot;); assertEquals(quot;= 3quot;, run(quot;3n4nmodnquot;)); commands.put(quot;++quot;, quot;incrementquot;); assertEquals(quot;= 0quot;, run(quot;3n0nmodnquot;)); commands.put(quot;--quot;, quot;decrementquot;); assertEquals(quot;= 1quot;, run(quot;7n3n%nquot;)); commands.put(quot;comparequot;, quot;comparequot;); assertEquals(quot;= 1quot;, run(quot;7n3nmodulonquot;)); } } public void run() throws IOException public void testComparisonOperation() { { try assertEquals(quot;= 1quot;, run(quot;5n3ncomparenquot;)); { assertEquals(quot;= 0quot;, run(quot;3n3ncomparenquot;)); while(input.hasNextLine()) assertEquals(quot;= -1quot;, run(quot;3n5ncomparenquot;)); { } String line = input.nextLine(); public void testStackControlOperations() if(line.length() == 0) { { assertEquals(quot;= 27 27quot;, run(quot;27ndupnquot;)); showStack(); assertEquals(quot;= 17quot;, run(quot;17n27ndropnquot;)); } assertEquals(quot;=quot;, run(quot;9n6n17n27nclearnquot;)); else assertEquals(quot;=quot;, run(quot;clearnquot;)); { assertEquals(quot;= 17 27quot;, run(quot;17n27nswapnquot;)); for(String token : line.split(quot;[ t]+quot;)) } if(token.length() != 0) public void testStacking() interpretToken(token); { } assertEquals(quot;=quot;, run(quot;nquot;)); } assertEquals(quot;= 4 3 2 1quot;, run(quot;1n2n3n4nnquot;)); } assertEquals(quot;= 3 4 2 1quot;, run(quot;1n2n3n4nswapnquot;)); catch(Exit ignored) assertEquals(quot;= 7 2 1quot;, run(quot;1n2n3n4nplusnquot;)); { } } public void testSpaceSeparation() } { private void interpretToken(String token) throws IOException assertEquals(quot;= 4 3 2 1quot;, run(quot;1 2 3 4nnquot;)); { assertEquals(quot;= 4 3 2 1quot;, run(quot; 1 2 3t4 nnquot;)); try assertEquals(quot;= 3 4 2 1quot;, run(quot;1 2 3 4 swapnquot;)); { assertEquals(quot;= 3 4 2 1quot;, run(quot;1 2 3n4 swap nquot;)); calculator.push(Integer.parseInt(token)); assertEquals(quot;quot;, run(quot;1 2 3 exit nquot;)); } } catch(NumberFormatException ignored) public void testStackUnderflow() { { if(token.equals(quot;exitquot;)) assertEquals(quot;Stack underflowquot;, run(quot;dropnquot;)); { assertEquals(quot;Stack underflowquot;, run(quot;dupnquot;)); throw new Exit(); assertEquals(quot;Stack underflowquot;, run(quot;negatenquot;)); } assertEquals(quot;Stack underflowquot;, run(quot;1nswapnquot;)); else assertEquals(quot;Stack underflowquot;, run(quot;1nplusnquot;)); { } try public void testUnknownCommands() { { executeCommand(commands.get(token)); assertEquals(quot;Unknown commandquot;, run(quot;dripnquot;)); } assertEquals(quot;Unknown commandquot;, run(quot;1n2ndroopnquot;)); catch(StackCalculator.UnderflowException caught) } { public void testExit() output.write(quot;Stack underflownquot;); { } assertEquals(quot;quot;, run(quot;exitnquot;)); catch(UnknownCommand caught) assertEquals(quot;quot;, run(quot;6n9nexitnquot;)); { assertEquals(quot;quot;, run(quot;6n9nexitn17nquot;)); output.write(quot;Unknown commandnquot;); assertEquals(quot;quot;, run(quot;quot;)); } assertEquals(quot;quot;, run(quot;6n9nquot;)); } } } private static String run(String input) } { private void executeCommand(String methodName) throws IOException Reader source = new StringReader(input); { Writer sink = new StringWriter(); if(methodName != null) CalculatorShell shell = new CalculatorShell(source, sink); { try try { { calculator.getClass().getMethod(methodName).invoke(calculator); shell.run(); showStack(); } } catch(IOException caught) catch(InvocationTargetException caught) { { throw new RuntimeException(caught); throw (StackCalculator.UnderflowException) caught.getCause(); } } catch(IllegalAccessException caught) return sink.toString().trim(); { } throw new UnknownCommand(); } } catch(NoSuchMethodException caught) { throw new UnknownCommand(); } } else { throw new UnknownCommand(); } } private void showStack() throws IOException { String stack = quot;=quot;; for(int index = 0; index != calculator.depth(); ++index) stack += quot; quot; + calculator.get(index); output.write(stack + quot;nquot;); } 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 somewhere Common cases, using equivalence partitioning to identify representatives Boundary cases, testing at and around Contractual 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 code Do not assert on implementation specifics — a comparison may return 1 but test for > 0 — or incidental presentation — spacing, spelling, etc.
  • It is better to be roughly right than precisely wrong. John Maynard Keynes
  • The following is overcommitted and unnecessarily specific, hardwiring incidental implementation details as requirements: assertEquals(-1, lower.compareTo(higher)); assertEquals(0, value.compareTo(value)); assertEquals(+1, higher.compareTo(lower)); The following reflects the actual requirement, also making the intent clearer by introducing specifically named custom assertions: assertNegative(lower.compareTo(higher)); assertZero(value.compareTo(value)); assertPositive(higher.compareTo(lower));
  • 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 class RecentlyUsedList { { public RecentlyUsedList() public void Add(string newItem) { { list = new List<string>(); list.Remove(newItem); } list.Add(newItem); public void Add(string newItem) } { public int Count if (list.Contains(newItem)) { { get int position = list.IndexOf(newItem); { string existingItem = list[position]; return list.Count; list.RemoveAt(position); } list.Insert(0, existingItem); } } public string this[int index] else { { get list.Insert(0, newItem); { } return list[Count - index - 1]; } } public int Count } { private List<string> list = new List<string>(); 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; }
  • 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 tests They should focus on interface contract They 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 it Don't solve symptoms — quot;Unit testing is boring/difficult/impossible, therefore don't do unit testingquot; — identify and resolve root causes — quot;Why is unit testing boring/difficult/impossible?quot;
  • • • • • • •• • Consider a number of possible Dependency inversion allows a design's change scenarios and mark dependencies to be reversed, loosened affected components. and manipulated at will, which means that dependencies can be aligned with known or anticipated stability.
  • Client Client Feature Option A Option B Option A Option B
  • B A ⊗ ⊗ C ⊗ ⊗ ⊗ D ⊗⊗⊗⊗ E ⊗ ⊗ ⊗⊗ ⊗ 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 problems If 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
  • Infrastructure Plumbing and service foundations introduced in root layer of the hierarchy. Services Services adapted and extended appropriately for use by the domain classes. Domain Application domain concepts modelled and represented with respect to extension of root infrastructure.
  • Domain Services Infrastructure Application domain Services adapted Plumbing and service concepts modelled and appropriately for foundations for use represented with respect use by the domain as self-contained to plug-in services. classes. plug-ins. concept realisation
  • Core External Root Code Wrapper Wrapped Core Usage Root Code Interface External Wrapper Decoupled
  • Name based on Client implementation Implementation name Name based on Client client usage Implementation name
  • 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 code Anaemic, underachieving objects that are plain ol' data structures in disguise Objects 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(quot;Aardvarkquot;); public class RecentlyUsedList { Assert.AreEqual(1, list.List.Count); Assert.AreEqual(quot;Aardvarkquot;, list.List[0]); public void Add(string newItem) } { [Test] list.Remove(newItem); public void AdditionOfDistinctItemsIsRetainedInStackOrder() list.Insert(0, newItem); { RecentlyUsedList list = new RecentlyUsedList(); } list.List = new List<string>(); public List<string> List list.Add(quot;Aardvarkquot;); { list.Add(quot;Zebraquot;); list.Add(quot;Mongoosequot;); get { Assert.AreEqual(3, list.List.Count); return list; Assert.AreEqual(quot;Mongoosequot;, list.List[0]); } Assert.AreEqual(quot;Zebraquot;, list.List[1]); Assert.AreEqual(quot;Aardvarkquot;, list.List[2]); set } { [Test] list = value; public void DuplicateItemsAreMovedToFrontButNotAdded() } { RecentlyUsedList list = new RecentlyUsedList(); } list.List = new List<string>(); private List<string> list; list.Add(quot;Aardvarkquot;); } list.Add(quot;Mongoosequot;); list.Add(quot;Aardvarkquot;); Assert.AreEqual(2, list.List.Count); Assert.AreEqual(quot;Aardvarkquot;, list.List[0]); Assert.AreEqual(quot;Mongoosequot;, list.List[1]); }
  • 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 metrics Low coverage is always a warning sign Otherwise, understanding what kind of coverage is being discussed is key — is coverage according to statement, condition, path or value? And be careful with coverage myths Without a context of execution, plain assertions in code offer no coverage
  • Watch out for misattribution If you have GUTs, but you fail to meet system requirements, the problem lies with system testing and managing requirements, not with unit testing If 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 messenger If the bulk of testing occurs late in development, it is likely that unit testing will be difficult and will offer a poor ROI If 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