JUnit is a unit testing framework for Java, created by Erich Gamma and Kent Beck.
Theories is an experimental feature for property-based testing where tests are theories to be proven against multiple data points. Unlike parameterized tests that explicitly define test data, theories use data points that can be automatically discovered and combined.
Marks a method as a theory instead of a regular test. Theories are executed once for each valid combination of parameter values.
/**
* Marks a method as a theory
* Theories are run with all possible parameter combinations from data points
* @param nullsAccepted - Whether null values should be accepted as parameters (default true)
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Theory {
boolean nullsAccepted() default true;
}Usage Examples:
import org.junit.experimental.theories.Theories;
import org.junit.experimental.theories.Theory;
import org.junit.experimental.theories.DataPoint;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
import static org.junit.Assume.*;
@RunWith(Theories.class)
public class StringTheoryTest {
@DataPoint
public static String EMPTY = "";
@DataPoint
public static String SHORT = "a";
@DataPoint
public static String LONG = "hello world";
@Theory
public void lengthIsNonNegative(String str) {
assertTrue(str.length() >= 0);
}
@Theory
public void concatenationIncreasesLength(String s1, String s2) {
int originalLength = s1.length() + s2.length();
String concatenated = s1 + s2;
assertEquals(originalLength, concatenated.length());
}
}Marks a field or method as providing a data point for theories. Theories will be executed with each data point.
/**
* Marks a public static field or method as a data point source
* Each data point will be used as parameter for theories
* @param value - Optional array of names to filter data points
* @param ignoredExceptions - Exceptions to ignore when using this data point
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD})
public @interface DataPoint {
String[] value() default {};
Class<? extends Throwable>[] ignoredExceptions() default {};
}Usage Examples:
import org.junit.experimental.theories.Theories;
import org.junit.experimental.theories.Theory;
import org.junit.experimental.theories.DataPoint;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
@RunWith(Theories.class)
public class NumberTheoryTest {
@DataPoint
public static int ZERO = 0;
@DataPoint
public static int POSITIVE = 5;
@DataPoint
public static int NEGATIVE = -5;
@DataPoint
public static int MAX = Integer.MAX_VALUE;
@Theory
public void additionIsCommutative(int a, int b) {
assumeTrue(canAdd(a, b)); // Skip if overflow would occur
assertEquals(a + b, b + a);
}
@Theory
public void absoluteValueIsNonNegative(int x) {
assertTrue(Math.abs(x) >= 0);
}
private boolean canAdd(int a, int b) {
try {
Math.addExact(a, b);
return true;
} catch (ArithmeticException e) {
return false;
}
}
}
// Data point methods
@RunWith(Theories.class)
public class MethodDataPointTest {
@DataPoint
public static String generateEmptyString() {
return "";
}
@DataPoint
public static String generateShortString() {
return "test";
}
@Theory
public void everyStringHasDefinedLength(String str) {
assertNotNull(str);
assertTrue(str.length() >= 0);
}
}Marks a field or method as providing multiple data points as an array or iterable.
/**
* Marks a public static field or method as providing multiple data points
* The field must be an array or Iterable
* @param value - Optional array of names to filter data points
* @param ignoredExceptions - Exceptions to ignore when using these data points
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD})
public @interface DataPoints {
String[] value() default {};
Class<? extends Throwable>[] ignoredExceptions() default {};
}Usage Examples:
import org.junit.experimental.theories.Theories;
import org.junit.experimental.theories.Theory;
import org.junit.experimental.theories.DataPoints;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
import static org.junit.Assume.*;
@RunWith(Theories.class)
public class CollectionTheoryTest {
@DataPoints
public static int[] NUMBERS = {0, 1, -1, 5, -5, 100};
@DataPoints
public static String[] STRINGS = {"", "a", "hello", "world"};
@Theory
public void multiplicationByZeroIsZero(int x) {
assertEquals(0, x * 0);
}
@Theory
public void stringConcatenationIsNotNull(String s1, String s2) {
assertNotNull(s1 + s2);
}
}
// DataPoints with List
@RunWith(Theories.class)
public class ListDataPointsTest {
@DataPoints
public static List<Integer> PRIMES = Arrays.asList(2, 3, 5, 7, 11, 13);
@Theory
public void primesAreGreaterThanOne(int prime) {
assertTrue(prime > 1);
}
@Theory
public void productOfPrimesIsComposite(int p1, int p2) {
assumeTrue(p1 != p2);
int product = p1 * p2;
assertTrue(product > p1);
assertTrue(product > p2);
}
}References named data points to be used for a specific parameter. Allows selective use of data points.
/**
* Indicates which named data points should be used for a parameter
* @param value - Name of the data point group to use
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface FromDataPoints {
String value();
}Usage Examples:
import org.junit.experimental.theories.Theories;
import org.junit.experimental.theories.Theory;
import org.junit.experimental.theories.DataPoints;
import org.junit.experimental.theories.FromDataPoints;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
@RunWith(Theories.class)
public class NamedDataPointsTest {
@DataPoints("positiveNumbers")
public static int[] POSITIVE = {1, 2, 5, 10, 100};
@DataPoints("negativeNumbers")
public static int[] NEGATIVE = {-1, -2, -5, -10, -100};
@DataPoints("validStrings")
public static String[] VALID = {"hello", "world", "test"};
@DataPoints("emptyStrings")
public static String[] EMPTY = {"", null};
@Theory
public void positiveTimesPositiveIsPositive(
@FromDataPoints("positiveNumbers") int a,
@FromDataPoints("positiveNumbers") int b
) {
assertTrue(a * b > 0);
}
@Theory
public void positiveTimesNegativeIsNegative(
@FromDataPoints("positiveNumbers") int positive,
@FromDataPoints("negativeNumbers") int negative
) {
assertTrue(positive * negative < 0);
}
@Theory
public void validStringsAreNotEmpty(
@FromDataPoints("validStrings") String str
) {
assertFalse(str.isEmpty());
}
}The runner that executes theories with all valid parameter combinations.
/**
* Runner for executing theories
* Runs theory methods with all possible parameter combinations
*/
public class Theories extends BlockJUnit4ClassRunner {
/**
* Creates Theories runner
* @param klass - Test class containing theories
* @throws InitializationError if initialization fails
*/
public Theories(Class<?> klass) throws InitializationError;
}Usage Examples:
import org.junit.experimental.theories.Theories;
import org.junit.experimental.theories.Theory;
import org.junit.experimental.theories.DataPoint;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
@RunWith(Theories.class)
public class MathTheories {
@DataPoint public static int ZERO = 0;
@DataPoint public static int ONE = 1;
@DataPoint public static int TWO = 2;
@DataPoint public static int MINUS_ONE = -1;
@Theory
public void additionIsCommutative(int a, int b) {
assertEquals(a + b, b + a);
}
@Theory
public void addingZeroDoesNotChange(int x) {
assertEquals(x, x + 0);
assertEquals(x, 0 + x);
}
@Theory
public void multiplyingByOneDoesNotChange(int x) {
assertEquals(x, x * 1);
assertEquals(x, 1 * x);
}
}Base class for custom parameter suppliers. Allows programmatic generation of data points.
/**
* Supplies values for theory parameters
* Extend this to create custom data point sources
*/
public abstract class ParameterSupplier {
/**
* Get potential values for a parameter
* @param signature - Parameter signature
* @return List of potential assignments
*/
public abstract List<PotentialAssignment> getValueSources(ParameterSignature signature) throws Throwable;
}Usage Examples:
import org.junit.experimental.theories.ParameterSupplier;
import org.junit.experimental.theories.PotentialAssignment;
import org.junit.experimental.theories.ParameterSignature;
import org.junit.experimental.theories.ParametersSuppliedBy;
import org.junit.experimental.theories.Theories;
import org.junit.experimental.theories.Theory;
import org.junit.runner.RunWith;
import java.util.ArrayList;
import java.util.List;
import static org.junit.Assert.*;
// Custom supplier for ranges
public class BetweenSupplier extends ParameterSupplier {
@Override
public List<PotentialAssignment> getValueSources(ParameterSignature sig) {
Between annotation = sig.getAnnotation(Between.class);
List<PotentialAssignment> values = new ArrayList<>();
for (int i = annotation.first(); i <= annotation.last(); i++) {
values.add(PotentialAssignment.forValue(String.valueOf(i), i));
}
return values;
}
}
// Custom annotation
@Retention(RetentionPolicy.RUNTIME)
@ParametersSuppliedBy(BetweenSupplier.class)
public @interface Between {
int first();
int last();
}
// Usage
@RunWith(Theories.class)
public class RangeTheoryTest {
@Theory
public void numbersInRangeAreValid(@Between(first = 1, last = 10) int x) {
assertTrue(x >= 1 && x <= 10);
}
@Theory
public void sumOfSmallNumbersIsSmall(
@Between(first = 1, last = 5) int a,
@Between(first = 1, last = 5) int b
) {
assertTrue(a + b <= 10);
}
}Specifies a custom parameter supplier for a parameter.
/**
* Indicates which ParameterSupplier should provide values
* @param value - ParameterSupplier class
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.ANNOTATION_TYPE, ElementType.PARAMETER})
public @interface ParametersSuppliedBy {
Class<? extends ParameterSupplier> value();
}Built-in annotation for specifying integer values to test on.
/**
* Specifies integer values to test a parameter with
* @param ints - Array of integer values
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
@ParametersSuppliedBy(TestedOnSupplier.class)
public @interface TestedOn {
int[] ints();
}Usage Examples:
import org.junit.experimental.theories.Theories;
import org.junit.experimental.theories.Theory;
import org.junit.experimental.theories.suppliers.TestedOn;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
@RunWith(Theories.class)
public class TestedOnExample {
@Theory
public void multiplyingByOneDoesNotChange(
@TestedOn(ints = {0, 1, 2, -1, -5, 100}) int x
) {
assertEquals(x, x * 1);
}
@Theory
public void squareIsNonNegative(
@TestedOn(ints = {0, 1, 2, 5, -1, -5}) int x
) {
assertTrue(x * x >= 0);
}
@Theory
public void divisionByPowerOfTwo(
@TestedOn(ints = {0, 16, 32, 64, 128}) int x
) {
assertTrue(x % 2 == 0);
assertTrue(x / 2 <= x);
}
}import org.junit.experimental.theories.Theories;
import org.junit.experimental.theories.Theory;
import org.junit.experimental.theories.DataPoints;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
import static org.junit.Assume.*;
@RunWith(Theories.class)
public class ConstrainedTheories {
@DataPoints
public static int[] NUMBERS = {-10, -1, 0, 1, 10, 100};
@Theory
public void divisionWorks(int a, int b) {
assumeTrue(b != 0); // Skip when b is zero
int result = a / b;
assertEquals(a, result * b + (a % b));
}
@Theory
public void squareRootOfSquareIsOriginal(int x) {
assumeTrue(x >= 0); // Only test non-negative numbers
double sqrt = Math.sqrt(x * x);
assertEquals(x, sqrt, 0.0001);
}
@Theory
public void sortedPairIsOrdered(int a, int b) {
assumeTrue(a <= b); // Only test when a <= b
int[] sorted = sort(a, b);
assertTrue(sorted[0] <= sorted[1]);
}
}import org.junit.experimental.theories.Theories;
import org.junit.experimental.theories.Theory;
import org.junit.experimental.theories.DataPoints;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
@RunWith(Theories.class)
public class MixedTests {
@DataPoints
public static int[] NUMBERS = {0, 1, 2, -1};
// Regular test - runs once
@Test
public void testSpecificCase() {
assertEquals(4, 2 + 2);
}
// Theory - runs for all data point combinations
@Theory
public void additionIsCommutative(int a, int b) {
assertEquals(a + b, b + a);
}
// Another regular test
@Test
public void testAnotherCase() {
assertTrue(true);
}
}import org.junit.experimental.theories.Theories;
import org.junit.experimental.theories.Theory;
import org.junit.experimental.theories.DataPoints;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
@RunWith(Theories.class)
public class ComplexObjectTheories {
public static class User {
String name;
int age;
User(String name, int age) {
this.name = name;
this.age = age;
}
}
@DataPoints
public static User[] USERS = {
new User("Alice", 25),
new User("Bob", 30),
new User("Charlie", 35)
};
@DataPoints
public static String[] NAMES = {"Alice", "Bob", "Charlie", "David"};
@Theory
public void userNamesAreNotNull(User user) {
assertNotNull(user.name);
}
@Theory
public void agesArePositive(User user) {
assertTrue(user.age > 0);
}
@Theory
public void namesAreNotEmpty(String name) {
assertFalse(name.isEmpty());
}
}/**
* Represents a potential parameter value assignment
*/
public class PotentialAssignment {
/**
* Create assignment with value
* @param name - Description of value
* @param value - The value
* @return PotentialAssignment
*/
public static PotentialAssignment forValue(String name, Object value);
/**
* Get the value
* @return Value object
*/
public Object getValue() throws CouldNotGenerateValueException;
/**
* Get description
* @return Description string
*/
public String getDescription();
}
/**
* Describes a parameter's type and annotations
*/
public class ParameterSignature {
/**
* Get parameter type
* @return Parameter class
*/
public Class<?> getType();
/**
* Get parameter annotations
* @return List of annotations
*/
public List<Annotation> getAnnotations();
/**
* Get specific annotation
* @param annotationType - Annotation class
* @return Annotation or null
*/
public <T extends Annotation> T getAnnotation(Class<T> annotationType);
/**
* Check if parameter has annotation
* @param type - Annotation class
* @return true if annotation present
*/
public boolean hasAnnotation(Class<? extends Annotation> type);
/**
* Get parameter name
* @return Parameter name
*/
public String getName();
/**
* Check if type can accept value
* @param value - Value to check
* @return true if compatible
*/
public boolean canAcceptValue(Object value);
/**
* Check if type can potentially accept another type
* @param type - Type to check
* @return true if potentially compatible
*/
public boolean canPotentiallyAcceptType(Class<?> type);
}
/**
* Thrown when parameter value cannot be generated
*/
public class CouldNotGenerateValueException extends Exception {
public CouldNotGenerateValueException();
public CouldNotGenerateValueException(Throwable cause);
}
/**
* Exception indicating parameterized assertion failure
*/
public class ParameterizedAssertionError extends AssertionError {
public ParameterizedAssertionError(Throwable targetException, String methodName, Object... params);
}| Aspect | Theories | Parameterized Tests |
|---|---|---|
| Purpose | Property-based testing | Data-driven testing |
| Data definition | Flexible data points | Explicit parameter sets |
| Combinations | Automatic combinations | Manual specification |
| Assumptions | Use assumeXxx to skip | Not available |
| Use case | General properties | Specific test cases |
| Runner | @RunWith(Theories.class) | @RunWith(Parameterized.class) |
| Failure reporting | Shows failing combination | Shows failing parameters |
When to use Theories:
When to use Parameterized:
Install with Tessl CLI
npx tessl i tessl/maven-junit--junit