public class Y2KChecker { public void check() throws Y2KException { Calendar calendar = Calendar.getInstance(); calendar.set(2000, 1, 1); Date date = calendar.getTime(); if (date.getTime() == System.currentTimeMillis()) { throw new Y2KException("Y2K Bug!"); } } public static class Y2KException extends Exception { public Y2KException(String reason) { super(reason); } } }Getting this code to throw the Y2KException is difficult without some sort of test double injection framework or dependency injection/inversion of control abstraction. To illustrate the latter, we could refactor the code into something along these lines:
public class Y2KChecker {This code is now more testable. To trigger the Y2KException, you could write a test case that looks like this:
public static class Clock {
public long currentTimeMillis() {
return System.currentTimeMillis();
}
}
private final Clock clock;
public Y2KChecker(Clock clock) {
this.clock = clock;
}
public void check() throws Y2KException { Calendar calendar = Calendar.getInstance(); calendar.set(2000, 1, 1); Date date = calendar.getTime(); if (date.getTime() == clock.currentTimeMillis()) { throw new Y2KException("Y2K Bug!"); } } public static class Y2KException extends Exception { public Y2KException(String reason) { super(reason); } }
}
public class Test {The problem with this approach, however, is that implementational details of the class under test have to be exposed in its public API in order to test it. This problem is shared with most dependency injection/inversion of control approaches to unit testing.
@Test(expectedExceptions = Y2KChecker.Y2KException.class)
public void testCheck() {
Y2KChecker checker = new Y2KChecker(new Y2KChecker.Clock() {
private static final long Y2K_MILLIS = 949433850262L;
public long currentTimeMillis() {
return Y2K_MILLIS;
}
}
checker.check();
}
}
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.duderino.injection</groupId> <artifactId>injection</artifactId> <name>Injection</name> <version>1.0-SNAPSHOT</version> <packaging>jar</packaging> <url>https://github.com/duderino/injection</url> <repositories> <repository> <id>jmockit-svn</id> <url>http://jmockit.googlecode.com/svn/maven-repo</url> <releases> <checksumPolicy>ignore</checksumPolicy> </releases> </repository> </repositories> <dependencies> <dependency> <groupId>mockit</groupId> <artifactId>jmockit</artifactId> <version>0.999.10</version> <scope>test</scope> </dependency> <dependency> <groupId>org.testng</groupId> <artifactId>testng</artifactId> <version>6.1.1</version> <scope>test</scope> </dependency> </dependencies> <build> <defaultGoal>test</defaultGoal> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>2.9</version> <configuration> <useFile>false</useFile> <suiteXmlFiles> <suiteXmlFile>src/test/resources/org/duderino/injection/testng.xml</suiteXmlFile> </suiteXmlFiles>Providing you put your code and other resources in the standard directories, Maven will take care of the rest.
<argLine>-javaagent:"${settings.localRepository}"/mockit/jmockit/0.999.10/jmockit-0.999.10.jar</argLine>
</configuration> </plugin> </plugins> </build> </project>
WARNING: JMockit was initialized on demand, which may cause certain tests to fail; please check the documentation for better ways to get it initialized.As you add unit tests, add them to the testng.xml file. Our testng.xml file looks like this:
<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd" > <suite name="Injection" verbose="1" parallel="false" thread-count="1"> <test name="unit"> <classes> <class name="org.duderino.injection.jmockit.basic.Y2KCheckerTest"/> </classes> </test> </suite>
import mockit.Mock; import mockit.Mockit; import org.testng.annotations.Test; public class Y2KCheckerTest { private static class MockSystem { private static final long Y2K_MILLIS = 949433850262L; @Mock public long currentTimeMillis() { return Y2K_MILLIS; } } @Test(expectedExceptions = Y2KChecker.Y2KException.class) public void testCheck() throws Exception { Y2KChecker checker = new Y2KChecker(); Mockit.setUpMock(System.class, MockSystem.class); checker.check(); } }The first thing to realize is that we didn't have to modify the Y2KChecker class in order to inject our test double. We didn't have to create a wrapper for System.currentTimeMillis() either. Instead we created a MockSystem test double with a replacement currentTimeMillis() method. Note the @Mock annotation on the replacement currentTimeMillis() method.
<build> <defaultGoal>test</defaultGoal> ... </build>... in our Maven POM, all we have to do is type 'mvn' in our top-level directory and Maven will run the test suite after completing all its prerequisites (e.g., compiling both the code under test and the test cases):
$ mvn
[INFO] Scanning for projects...
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Building Injection 1.0-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] --- maven-resources-plugin:2.4.3:resources (default-resources) @ injection ---
[WARNING] Using platform encoding (MacRoman actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] skip non existing resourceDirectory /Users/blattj/dev/injection/src/main/resources
[INFO]
[INFO] --- maven-compiler-plugin:2.3.2:compile (default-compile) @ injection ---
[INFO] No sources to compile
[INFO]
[INFO] --- maven-resources-plugin:2.4.3:testResources (default-testResources) @ injection ---
[WARNING] Using platform encoding (MacRoman actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] Copying 1 resource
[INFO]
[INFO] --- maven-compiler-plugin:2.3.2:testCompile (default-testCompile) @ injection ---
[INFO] Nothing to compile - all classes are up to date
[INFO]
[INFO] --- maven-surefire-plugin:2.9:test (default-test) @ injection ---
[INFO] Surefire report directory: /Users/blattj/dev/injection/target/surefire-reports
-------------------------------------------------------
T E S T S
-------------------------------------------------------
Running TestSuite
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.798 sec
Results :
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 3.205s
[INFO] Finished at: Wed Nov 23 11:49:47 PST 2011
[INFO] Final Memory: 4M/81M
[INFO] ------------------------------------------------------------------------