Sunday, March 29, 2015

Unit-Testing Tutorial 2 Appendix: My Tests

package com.proquest.chess;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
public class PawnTest
{
   private Board board;
   private Pawn whitePawn;
   private Pawn blackPawn;
   private Knight whiteKnight;
   private Knight blackKnight;
   private Bishop whiteBishop;
   private Bishop blackBishop;

   @Before
   public void setUp() {
      board = new Board();
      whitePawn = new Pawn(Color.White);
      blackPawn = new Pawn(Color.Black);
      whiteKnight = new Knight(Color.White);
      blackKnight = new Knight(Color.Black);
      whiteBishop = new Bishop(Color.White);
      blackBishop = new Bishop(Color.Black);
   }

   @Test
   public void shouldAdvancePawnOneSquare_fromMiddleOfBoard() {
      board.placePiece(whitePawn, new Position(2, 3));
      assertPawnCanMoveTo (whitePawn, 2, 4);
      board.placePiece(blackPawn, new Position(2, 3));
      assertPawnCanMoveTo (blackPawn, 2, 2);
   }

   @Test
   public void shouldNotAdvancePawnIfBlocked() {
      board.placePiece(whitePawn, new Position(2, 3));
      board.placePiece(blackPawn, new Position(2, 4));
      assertPawnCanMoveTo (whitePawn);
      assertPawnCanMoveTo (blackPawn);
   }

   private void assertPawnCanMoveTo (Pawn pawn, Integer ... xyValues) {
      List<Move> moves = pawn.makeMoves(board);
      assertEquals (xyValues.length/2, moves.size());
      for (int i=0; i<xyValues.length; i+=2) {
         ChessTestUtils.findMoveToSquare(moves, xyValues[i],
         xyValues[i+1]);
      }
   }

   @Test
   public void shouldAdvancePawnTwoSquares_fromStartingPosition() {
      board.placePiece(whitePawn, new Position(2, 1));
      assertPawnCanMoveTo(whitePawn, 2,2, 2,3);
      board.placePiece(blackPawn, new Position(2, 6));
      assertPawnCanMoveTo(blackPawn, 2,5, 2,4);
   }

   @Test
   public void shouldNotAdvancePawnTwoSquares_fromStartingPosition_when2ndMoveBlocked() {
      board.placePiece(whitePawn, new Position(2, 1));
      board.placePiece(whiteKnight, new Position(2, 3));
      assertPawnCanMoveTo(whitePawn, 2,2);
      board.placePiece(blackPawn, new Position(2, 6));
      board.placePiece(blackKnight, new Position(2, 4));
      assertPawnCanMoveTo(blackPawn, 2,5);
   }

   @Test
   public void shouldCaptureForwardAndRightOrLeft_forWhite() {
      board.placePiece(whitePawn, new Position(4, 3));
      board.placePiece(blackKnight, new Position(5, 4));
      board.placePiece(blackBishop, new Position(3, 4));
      assertPawnCanCapture (whitePawn, blackKnight);
      assertPawnCanCapture (whitePawn, blackBishop);
   }

   @Test
   public void shouldCaptureForwardAndRightOrLeft_forBlack() {
      board.placePiece(blackPawn, new Position(5, 4));
      board.placePiece(whiteKnight, new Position(6, 3));
      board.placePiece(whiteBishop, new Position(4, 3));
      assertPawnCanCapture (blackPawn, whiteKnight);
      assertPawnCanCapture (blackPawn, whiteBishop);
   }

   @Test
   public void shouldCaptureForwardAndRight_atLeftEdgeOfBoard() {
      board.placePiece(whitePawn, new Position(0, 3));
      board.placePiece(blackKnight, new Position(1, 4));
      assertPawnCanCapture (whitePawn, blackKnight);
      assertEquals (1, countCaptures(whitePawn.makeMoves(board)));
   }

   @Test
   public void shouldNotCaptureForwardAndRightOrLeft_forWhitesOwnPieces() {
      board.placePiece(whitePawn, new Position(4, 3));
      board.placePiece(whiteKnight, new Position(5, 4));
      board.placePiece(whiteBishop, new Position(3, 4));
      assertEquals (0, countCaptures(whitePawn.makeMoves(board)));
   }

   private void assertPawnCanCapture (Pawn pawn, Piece piece) {
      List<Move> moves = pawn.makeMoves(board);
      Move capture = ChessTestUtils.findMoveToSquare(moves,
      piece.getPosition().getX(), piece.getPosition().getY());
      assertEquals (piece, capture.getCapture());
   }

   private int countCaptures (List moves) {
      int count = 0;
      for (Move move: moves) {
         if (move.getCapture() != null) ++count;
      }
      return count;
   }
}

Unit-Testing Tutorial II: Test-Driven Development


I. Introduction

"Test Driven Development" (TDD) is a discipline for writing code by writing the unit-tests first.  More precisely, it is a cycle of:
  1. Write a (failing!) test
  2. Write just enough code to pass the test.
  3. Refactor the code to keep it clean (but still passing)
where, at each iteration, you build up the "production" code by adding new tests that extend (and define) what the production code is supposed to do.  The process is remarkably similar to "throwing" (as it's called) a clay pot on a potter's wheel: many repetitions of working and examining the work, as you build it up.

I do not specifically recommend TDD as a daily practice.  Familiarity with TDD as a discipline, however, is a very useful skill.  It encourages the close bond between writing code and testing code, that is the hallmark of good, clean, coding.

(Personally I practice what I call TND: "Test Near Development".  I tend to write small chunks of code, and then immediately write tests.  Often the tests then point out edge cases that I might not have thought of in the original code.  This process repeats in many small cycles, similar to strict TDD.)

II. Purpose of the Exercises

The rest of this page is a guide to working through a pair of TDD "katas", or exercises.  Like a martial arts kata, the purpose is to train your (mental) muscles.  This does not mean that code must or should be written exactly this way, at this level of detail.  (Although some people in the industry do, and I'm not sure they're wrong.)

Rather, take the exercises as "fractal" representations of how TDD might apply in writing production code.  The cycle of test, code, refactor is still an extremely useful one to master, even if the granularity, or level of abstraction, is larger than appears in the exercises.

It's also a tiny example of "emergent design", where the code "emerges" from the iteration of adding more tests.  There are flame-wars pro and con emergent design, but it's still worth consideration.

Perhaps the most important point is this: even when you have "finished" your code (real code, not just this kata), remember to do the last "refactor" step.  I have seen entirely too much 'production' code in my career that "works" but was not truly finished, did not have the rough edges sanded down, as it were.
  1. The first exercise (prime factorization) is a "pure" kata.  It emphasizes the step-by-step TDD process at a very fine-grained level.  Most developers look at this exercise and complain "why don't I just write the code?"  This kata is not about the final answer, but about practicing the TDD cycle.

  2. The second exercise (chess pawn moves) is more like a real-world program.  It has several relatively independent parts, which can be developed one at a time via tests.

III. The Prime Factorization Kata

(Heavily borrowed from Uncle Bob Martin's example.)

The "Prime Factorization" kata is meant to be done -- not read.  If you're reading this, take each step at a time, and write your own implementation of "just enough code" to make the next test pass.  Don't skip ahead -- really go step-by-step.

I've written up descriptions of each step as a guide, but only use it if you're stuck.
  1. Get the skeleton in place.  Start by checking out the base project from svn://caucus.com/CodeExercise/trunk.  Look at the com.proquest.kata package.  This provides a basically empty framework with the skeleton already created.  You'll see that the first test, and the initial implementation, define the prime factors of 1 as an empty list.

    I've also provided the listOf() method to easily generate lists of numbers to be used in the test asserts.

       public List<Integer> getFactors(int num) {
          List<Integer> results = new ArrayList<Integer>();
          return results;
       }
        
       = = = = = = = = = = = = = = = = = = = = = = = = = = = =
        
       @Before
       public void setup() {
          factorizer = new PrimeFactorizer();
       }
        
       @Test
       public void test1() {
          assertEquals (listOf(), factorizer.getFactors(1));
       }
        
       private List<Integer> listOf(Integer ... numbers) {
          List<Integer> results = new ArrayList<Integer>();
          for (Integer number: numbers)
             results.add(number);
          return results;
       }
    
    
  2. Add the first test(s).  The process we'll follow is very simple: we'll just keep adding tests for the next integer, until something breaks.  So we add:

       @Test
       public void test2() {
          assertEquals (listOf(2), factorizer.getFactors(2));
       }
    
    
    This test fails.  What's the simplest thing we can do to make it pass?  (That doesn't just hard-code the single answer we're looking for?)

    Add:
       if (num > 1) {
          results.add(num);
       }
    
    
    In fact, this change also passes the next test:

       assertEquals (listOf(3), factorizer.getFactors(3))
    
    

  3. Add just a little bit of logic.  Now it gets interesting.  When we add a test for:

       assertEquals (listOf(2,2), factorizer.getFactors(4));
    
    
    we have to actually do some computation.  But how much code is "just enough to pass"?  In this case, let's just handle a single factor of 2.  Now the code looks like:

       if (num % 2 == 0) {
          results.add(2);
          num = num / 2;
       } 
       if (num > 1) {
          results.add(num);
       }
    
    
    You'll notice the last part is the same as before.  So all we're adding is "taking out" the first factor of 2 (if there is one).

    You should begin to see a pattern here.  At each new test, handle the new case.  If the new case looks a lot like the previous case, do the simplest possible generalization of both cases.

    This change actually handles the test cases for 4, 5, 6, and 7!

       assertEquals (listOf(2,2), factorizer.getFactors(4));
       assertEquals (listOf(5),   factorizer.getFactors(5));
       assertEquals (listOf(2,3), factorizer.getFactors(6));
       assertEquals (listOf(7),   factorizer.getFactors(7));
    


  4. Generalize a single piece of logic.  Adding a test for 8 raises a more general case.  How do we handle multiple instances of the same factor?  Again, take the "single case code" and make it more general, while changing as few other things as possible.
       while (num % 2 == 0) {
          results.add(2);
          num = num / 2;
       }
       if (num > 1) {
          results.add(num);
       }
    
    
    So really, all we've done is change the "if" to a "while"!

  5. Rinse and Repeat.  Adding a test for 9 is much like what we did for 8, only now we're making it more general than hard-coding a divisor of 2.  Essentially, we're wrapping a loop of candidate factors around the previous code, and substituting the candidate for the previously hard-coded 2:

       for (int factor=2; num > 1; ++factor) {
          while (num % factor == 0) {
             results.add(factor);
             num = num / factor;
          }
       }
       if (num > 1) {
          results.add(num);
       }
    
    
    We actually had to do two things here, which is a little bit of a cheat.  We added the loop, and we told it when to stop (when num got reduced, by division, to 1). This passes the test for 9, and 10, and ... well, as it turns out, for everything!
  6. Refactor.  But we're not done... remember, the cycle is failing test, passing test, and then refactor.  Looking closely at the above code, the "if (num > 1)" is apparently no longer needed.  Try taking it out, and see if the tests pass!

  7. More refactoring?  What other refactoring can be done?  The inner while loop could become a for loop, which removes one line of code.  It's not clear which form is more obvious to the reader.  Think about it.

  8. Other Tests?  Are there are any other tests we should consider?  Certainly take a stab at some larger numbers.  But also consider smaller numbers: are there any edge cases we need to test?  What about 0?  -1?  Write tests for those as well.

  9. Refactor Tests.  Think about refactoring the tests, too.  Is there code common to all of the tests?  This is very simple set of tests. 

    It would be tempting to just lump all the asserts together in one @Test method.  For this example, it might even make sense (and certainly be shorter).  In general, though, each test is making a declarative statement about the code.  Don't over-optimize that, at least not at first.  Once you've got truly working code, then think about optimizing your tests.

    (OTOH, don't blindly cut-and-paste large amounts of code for each test... if you're copying more than 3 lines at a time in your tests, then it's time to do some refactoring.  Fail, pass, refactor should apply to your tests just as well as to your production code.)

  10. Efficiency?  Now that the code is working, we can think about efficiency.  Avoid "premature optimization" -- get something working first so that you fully understand the problem (and have a good suite of tests!), and then think about optimization.  (Obviously there are cases where some thought about efficiency may have to come first.  But they're pretty rare.)

    Even the optimizations can be TDD'd.  Consider:
    • Candidate factors could be 2, and then only odd numbers.
    • Candidate factors could be just prime numbers.
    • Candidate factors could be just prime numbers, no higher than the square root of the original number.  (Or does it matter?)
    • Candidate factors could be cached for future calls to getFactors.
    • Is there a (presumably) faster way to get the dividend and the modulus in one operation?  (Most hardware does both in one operation.)
       
  11. Coda (aka the little bit of music at the end of a symphony, after you think the symphony is over):

    Do this whole exercise over again, from scratch -- in about two weeks.  Don't look at this exercise, don't look at the code you wrote the first time.  Do it literally like a martial arts kata.  Just follow the fail / pass / refactor pattern.  Afterwards, see what looks the same and what looks different.  If feasible, try it in a different language.

  12. Before I had studied Zen for thirty years, I saw mountains as mountains, and waters as waters. When I arrived at a more intimate knowledge, I came to the point where I saw that mountains are not mountains, and waters are not waters. But now that I have got its very substance I am at rest. For it's just that I see mountains once again as mountains, and waters once again as waters.
    Ch'uan Teng Lu, 22. (The Way of Zen 126)

 

IV.  Chess Pawn Moves

In the first module, I introduced a (very) minimal set of classes for supporting Chess problems.  They are in the com.proquest.chess package in the same project as the prime factorization exercise.
  1. Pawn moves.  For this second kata, we'll build on the chess package, and create a Pawn class that generates all of the legal moves that a Pawn can make.  Now the Pawn is probably the most illogical piece in chess: it has four different kinds of moves, some of which can be combined.  This makes for a very nice breakdown of TDD'ing writing the class.

    To refresh your chess knowledge, a pawn can:

    1. Advance one square forward, if nothing is blocking it.

    2. Advance two squares forward, if it is in its starting position (2nd rank, or y=1 for white, y=6 for black) and nothing is blocking it.

    3. Capture an opposing piece one square to the right and forward, or left and forward.

    4. Promote to a Queen, Rook, Knight, or Bishop if its move or capture leaves it on the last rank (y=7 for white, y=0 for black)

    5. "En passant" capture.  If a (e.g. black) pawn takes the 2-square move option, and lands next to a white pawn, the white pawn can capture the black pawn and lands on the square where the black pawn would have been if it had only moved one square.

      Just to make it worse, the white player must capture the black pawn immediately or forever forfeit the move.  (And my Board class currently has no way to record move numbers, which would be required to properly implement the "do it now or never" rule.)
       


  2. TDD strategy.  The TDD approach is to take the rules above, one at a time (or in even smaller pieces), write a failing test, then make just that test pass, and then refactor (and keep all the tests passing).  When I worked the exercise, my thinking (and the tests) went something like this:

    1. White pawn should move forward (+y) one square
       
    2. Black  pawn should move forward (-y) one square
       
    3. Hmm, I'm gonna have a lot of this white pawn, black pawn, stuff.  Refactor my tests to combine both in each test.  Remember that test code should be just as clean and as abstracted as production code!  So I wrote an "assertPawnCanMoveTo(pawn, Integer ... xyValues)" utility method that abstracts my tests and makes them shorter.
       
    4. Pawn should not move forward if blocked.
       
    5. Pawn on starting position can move two squares forward if not blocked.  The move-one-square and then move-two-squares code is probably going to look similar; after my test passes, I may want to refactor.  This is a good time to use Emma's "coverage as... Junit" to see if all paths have been exercised.
       
    6. Normal Pawn capture.   Hmm, I may need to go back to one test for White, and one test for Black.  Unless I can come up with a refactoring that is both clever and clear... which may be impossible.  When the board gets complicated, use board.print() just to see what's going on.
       
    7. Oops, don't allow captures of your own (same-colored) pieces!
       
    8. I ran coverage again, and noticed I never exercised prohibiting captures past the left or right edge of the board.  So I added a test for that, too.
       
    9. Promotion to Queen, Rook, Knight, or Bishop on moving a Pawn to the last rank.  Note that this counts as 4 different moves.
       
    10. Promotion during a capture onto the last rank.  A quick refactoring of the Pawn code, to re-use promotion code in both moves and captures, probably suggests itself here.
       
  3. Safe Refactoring.  At this point, I have everything working except "en passant" moves, so it's time to look back for refactoring.

    1. There's probably a lot of code that does something like "a white pawn at y==6 or a black pawn at y==1" that could be improved.  We could use data instead of code (perhaps a Map indexed by Color that gives us y values, or maybe Position's).

    2. Or perhaps a better approach would be two new sub-classes BlackPawn and WhitePawn that extend Pawn, but with some different constants.   E.g. lastRank() is 7 for WhitePawn but 0 for BlackPawn.

    3. The makeMoves() method probably has two different responsibilities: one for moves, and one for captures, and they probably deserve different (sub) methods, if only for readability.

    The point is, now that we have good test coverage -- and your whole Pawn class should be totally green in coverage right now -- we can confidently refactor all we want!  Don't just stop because it works, now make it good!
     
  4. Check against my tests.   Because we have a standard API for Piece's, you can cross-check your code by running my tests.  Just paste that text into a new test class and run it.  Take a look at the names I used for the tests -- I have zero comments, and (I think) no need to add any.  Look at the abstractions, like assertContainsPromotionTo().  How would you improve these tests?  Do they cover cases that you didn't?  Did you cover cases that my tests didn't?









Saturday, March 28, 2015

Welcome!

This is the 'landing' page for my blog posts.  While Blogger is cool, it's not exactly oriented towards organizing related articles.  So I'll link all of my articles here, to make them easier to find.

And yes, I'm an engineer and a humanist.   So I'm offering either one free massage (yes, I studied massage) or one unit-testing lesson (yes, I teach unit-testing) to the person with the best punch line for "An engineer and a humanist walk into a bar...".

Unit-Testing and Java
  1. Tutorial 1: Introduction
  2. Tutorial 2: TDD Kata (Prime Factorization, and chess pieces)
  3. The Junit "Green Lantern" Oath!

Wednesday, September 11, 2013

Unit-Testing Tutorial 01: Introduction

 

I. Introduction and Purpose

This series of blog posts is an adaptation of a set of classes on unit-testing that I teach at ProQuest (a leading academic and corporate search engine for 'paywalled' articles and data).  ProQuest has been kind enough to allow me to publish this series for anyone to read and learn from.  For the background, see [link].

This is the first in a five-part series on unit-testing in Java.  The complete series covers:
  1. What is unit-testing?  why do we do it, what do we gain?
  2. An exercise in "TDD", test-driven development.
  3. Writing tests with "EasyMock" mock objects.
  4. Patterns for cleaning up existing (messy) unit-tests
  5. Advanced unit-testing.  (Possibly including taking a big ugly mess of legacy code, and making it testable.)
The goal of this series is to improve developers' skill and comfort level with unit-tests, and thereby improving the overall number and quality of unit-tests in any Java application.

Important: before attending or working through this class, please make sure to set up your environment, as described in Tutorial 00: Before You Begin.

II. What is unit-testing?

Mike Cohn breaks automated software testing into three layers of a triangle, based on the "Three Little Pigs" story:
  • Straw is the end-to-end, GUI testing, usually driven from a framework like Selenium.  It's the easiest and quickest to build, and covers a lot of ground.  But it's terribly brittle, falls over easily, and is expensive to maintain.
  • Sticks are "integration" testing.  It means testing high level (but not GUI) classes against real (often external) services and dependencies.  Integration tests are often driven from Junit.
  • Bricks are "unit-testing".  They are solid, expensive to build, but cheap to maintain.  They test small "units" of code -- the smaller, the better!  They are totally self-contained -- in fact, I frequently run all of our unit-tests w/o a network connection, just to make certain!
As an aside, Cohn recommends that the investment into testing match the areas of the three different parts of the triangle.  All three levels are valuable, and for different reasons.  And manual testing is still useful too, but that's another story.
(I stole the triangle from Patrick Wilson-Welsh's excellent article about the testing triangle, which is well worth reading separately.  Plus it has cool pictures.)

A Unit-test Exercise
OK, but what does a unit-test actually look like?  What does it test?
When I interview Java developer candidates, I give them a simple coding exercise to do in-person.  (A practice which I borrowed from Menlo.)  It's just a simple "reverse the words in text" exercise, but  I require that they write unit-tests along the way.  Here's the exercise class itself, under src/main/java:

public class TextExercise {
    /**
     * In English, words on a line are read left-to-right.  In some other
     * languages, words are read right-to-left instead.
     *
     * For this exercise, assume that we have been given a single text
     * string, containing 0, 1, or more lines (separated by the newline
     * character \n).   Reverse the order of the words in *each* line,
     * and return a new String containing the resulting text.  Maintain
     * the same number of lines.
     *
     * Assume that words are separated by (one or more) spaces.  You do
     * not need to preserve multiple spaces between words.  Do not worry
     * about punctuation at the end of a word, just treat it as
     * part of the word.
     *
     * So, for example, "Hello, world!" should be transformed into
     * "world! Hello,".
     */
    public String reverseWordPositionsInAllLines (String text) {
        return text;   // obviously, this is wrong, it's just a place to start.
    }
}

Then, under src/test/java, there's a matching unit-test class:

public class TextExerciseTest {
    @Test
    public void shouldReverseHelloWorld() {
        TextExercise exercise = new TextExercise();
        assertEquals ("world! Hello,",
           exercise.reverseWordPositionsInAllLines("Hello, world!"));
    }
}

We use the Junit framework to run the tests.  In Eclipse, this is as simple as right-clicking in the test class, and selecting "run as Junit Test".  Every method annotated with "@Test" gets run.  The assertEquals() method (and a whole host of other assertions) is provided by Junit.  If the assert succeeds, the test passes, and Eclipse shows a green bar.  If any assert fails, we get a red bar, and Eclipse tells us what failed and why.

So a unit-test is exercising a small chunk of code, with defined inputs and outputs.  A nicely contained black-box, if you will.  Sometimes those boxes can get pretty big... but more about that later.  But in essence, a unit-test is an experimental framework, wrapped around the "black-box" we want to test, that tells us if the box does everything we think it should do.
There's a key phrase right there: everything we think it should do.  When I'm interviewing a candidate, I'm looking for:
  • Could they actually write the code?  Did they write a method that passed shouldReverseHelloWorld() ?  (You may think that's trivial, but do it under pressure with an interviewer watching you.)
  • Did they write other tests?  Tests with more text and spaces.  Text with multiple lines.
  • Did they consider edge cases?  A null input.  An empty line.  White-space characters other than "space"?  (E.g. tabs, weird Unicode spaces, etc.)
  • Did their tests actually exercise all of the code that they wrote?  (We'll demonstrate how to see that in a little bit.)
This is all about "did it do everything we think it should do".  Not just the happy paths, but edge cases, null inputs, and so on.  We have to think all the way around the box, in order to be sure.  A good set of unit-tests covers all of those cases, while managing to avoid exponential explosion.  (We don't really need to test the entire works of Shakespeare.)

So try it for yourselves.  If you've already checked-out the CodeExercise project from git, it's in the package com.proquest.codeexercise.  Alernately you can check it out via svn from svn://caucus.com/CodeExercise/trunk.

More about Junit
Junit has several other method annotations that may be used  in a test class:

@BeforeExecute this method before every @Test method.  Put common initialization/preparation code in this method.
@AfterExecute this method after every @Test method.  Put common cleanup code here.  (Rarely needed.)
@IgnoreIgnore (don't run) an @Test method.  If you do this, add a comment explaining why!
@BeforeClassRun this method once, before executing any tests.  Used mostly to set up statics.  Rare (because statics are evil!)
@AfterClassRun this method once, after executing all tests.  Used mostly to cleanup statics.  (I use it to kill threads accidentally started by some tests.)
@Test(expected=Exception.class)Test fails if the method does not throw the named exception.

Junit also has a suite of assert methods.  The most commonly used ones include:
  • assertEquals(a, b)
  • assertTrue (a)
  • assertFalse(a)
  • assertNull(a)
  • assertNotNull(a)
  • fail()     (If you get to this point in the code, the test has failed!  Often used with try/catch.)
More detailed information can be found at:

III. Why Unit-test?

Why do we write unit-tests?  What's the benefit?  After all, if we're writing the code correctly in the first place, why do we need the extra cost of writing tests?

The simple answer is, we're all human.  We make mistakes.  Tests help protect us from our mistakes.  Remember the Mars Climate Orbiter, which was destroyed because the on-board software used metric units, and the ground-control software used English units.   Keeping the tests as part of the project means we can run all of them at any time, and raise our confidence that we haven't made any new mistakes.

But the real answer is much deeper than that.  Over the course of several years, I found that writing unit-tests forced me to write better code.  Not just code with fewer mistakes, but literally better code.  Like many things, this is a lesson perhaps better learned through experience.  But I can sum it up in these bullet points:
  1. Fewer mistakes.  OK, that's the easy one.  Code that has tests typically has fewer mistakes.

    Change == Surprise.  Unit-tests save us from the Law of Unintended Consequences.  A unit-test can save us from future bugs that are "accidentally" inserted, especially by "other" people.  That's why, when we're about to merge our changes into a project, we run all tests.  Not just the ones concerned with the code we were working on.
     
  2. No Fear.  Take #2 a little further.  When we don't have good unit-test coverage, there's always a risk in making changes to existing, (presumably) working, code. (And if you're not afraid of this case... well, you should be!)

    When you do have good test coverage, you have a safety net.  You can make changes, fix bugs, improve existing code... and at least have some confidence that if you break anything in a really bad way, the existing tests will catch you.  Entirely too much bad code gets written because someone hacked around existing code, fearful of changing its behavior, when they would have been much better off rewriting the (small) relevant part.

    (I've written a unabashedly geeky article, just about how programmers misuse this fear, called the Junit / Green Lantern Oath.)
     
  3. Tests are part of the code.  They're not just bolted on afterwards.  This forces us to think about how we'll test a method, as we're writing it.  There's a methodology called "TDD" (Test Driven Design), where tests are written first: the design of a method is built up, piece by piece, by adding tests... writing just enough code to pass each test in turn.

    I'm not religious about TDD.  But tests should be written along with the production code, in some form.  Use the tests as a kind of scaffolding, supporting the structure as we're building it.
     
  4. Tests document the code.  Good tests essentially become the specifications for the code!  Even better: add commented links, in each test, to acceptance criteria in the original story.  This gives us an easily-travelled highway, from story to code and back again.  Writing the tests often expose ambiguities in the original story.
     
  5. Tests force smaller methods.  Long methods are simply harder to test.  If I know I'm going to be writing tests in the next few minutes, I'm going to make it easy on myself and break the new code into smaller pieces, each of which can be tested independently.  And while I'm doing that, I now have the opportunity to name the (new, smaller) methods in a way that is more self-documenting than a longer, single method.  These smaller methods with better names in turn invite me to break up the flow of the code into more abstract concepts, that can become a sort of higher-level "domain" language about the problem that is being solved.  All because I wanted to write tests!
     
  6. Tests force de-coupling.  Classes that have tight coupling to other classes are hard to work with.  They're even harder to test.  The worst cases are classes that are tied to specific implementations of other classes.  These are most often the result of using static methods from another class (see Why Static Classes Are Evil), or from over-using 'new' (constructors) of other complicated objects.

    If tight-coupling gets in the way of testing, then it's getting in the way of good design, period.  I'd rather have a class with a few more arguments in its constructors, than a class that always depends on a specific implementation of another class.  When writing tests becomes painful, it's an early warning that my code is starting to smell.
     
  7. Tests force fewer side-effects.  The easiest method to test is one that takes some arguments, and returns a (single) result, with no changes to the object's internal state, or any other side-effects.  That's not often practical: frequently a method has to change its object's state, by design.  In that case the change of state should be testable!  (E.g. there should be a getter method that lets you examine the new state.)

    If the method changes the state of some other object... now we're sliding down the slippery slope of side-effects.  Once again, if it's hard to test, it's hard to use, and suggests that the design itself is flawed.

IV. Emma Code Coverage

Once you have unit-tests, it's really nice to know how much testing you have in place.  Enter EMMA, the unit-test writer's friend.  Emma works hand-in-hand with Junit, to measure how many lines (or instructions, or blocks, etc.) of your code is actually exercised by your tests.  Emma can produce a statistical report, e.g. such-and-such a percentage of the lines of code in project X, or package Y, or class Z, are covered (or not covered) by tests.

With the EclEmma Eclipse-plugin, you can also see visually how much of a class is covered.  When the plugin is installed (Help, Eclipse marketplace, find "emma".  Install EclEmma, done), right-click on a test class or an entire project, and choose "coverage as... Junit".  You'll see lines covered by tests in green, lines not covered in red, and lines that are partly covered (e.g. an if with only one of the two possible outcomes covered) in yellow.

Code coverage isn't everything, however.  It's possible to write a bad test... that exercises a method, but never actually makes any useful assertions about the result of the test.  The best use of Emma is to find the "red" code, paths that are definitely not tested, and add tests that both cover it and verify that the results of the code are correct.

 

V. A Chess Exercise

The rest of this module is devoted to working an actual exercise, writing both tests and code, that involves chess.  I've found that chess programming problems are small enough to work with, but sufficiently complicated (and object-oriented enough) to make useful exercises.  I've built the underpinnings of a very simple chess board in the com.proquest.chess package in the CodeExercise project mentioned earlier.  It includes some tests, to give you a feel for the pattern.

The goal of this exercise is to add a Rook class that correctly identifies all of the possible moves that a rook can make.  For our purposes, ignore whether a move leaves (or puts) a King in check: this is just about where a Rook can move, or capture, depending on its location and other pieces around it.

The fundamental classes are:
  1. Color.  An enum: Black or White.
  2. Type.  Another enum.  K, Q, R, B, N, or P.  Or Nothing.
  3. Position.  Just an (x,y) pair that represents a spot on the board.   Positions are either valid or invalid: an invalid position is "off the board".
  4. Piece.  An abstract class.  All specific pieces (e.g. King, Knight, Rook) extend Piece.  A piece has a Color, a Type, and a Position.
  5. Move.  Each move has a position (where a piece ends up after making a move), the type of promotion (for pawns only), and the piece captured by the move (if any).
  6. Board.  The actual board where everything happens, plus collections of the white and black pieces currently on the board.  A new Board is empty.  The two most useful methods in Board are placePiece (puts a piece on the Board) and print (displays the entire board in an easy-to-see format).
Take some time to wander through the classes, and their tests (notably BoardTest), to see how they operate.  They're not complete by any means, but they provide enough infrastructure to write the Rook class.  We're not concerned with efficiency here -- although if we write the tests correctly, we could improve efficiency later without changing the tests!

Once you feel familiar with the infrastructure, start by creating a Rook class that extends Piece, and returns an empty List of Move's.  Then write a test for just that much.

After that, start working on generating a list of Move's for your Rook.  When you write the tests, you may find the ChessTestUtils class helpful.

When you are confident that your Rook is working properly, you can add my unit-tests to the project, and see if they also run green.  (Or you can just merge in the branch 'rooktest', which is probably easier.)  But don't do this until you are completely confident in your own code (and your own tests!).

VI. Other Resources

  1. Shaun Abram has a very good article about Software Quality via Unit Testing, where he makes a strong case for the economic value of unit-testing.  The first few pages are an excellent introduction for managers who may not be familiar with the ins and outs of coding.
  2. A good book for total beginners is Pragmatic Unit Testing in Java with Junit.  (It's somewhat dated, as it's based on a older version of Junit, but still worth the read.)
  3. I believe every developer should own a copy of "Uncle" Bob Martin's Clean Code.  It's only partially about unit-testing, but this one book captures the essence of how to write clean code, including clean unit-tests!
  4. Michael Feathers' book Working Effectively With Legacy Code offers many, many strategies for how to add tests to a project that doesn't have any.  This is a slow but very valuable read -- I found myself just reading a few pages at a time, and pondering them for a day or two before moving on.
  5. A short but valuable article by Juri Strumpflohner on descriptive assert messages.
  
This article is Copyright (C) 2013 by Charles Roth.  All rights reserved.

Tuesday, July 5, 2011

The Junit Green Lantern Oath

In brightest day, in blackest night
     No failing tests shall escape my sight!
Let those who sloppy code would write
     Beware my power -- Junit's green light!


OK, it's kitsch.  Tacky.  Geeky.  Even (shudder) cute.
But it also has a point.  Bear with me.  (I'm assuming you're a software developer, and already familiar with unit-testing, and Green Lantern.)

Why Unit-Test?
I wrote my first Fortran program in 1970.  I've been coding ever since.  These days they call me a "senior architect" -- hopefully for my knowledge and experience, and not my gray hair.
 
But every day, in every shop, in every language, we're bathed in two feelings:
  • Delight.  The excitement, the joy, the power, of making these lumps of silicon jump through hoops, stand up and perform miracles at our command.

  • Fear.  Yes, fear!  We write new code in a burst of energy and delight.  And then... when we go to change it, or touch code that we didn't write... we are afraid.  Very afraid.  Because the faithful digital servant that takes our every instruction as the word of gods... turns on us, savages us in a microsecond, when we err. 

    That is why we unit-test.  Oh, people will blather on about Agile productivity, improved ROI, safety-nets, unit-tests as documentation, ad nauseum.  But those are just words to convince the blinkered bureacrats.  They will never know the gut-twisting agony and responsibility that we feel, when the spaghetti code slides, like razor blades, through our naked hands.
     
Why Green Lantern?  The parallels are striking:
  • To fly on a thought.  To weave constructs of energy out of our imagination and willpower.  To make a difference!  To build where others would destroy.
     
  • Fear is our nemesis.  In the emotional spectrum of the DC Universe, green is the color of will-power.  Yellow is the color of fear.  Originally, Green Lanterns were powerless against yellow.  Even now, new Green Lanterns are recruited with Gabriel-like declarations: "Hal Jordan of sector 2814, you have shown the power to overcome great fear".

Why an Oath?
The Oath came to me in a "flash" in 2011, when I saw the (so-so) Green Lantern movie.  The green light of the Junit test framework merged in my mind with "Green Lantern's light!", and the rest was, well, history.  Or fantasy.  Or whatever.

In the real world, neither a green wrist band nor a comic-book oath is going to help us write better code or more tests.  But consider what's behind the oath.

In the 1994 Emerald Twilight and 2004's Green Lantern: Rebirth, a troubled Hal Jordan succumbs to Parallax -- the literal (yellow!) embodiment of fear, long imprisoned in the master Green Lantern battery on Oa.  Jordan nearly destroys the Green Lantern corps, redeems himself by reigniting our dying (yellow!) sun, and finally triumphs over Parallax with the aide of his fellow Lanterns:
John Stewart: Hal, what's your plan?
Hal Jordan: Remember fear.
Guy Gardner: "Remember fear?"  What the hell kind of science crap is that?

Remember Fear
As developers, that's exactly what we need to do.
When we don't acknowledge our fear, we:
  • Deny it, and insist on absolutely precise specifications.  ("Reaction formation").  "I could write perfect code if only the specifications were correct!"

  • Deny the issue entirely, and push it away from the ego: "I'm a brilliant programmer. Tests waste my time."
     
  • Turn the fear inwards, and obsess.  "I'm a careful programmer, I study every possible path of execution."   We hunch over our monitors, and carry the stress home in our bodies.
     
  • Despair, and live in fear.  "That's just the way it is.  We'll always have bugs."  And our delight slowly erodes.
     
  • Look for religion.  "If only we faithfully follow six sigma, or CMM Level 3, or all become SCRUM masters, we'll be saved."
     
  • Become neurotic, and make the fear part of us.  We painstakingly hack additions and kludges around existing code, like an oyster putting down layers of pearl around an irritating grain of sand, and think that this makes us a good programmer.
     
  • Project our fear, and blame everyone else for bad code.
     
  • Avoid the source of our fear.  Avoid change, avoid rewriting, avoid refactoring.
     
  • Escape into "if only's".  "If only we could rewrite the entire project, this time we'd do it cleanly!"  Or, "If only I could find a job where things were done right for a change."
When we face our fear, we...
  • Accept the up-front cost of unit-tests.
  • Build our own safety nets.
  • Sleep better at night!
  • And best of all... begin to realize that change can be safe, and even exhilirating.  The code that was a mess... can be changed, can be made clean, can be made whole.
That's why there's an oath: to remind us.  Face your fear.  Own your power.  And own the green light!