The Haskell library QuickCheck allows programmers to specify properties of a function that should hold true for some large (potentially infinite) set of possible arguments to the function, then executes the function using lots of random arguments to see whether the property holds up against them.
One JUnit answer to function properties is the notion of theories. Programmers write parameterized tests marked as theories, run using a special test runner.
import org.junit.experimental.theories.*; import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; import static org.junit.Assume.*; // Imagining the existence of classes Money and Account... @RunWith(Theories.class) public class Accounts { @Theory public void withdrawingReducesBalance( Money originalBalance, Money withdrawalAmount) { assumeThat(originalBalance, greaterThan(Money.NONE)); assumeThat( withdrawalAmount, allOf(greaterThan(Money.NONE), lessThan(originalBalance))); Account account = new Account(originalBalance); account.withdraw(withdrawalAmount); assertEquals( originalBalance.minus(withdrawalAmount), account.balance()); } }
TDD/BDD builds up designs example by example. The resulting test suites give programmers confidence that their code works for the examples they thought of. Theories offer a means to express statements about code that should hold for an entire domain of inputs, not just a handful of examples, and to validate those statements against lots of randomly generated inputs.
junit-quickcheck began its life as a library that supplies JUnit theories with random values with which to test the validity of the theories.
In this original incarnation, junit-quickcheck leveraged the ParameterSupplier feature of the JUnit theories machinery, via the annotation @ForAll marking theory parameters.
By default, when the Theories runner executes a theory, it attempts to scrape data points off the theory class to feed to the theories. Data points come from static fields or methods annotated with @DataPoint (single value) or @DataPoints (array/iterable of values). The Theories runner feeds all combinations of data points of types matching a theory’s parameters to the theory for execution.
Marking a theory parameter with an annotation that is itself annotated with @ParametersSuppliedBy tells the Theories runner to ask a ParameterSupplier for values for the theory parameter instead. This is how junit-quickcheck interacted with the Theories runner – @ForAll told the runner to use junit-quickcheck’s ParameterSupplier rather than the DataPoint-oriented one.
The Theories runner executes a theory method once for every combination of values for theory parameters. This means that for a two-parameter theory method, the Theories runner instantiates the theory class and executes the theory method 10,000 times (100 * 100).
@RunWith(Theories.class) public class GeographyTheories { @Theory public void northernHemisphere( @ForAll @InRange(min = "-90", max = "90") BigDecimal latitude, @ForAll @InRange(min = "-180", max = "180") BigDecimal longitude) { assumeThat(latitude, greaterThan(BigDecimal.ZERO)); assertTrue(Earth.isInNorthernHemisphere(latitude, longitude)); } }
This led to mitigation strategies such as:
public class Coordinate { // ... } public class Coordinates extends Generator<Coordinate> { // ... } @RunWith(Theories.class) public class GeographyTheories { @Theory public void northernHemisphere( @ForAll @From(Coordinates.class) Coordinate c) { assumeThat(c.latitude(), greaterThan(BigDecimal.ZERO)); assertTrue(c.inNorthernHemisphere()); } }
@RunWith(Theories.class) public class ThreeDimensionalSpaceTheories { public static class Point { public double x, y, z; } @Theory public void originDistance( @ForAll @From(Fields.class) Point p) { assertEquals( Math.sqrt(p.x * p.x + p.y * p.y + p.z * p.z), Space.distanceFromOrigin(p.x, p.y, p.z)); } }
@RunWith(Theories.class) public class GeographyTheories { public static class Coordinate { private final BigDecimal latitude, longitude; public Coordinate( @InRange(min = "-90", max = "90") BigDecimal latitude, @InRange(min = "-180", max = "180") BigDecimal longitude) { this.latitude = latitude; this.longitude = longitude; } // ... } @Theory public void northernHemisphere( @ForAll @From(Ctor.class) Coordinate c) { // ... } }