CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-junit--junit

JUnit is a unit testing framework for Java, created by Erich Gamma and Kent Beck.

Overview
Eval results
Files

theories.mddocs/

Theories

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.

Capabilities

Theory Annotation

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());
    }
}

DataPoint Annotation

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);
    }
}

DataPoints Annotation

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);
    }
}

FromDataPoints Annotation

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());
    }
}

Theories Runner

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);
    }
}

ParameterSupplier

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);
    }
}

ParametersSuppliedBy Annotation

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();
}

TestedOn Annotation

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);
    }
}

Advanced Theory Patterns

Using Assumptions to Constrain Theories

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]);
    }
}

Combining Theories with Regular Tests

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);
    }
}

Complex Object Theories

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());
    }
}

Types

/**
 * 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);
}

Theories vs Parameterized Tests

AspectTheoriesParameterized Tests
PurposeProperty-based testingData-driven testing
Data definitionFlexible data pointsExplicit parameter sets
CombinationsAutomatic combinationsManual specification
AssumptionsUse assumeXxx to skipNot available
Use caseGeneral propertiesSpecific test cases
Runner@RunWith(Theories.class)@RunWith(Parameterized.class)
Failure reportingShows failing combinationShows failing parameters

When to use Theories:

  • Testing general properties that should hold for all inputs
  • Exploring edge cases automatically
  • Property-based testing approach
  • When you want automatic parameter combinations

When to use Parameterized:

  • Testing specific known scenarios
  • Explicit test case data
  • When each parameter set is independent
  • More readable test output needed

Install with Tessl CLI

npx tessl i tessl/maven-junit--junit

docs

annotations.md

assertions.md

assumptions.md

categories.md

index.md

matchers.md

rules.md

standard-runners.md

test-runners.md

theories.md

tile.json