JUnit: A Hands-On Introduction

Elliotte Rusty Harold

Friday, January 19, 2007

elharo@metalab.unc.edu

http://www.cafeaulait.org/


Different Kinds of Tests


Different Styles of Testing


When To Write the Tests


Or


Why write automated tests?

Often a developer will want to make some optimizations to a piece of code on which a lot of user-interface behavior depends. Sure, the new table sorter will be ten times faster and more memory efficient, but are you sure the changes won't affect the report generator? Run the tests and find out. A developer is much more likely to run a test that has encapsulated how to set up and test a report than he is to launch the application by hand and try to track down who knows how to do that particular test.

Micro Test Driven Development

  1. Write a test for a feature

  2. Code the simplest thing that can possibly work

  3. Run the test

  4. Test passed ? goto 1 : goto 2


Everything is preceded by a test


JUnit


A class to develop: The Fraction Class


Basic operations the class needs


Big Question


Java utilities the class needs


Test Classes


Demo: A simple test for addition

import junit.framework.TestCase;

public class FractionTest extends TestCase {

    public void testAddition() {
      
        Fraction half = new Fraction(1, 2);
        Fraction fourth = new Fraction(1, 4);
        
        Fraction actual = half.add(fourth);
        Fraction expected = new Fraction(3, 4);
        assertEquals(expected, actual);
        
    }
    
}

The Result: A simple Fraction class


public class Fraction {
    
    public Fraction(int numerator, int denominator) {
    }

    public Fraction add(Fraction fourth) {
        return null;
    }

    public boolean equals(Object o) {
        return true;
    }
    
}

What's wrong with this?


Another Test

    public void testAddition2() {
        
        Fraction half = new Fraction(1, 2);
        Fraction fourth = new Fraction(1, 2);
        
        Fraction actual = half.add(fourth);
        Fraction expected = new Fraction(1, 1);
        assertEquals(expected, actual);
        
    }

Some Tests Pass the First Time You Run Them


We need to test equals failing

    public void testNotEquals() {
        
        Fraction half = new Fraction(1, 2);
        Fraction fourth = new Fraction(1, 4);
        
        assertFalse(half.equals(fourth));
        
    }

Plausible class


public class Fraction {

  private int numerator;
  private int denominator;
  
  public Fraction(int numerator, int denominator) {
    this.numerator = numerator;
    this.denominator = denominator;
  }
  
  public Fraction add(Fraction f) {
    
    int denominator = this.denominator * f.denominator;
    int numerator = this.numerator * f.denominator + f.numerator * this.denominator;
    
    return new Fraction(numerator, denominator);
  }
  
  public boolean equals(Object o) {
    Fraction f = (Fraction) o;
    return this.numerator / (double) this.denominator 
      == f.numerator / (double) f.denominator;
  }
    
}

Exercise 1: Compile and run the simple addition test


What else do we need to test?

?

Exercise 2: Expanding the test suite


Macro Test Driven Development


Benefits of Test Driven Development


For Unit Testing to Work


Test Driven Testing


Unit Testing


Reinitialization Example


Consequences


static data is not reinitialized

One of these tests will fail:

    private static LinkedList list = new LinkedList();   
    
    public void testAdd() {
        String s = "Brooklyn";
        list.add(s);
        Object o = list.getFirst();
        assertEquals(s, o); 
    }
    
    public void testSizeAfterAddingOneElementIsOne() {
        String s = "New York";
        list.add(s);
        assertEquals(1, list.size());
    }

Fixtures


Exercise 3: Adding a Fixture

Move the shared code into fields. Write a setUp() method that initializes these fields.


Answer 3: Adding a Fixture

import junit.framework.TestCase;

public class FractionTest extends TestCase {
    
    private Fraction half;
    private Fraction fourth;

    protected void setUp() {
        half = new Fraction(2, 4);
        fourth = new Fraction(1, 4);       
    }
    
    public void testAddition() {
        Fraction actual = half.add(fourth);
        Fraction expected = new Fraction(3, 4);
        assertEquals(expected, actual);
    }
    
    public void testSubtraction() {
        Fraction actual = half.subtract(fourth);
        Fraction expected = fourth;
        assertEquals(expected, actual);
    }
    
    public void testAddNumberToItself() {
        Fraction actual = half.add(half);
        Fraction expected = new Fraction(1, 1);
        assertEquals(expected, actual);
    }
    
}

Freeing resources after each test


Constructors for TestCase


Exercise 4: Adding a Constructor

Add a constructor to your test class.


Answer 4: Adding a Constructor

    public FractionTest() {}

    public FractionTest(String name) {
        super(name);
    }

Assertion Messages


Exercise 5: Messages

Add assertion messages to each test.


Answer 5: Assertion Messages

import junit.framework.TestCase;

public class FractionTest extends TestCase {
    
    private Fraction half;
    private Fraction fourth;

    protected void setUp() {
        half = new Fraction(2, 4);
        fourth = new Fraction(1, 4);       
    }
    
    public void testAddition() {
        Fraction actual = half.add(fourth);
        Fraction expected = new Fraction(3, 4);
        assertEquals("Could not add 1/2 to 1/4", expected, actual);
    }
    
    public void testSubtraction() {
        Fraction actual = half.subtract(fourth);
        Fraction expected = fourth;
        assertEquals("Could not subtract 1/4 from 1/2",expected, actual);
    }
    
    public void testAddNumberToItself() {
        Fraction actual = half.add(half);
        Fraction expected = new Fraction(1, 1);
        assertEquals("Could not add 1/2 to itself using same object",
                      expected, actual);
    }
    
}

Floating point assertions

As always with floating point arithmetic, you want to avoid direct equality comparisons.


Exercise 6: Floating point test

  1. Write a test for the double value of 5/3.

  2. Implement the doubleValue() method.


Answer 6: Floating point test

    public void testDoubleValue() {
        Fraction a = new Fraction(5, 3);
        assertEquals(5.0/3.0, a.doubleValue(), 0.000000001);
    }

Integer assertions

Straight-forward because integer comparisons are straight-forward:


Exercise 7: Integer test


Answer 7: Integer test

    public void testGetNumerator() {
        Fraction a = new Fraction(5, 3);
        assertEquals(5, a.getNumerator());
    }
    
    public void testGetProperNumerator() {
        Fraction a = new Fraction(5, 3);
        assertEquals(2, a.getProperNumerator());
    }

Integration Tests


Exercise 8: Data Driven TestCase

Working in teams of 4, develop a list of different fraction pairs likely to expose bugs and cover the problem space. Then write a parameterized test that loads this data to test addition, multiplication, subtraction, and division.


Answer 8: Data Driven Test


Data Driven Test Issues


Answer 8 Improved: Data Driven Test

This variant now runs all tests even if one test fails:

    public void testIntegrationGetProperNumerator() {
        
        int failures = 0;
        StringBuffer failureMessage = new StringBuffer();
        for (int i = 0; i < data.length; i++) {
          int[] row = data[i];
          Fraction f = new Fraction(row[0], row[1]);
          try {
              assertEquals(row[2], f.getProperNumerator());
          }
          catch (AssertionFailedError err) {
              failures++;
              failureMessage.append(err.getMessage() + "\n");
          }
        }
        
        if (failures > 0) fail(failureMessage.toString());
        
    }

Parameterized TestCase Pattern


Parameterized TestCase Example

import junit.framework.*;
import junit.textui.TestRunner;

public class DataDrivenTest extends TestCase {

    private int numerator;
    private int denominator;
    private int result;
    
    public DataDrivenTest(int numerator, int denominator, int result) {
        super("testIntegrationGetProperNumerator");
        this.numerator = numerator;
        this.denominator = denominator;
        this.result = result;
    }
    
    public static TestSuite suite() {
        TestSuite result = new TestSuite();
        result.addTest(new DataDrivenTest(5, 3, 2));
        result.addTest(new DataDrivenTest(-5, 3, 2 ));
        result.addTest(new DataDrivenTest(0, 3, 0));
        result.addTest(new DataDrivenTest(6, 2, 0));
        return result;
    }
        
    public void testIntegrationGetProperNumerator() {
      Fraction f = new Fraction(numerator, denominator);
       assertEquals("Testing " +
       numerator + "/" + denominator, result, f.getProperNumerator());
    }
    
}

Autogeneration of Tests


Round Trip Tests


Exercise 9: Randomized Tests

Write a program that randomly generates a thousand different test, and uses round tripping to test multiplication, addition, subtraction, and division.


Answer 9: Data Driven Test


Object comparisons: asserting object equality


Object comparisons: asserting object identity


Asserting Truth


Asserting Falsity


Asserting Nullness

e.g. testing for memory leak:

    public void testMemoryLeak() throws InterruptedException {
        Fraction a = new Fraction(1, 2);
        WeakReference ref = new WeakReference(a);
        a = null;
        System.gc();
        System.gc();
        System.gc();
        Thread.sleep(1000);
        assertNull(ref.get());
    }

Asserting Non-Nullness

    public void testToString() {
        assertNotNull(list.toString());
    }

Deliberately failing a test


Tests that throw Exceptions


Testing exceptional conditions

    public void testZeroDenominator() {
      
        try {
            new Fraction(1, 0);
            fail("Allowed zero denominator");
        }
        catch (ArithmeticException success) {
            
            
        }
      
    }

Exercise 10: Exploring an API through Tests

Can you add null to a Fraction? What happens if you do? i.e.

Write a test that specifies the answer to this question, and then adjust the behavior to match it.


Answer 10: Testing Exceptional Conditions

Is this behavior properly documented? If not, it's a bug.

    public void testAddNull() {
        Fraction a = new Fraction(5, 3);
        try {
            Fraction f = null;
            a.add(null);
            fail("added null");
        }
        catch (NullPointerException success) {
            assertNotNull(success.getMessage());
        }
    }

Testing Files


Faking Input

  1. Write sample data onto a ByteArrayOutputStream.

  2. Convert to byte array

  3. Read from a ByteArrayInputStream


Testing Output


Exercise 11: Serialization

Write a test that serializes and deserializes a Fraction object; That is:

  1. Create a Fraction

  2. Create a ByteArrayOutputStream

  3. Chain an ObjectOutputStream to the ByteArrayOutputStream

  4. Write the Fraction object onto the ObjectOutputStream using writeObject()

  5. Close the ObjectOutputStream

  6. Get a byte array from the ByteArrayOutputStream using toByteArray()

  7. Make a ByteArrayInputStream from the byte array

  8. Chain an ObjectInputStream to the ByteArrayInputStream

  9. Read the object from the ObjectInputStream using readObject()

  10. Verify that the object is equal to but not the same as the original object


Answer 11: Testing I/O

    public void testSerialization() throws IOException, ClassNotFoundException {
        Fraction source = new Fraction(1, 2);
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        ObjectOutputStream oout = new ObjectOutputStream(out);
        oout.writeObject(source);
        oout.close();
        
        byte[] data = out.toByteArray();
        ByteArrayInputStream in = new ByteArrayInputStream(data);
        ObjectInputStream oin = new ObjectInputStream(in);
        Fraction result = (Fraction) oin.readObject();
        
        assertEquals(source, result);
        assertNotSame(source, result);
    }

Test Suites


Test Suites

Multiple test classes can be combined in a test suite class:

public class FunctionTests {
    
    public static Test suite() {
        
        TestSuite result = new TestSuite();
        result.addTest(new TestSuite(TranslateFunctionTest.class));
        result.addTest(new TestSuite(SubstringTest.class));
        result.addTest(new TestSuite(SubstringBeforeTest.class));
        result.addTest(new TestSuite(SubstringAfterTest.class));
        result.addTest(new TestSuite(LangTest.class));
        result.addTest(new TestSuite(LastTest.class));
        result.addTest(new TestSuite(ConcatTest.class));
        result.addTest(new TestSuite(ContainsTest.class));
        result.addTest(new TestSuite(StringLengthTest.class));
        result.addTest(new TestSuite(StartsWithTest.class));
        result.addTest(new TestSuite(CountTest.class));
        result.addTest(new TestSuite(LocalNameTest.class));
        result.addTest(new TestSuite(NameTest.class));
        result.addTest(new TestSuite(NamespaceURITest.class));
        result.addTest(new TestSuite(SumTest.class));
        result.addTest(new TestSuite(NumberTest.class));
        result.addTest(new TestSuite(RoundTest.class));
        result.addTest(new TestSuite(StringTest.class));
        result.addTest(new TestSuite(BooleanTest.class));
        result.addTest(new TestSuite(CeilingTest.class));
        result.addTest(new TestSuite(FloorTest.class));
        result.addTest(new TestSuite(IdTest.class));
        result.addTest(new TestSuite(TrueTest.class));
        result.addTest(new TestSuite(FalseTest.class));
        result.addTest(new TestSuite(NotTest.class));
        result.addTest(new TestSuite(NormalizeSpaceTest.class));
        return result;
        
    }
    
}

Test Suites

Multiple test methods can also be combined in a test suite class:


Test Suites Can Be Combined Into Larger Suites

public class JaxenTests {

    public static Test suite() {
        
        TestSuite result = new TestSuite();
        result.addTest(SAXPathTests.suite());
        result.addTest(FunctionTests.suite());
        result.addTest(CoreTests.suite());
        result.addTest(DOMTests.suite());
        result.addTest(JDOMTests.suite());
        result.addTest(DOM4JTests.suite());
        result.addTest(XOMTests.suite());
        result.addTest(JavaBeanTests.suite());
        result.addTest(PatternTests.suite());
        result.addTest(BaseTests.suite());
        result.addTest(HelpersTests.suite());
        result.addTest(ExprTests.suite());
        result.addTest(UtilTests.suite());
        return result;
        
    }
    
}

Testing Fields


Testing Protected Methods


Testing Abstract Classes


Testing Package Protected Methods


Testing Private Methods


Testing Interfaces


What to Test


What Not to Test


TestRunners


Main method for TestCase

Use the one of the test runners to run the current class:

    public static void main(String[] args) {
      junit.textui.TestRunner runner = new junit.textui.TestRunner();
      runner.run(FractionTest.class);
    }

Exercise 12: Adding a main method

Add a main method to your test class and use it to run the tests.


Answer 12: Adding a main method

    public static void main(String[] args) {
      TestRunner.run(FractionTest.class);
    }

TestListeners

public interface TestListener {

 void   addError(Test test, Throwable t);
 void   addFailure(Test test, AssertionFailedError t);
 void   endTest(Test test);
 void   startTest(Test test);
 
}

Writing Your Own TestListener


Writing Your Own TestRunner


Integrating Tests into the Build Process


Ant

<property name="test.outputFormat" value="xml"/>   

<target name="test" depends="compile" 
        description="Run JUnit tests using command line user interface">

    <junit printsummary="off" fork="yes">
       <classpath refid="test.class.path" />
       <formatter type="${test.outputFormat}" />
       <batchtest fork="yes" todir="${testoutput.dir}">
         <fileset dir="${build.src}">
           <include name="**/*Test.java" />
           <exclude name="**/pantry/*" />
           <exclude name="**/MegaTest.java" />
           <exclude name="**/benchmarks/*.java" />
           <exclude name="**/EBCDICTest.java" />
         </fileset>
      </batchtest>
    </junit>

    <junitreport todir="${testoutput.dir}">
      <fileset dir="${testoutput.dir}">
        <include name="TEST-*.xml" />
      </fileset>
      <report todir="${testoutput.dir}" />
    </junitreport>

</target>

Maven

  <build>

    <nagEmailAddress>dev@jaxen.codehaus.org</nagEmailAddress>
    <sourceDirectory>src/java/main</sourceDirectory>
    <unitTestSourceDirectory>src/java/test</unitTestSourceDirectory>

    <!-- Unit test classes -->

    <unitTest>
      <includes>
        <include>**/*Test.java</include>
      </includes>
      
      <excludes>
        <!-- currently broken -->
        <exclude>org/jaxen/jdom/XPathTest.java</exclude>
      </excludes>

    </unitTest>

  </build>
Sample HTML output

Excluding Tests from the JavaDoc


Test Suite Performance


Profiling tests


Unnecessary Initialization


Measuring Test Coverage


Testing Databases


Testing Web Servers and Services


To Learn More


Index | Cafe con Leche

Copyright 2005-2007 Elliotte Rusty Harold
elharo@metalab.unc.edu
Last Modified January 28, 2007