What's New in JUnit 4


What's New in JUnit 4

Elliotte Rusty Harold

Software Development Best Practices 2005

Tuesday, September 27, 2005

elharo@metalab.unc.edu

http://www.cafeaulait.org/


Unit Testing is Good for your Health

If I write too much code without tests, I feel physical symptoms, like tightness in my chest. One day it'll be a heart attack, but for now it's garden-variety anxiety. Of course, I'm a bit of a nut.

--J. B. Rainsberger on the junit mailing list, Friday, April 29, 2005


Benefits of Test Driven Development are Well Known


TDD does not require any particular framework


Enter JUnit 4

The theme of JUnit 4 is to encourage more developers to write more tests by further simplifying JUnit. To this end, we have followed NUnit's lead in identifying tests by annotation instead of subclassing and naming conventions.

--Kent Beck on the junit mailing list, June 14, 2005


JUnit 4 in one slide


A Typical JUnit 3 Test Case

import junit.framework.TestCase;

public class AdditionTest extends TestCase {

  private int x = 1;
  private int y = 1;
  
  public void testAddition() {
    int z = x + y;
    assertEquals(2, z);
  }

}

A JUnit 4 Test Case

import org.junit.Test;
import junit.framework.TestCase;

public class AdditionTest extends TestCase {

  private int x = 1;
  private int y = 1;
  
  @Test public void testAddition() {
    int z = x + y;
    assertEquals(2, z);
  }

}

Don't Have to Follow Naming Conventions

import org.junit.Test;
import junit.framework.TestCase;

public class AdditionTest extends TestCase {

  private int x = 1;
  private int y = 1;
  
  @Test public void additionTest() {
    int z = x + y;
    assertEquals(2, z);
  }

}

or

import org.junit.Test;
import junit.framework.TestCase;

public class AdditionTest extends TestCase {

  private int x = 1;
  private int y = 1;
  
  @Test public void addition() {
    int z = x + y;
    assertEquals(2, z);
  }

}

Don't Have to Extend TestCase


Static imports make this even nicer

import static org.junit.Assert.*;
import org.junit.Test;

public class AdditionTest {

  private int x = 1;
  private int y = 1;
  
  @Test public void addition() {
    int z = x + y;
    assertEquals(2, z);
  }

}

Parameterized Test Cases

import org.junit.*;
import static org.junit.Assert.*;

public class AdditionTest {

  @Parameters public static int[][] data 
    = new int[][] {
       {0, 0, 0}, 
       {1, 1, 0}, 
       {2, 1, 1}, 
       {3, 2, 1}, 
       {4, 3, 1}, 
       {5, 5, 0}, 
       {6, 8, -2}
      };
		
  @Parameter(0) public int expected;
  @Parameter(1) public int input1; 
  @Parameter(2) public int input2; 
		
  @Test public void testAddition() {
    assertEquals(expected, add(input1, input2));
  }

  private int add(int m, int n) {
    return m+n;
  }
  
}

Is testAddition really a unit test any more?


Skipping a Test

    @Test public void setInternalDTDSubsetWithRelativeURLAndCrimson() 
      throws ParsingException, IOException {
        
        XMLReader crimson;
        try {
            crimson = XMLReaderFactory.createXMLReader(
              "org.apache.crimson.parser.XMLReaderImpl");
        } 
        catch (SAXException ex) {
            skip();
        }
        Builder builder = new Builder(crimson);
        Document doc = builder.build("data/outer21.xml");
        String subset = doc.getDocType().getInternalDTDSubset();
        assertEquals(subset, subset.indexOf("file:/"), -1);

    }

Initialization


@Before


Multiple @Before

@Before protected void findTestDataDirectory() {
    inputDir = new File("data");
    inputDir = new File(inputDir, "xslt");
    inputDir = new File(inputDir, "input");
}
    
@Before protected void redirectStderr() {
    System.setErr(new PrintStream(new ByteArrayOutputStream()));
}

Cleanup


@After


Class scope initialization and cleanup


Testing Exceptions


Ignoring Tests


Timeouts


Asserting Array Equality

public static void assertEquals(String message, Object[] expected, Object[] actual)
public static void assertEquals(String message, Object[] expected, Object[] actual)


Building and running JUnit 4


Compatible


Implementation Details


Failures

package org.junit.runner;

public class Failure {

    protected Throwable fThrownException;

    public Failure(Throwable thrownException);
    
    public Throwable getException();
    public String    getTrace();
    public String    getMessage();
    public String    getTestHeader();
    public boolean   isTestFailure();
    public String    toString();
 
}

TestListener

package org.junit.runner;

public interface TestListener {

    void testRunStarted(int testCount);
    void testRunFinished(Runner runner); //TODO: Something is wrong here... 
                                         //Should take an event anyway  
    void testStarted(Object test, String name);
    void testFinished(Object test, String name);
    void testFailure(Failure failure);
    void testIgnored(Object method);

}

XMLTestListener Example

import java.io.*;
import java.lang.reflect.Method;
import org.junit.runner.*;

public class XMLTestListener implements TestListener {

    private Writer out;
    
    public XMLTestListener(OutputStream out) {
        try {
            this.out = new BufferedWriter(new OutputStreamWriter(out, "UTF-8"));
        }
        catch (UnsupportedEncodingException e) {
            throw new RuntimeException("Broken VM does not support UTF-8");
        }
    }
    
    public void testRunStarted(int testCount) {
        try {
            out.write("<?xml version='1.0' encoding='UTF-8'?>\r\n");
            out.write("<testsuite>\r\n");
            out.write("  <properties>\r\n");
            // enumerate properties????
            out.write("  </properties>\r\n");
        }
        catch (IOException ex) {
            throw new RuntimeException(ex);
        }
    }

    public void testRunFinished(Runner runner) {

        double time = runner.getRunTime() / 1000.0; // seconds
        
        try {
            out.write("</testsuite>\r\n");
            out.flush();
        }
        catch (IOException ex) {
            throw new RuntimeException(ex);
        }
        
    }

    public void testStarted(Object test, String name) {

        try {
            out.write("  <testcase classname='" 
                    + test.getClass().getName()
                    + "' name='" + name + ">");
        }
        catch (IOException ex) {
            throw new RuntimeException(ex);
        }
        
    }

    public void testFinished(Object test, String name) {

        try {
            out.write("  </test>\r\n");
        }
        catch (IOException ex) {
            throw new RuntimeException(ex);
        }        
    }

    public void testFailure(Failure failure) {

        try {
            out.write("  <failure>");
            out.write(escape(failure.getMessage()));
            out.write("  </failure>\r\n");
        }
        catch (IOException ex) {
            throw new RuntimeException(ex);
        }
        
    }

    private String escape(String message) {

        String result = message.replaceAll("&", "&amp;");
        result = result.replaceAll("<", "&lt;");
        result.replaceAll(">", "&gt;");
        return result;
    }

    public void testIgnored(Method method) {

        // Why a Method instead of a Ignored????
        try {
            out.write("  <ignored>");
            out.write(escape(method.getName()));
            out.write("  </ignored>\r\n");
        }
        catch (IOException ex) {
            throw new RuntimeException(ex);
        }
        
    }

}

Runner

package org.junit.runner;

public class Runner implements TestNotifier {

    public void run(Class... testClasses);
    public void run(junit.framework.Test test);

    public int           getRunCount();
    public int           getFailureCount() ;
    public long          getRunTime();
    public List<Failure> getFailures();
    public int           getIgnoreCount();
    public boolean       wasSuccessful();
    
    public void addListener(TestListener listener);
    private List<TestListener> getListeners();
    
    public void fireTestStarted(Object test, String name);
    public void fireTestFailure(Failure failure);
    public void fireTestIgnored(Method method);
    public void fireTestFinished(Object test, String name) ;

}

Using Runner to Drive a TestListener

runner.addListener(new XMLTestListener(System.out));


What's Missing


To Learn More


Index | Cafe con Leche

Copyright 2005 Elliotte Rusty Harold
elharo@metalab.unc.edu
Last Modified September 27, 2005