JUnit Theories
Theories allows one to write tests that apply to a (potentially infinite) set of data points rather than having to recreate the same test multiple times with different data or creating one test and iterating through your own collection of data values.
Here is an example:
Messaging client
First off is the interface for the messaging client. It has two lifecycle methods, start and stop which are self-explanatory, as well as methods for reading and sending a message which for brevity are just strings.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public interface IMessagingClient { | |
public void start(); | |
public void stop(); | |
public String read(); | |
public void send(String message); | |
} |
The following enum represents the status of the JMS connection. As can be seen there are numerous states so it would be cumbersome to work out all the possible combinations for each status transition. This is where Theories become so useful. More of that later.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public enum Status { | |
STARTED, STOPPED, FAILED, RESUMED, PAUSED | |
} |
This class is used to restart a message client when it recognizes a status transition from FAILED to STARTED.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import org.apache.commons.lang.Validate; | |
public class Restarter { | |
private final IMessagingClient messagingClient; | |
private Status status; | |
public Restarter(IMessagingClient client) { | |
Validate.notNull(client, "Messaging client must be specified"); | |
this.messagingClient = client; | |
} | |
public void restart(Status newStatus) { | |
if (isTransistionFromFailureToStarted(newStatus)) { | |
messagingClient.stop(); | |
messagingClient.start(); | |
} | |
} | |
private boolean isTransistionFromFailureToStarted(Status newStatus) { | |
if (null == status) { | |
status = newStatus; | |
return false; | |
} | |
if (Status.FAILED == newStatus) { | |
status = newStatus; | |
} | |
boolean isTransitionFromFailure = (Status.FAILED == status) && (Status.STARTED == newStatus); | |
if (isTransitionFromFailure) { | |
status = newStatus; | |
} | |
return isTransitionFromFailure; | |
} | |
} |
Finally here's the test class. Notice I'm using mock objects here using Mockito. As I'm programming to the interface of the messaging client not its implementation, I can make use of a mock and verify its behaviour. Presently as of JUnit 4.8, theories is still within an experimental package.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import static org.mockito.Mockito.mock; | |
import static org.mockito.Mockito.times; | |
import static org.mockito.Mockito.verify; | |
import static org.mockito.Mockito.verifyZeroInteractions; | |
import org.junit.Before; | |
import org.junit.experimental.theories.DataPoints; | |
import org.junit.experimental.theories.Theories; | |
import org.junit.experimental.theories.Theory; | |
import org.junit.runner.RunWith; | |
@RunWith(Theories.class) | |
public class TheoryTest { | |
public static @DataPoints Status[] values = Status.values(); | |
private IMessagingClient messageClient; | |
private Restarter restarter; | |
@Before | |
public void setUp() throws Exception { | |
messageClient = mock(IMessagingClient.class); | |
restarter = new Restarter(messageClient); | |
} | |
@Theory | |
public void shouldCauseMessagingClientRestartIfFailedToStartedTransition(Status currentStatus, Status newStatus){ | |
System.out.println(currentStatus + " : " + newStatus); | |
restarter.restart(currentStatus); | |
verifyZeroInteractions(messageClient); | |
restarter.restart(newStatus); | |
if (Status.FAILED == currentStatus && Status.STARTED == newStatus) { | |
verify(messageClient, times(1)).stop(); | |
verify(messageClient, times(1)).start(); | |
} | |
} | |
@Theory | |
public void shouldNotCauseRestartIfNotFailedToStartedTransition(Status currentStatus, Status newStatus) { | |
if (Status.FAILED != currentStatus && Status.STARTED != newStatus) { | |
verify(messageClient, times(0)).stop(); | |
verify(messageClient, times(0)).start(); | |
} | |
} | |
} |
When you run the tests you'll find the all the combinations of the Status enums are passed to the test methods in question as shown below:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
STARTED : STARTED | |
STARTED : STOPPED | |
STARTED : FAILED | |
STARTED : RESUMED | |
STARTED : PAUSED | |
STOPPED : STARTED | |
STOPPED : STOPPED | |
STOPPED : FAILED | |
STOPPED : RESUMED | |
STOPPED : PAUSED | |
FAILED : STARTED | |
FAILED : STOPPED | |
FAILED : FAILED | |
FAILED : RESUMED | |
FAILED : PAUSED | |
RESUMED : STARTED | |
RESUMED : STOPPED | |
RESUMED : FAILED | |
RESUMED : RESUMED | |
RESUMED : PAUSED | |
PAUSED : STARTED | |
PAUSED : STOPPED | |
PAUSED : FAILED | |
PAUSED : RESUMED | |
PAUSED : PAUSED |
Conclusion
The use of theories allows tests to be devised that cover all possible combinations of data. This is in contrast to parameterized tests where the dataset to be passed to a test is strictly defined and the onus is on the developer to work out what data is needed for a particular range of tests. Each approach can be used in different situations as dictated by your requirements.