0
# Theories
1
2
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.
3
4
## Capabilities
5
6
### Theory Annotation
7
8
Marks a method as a theory instead of a regular test. Theories are executed once for each valid combination of parameter values.
9
10
```java { .api }
11
/**
12
* Marks a method as a theory
13
* Theories are run with all possible parameter combinations from data points
14
* @param nullsAccepted - Whether null values should be accepted as parameters (default true)
15
*/
16
@Retention(RetentionPolicy.RUNTIME)
17
@Target(ElementType.METHOD)
18
public @interface Theory {
19
boolean nullsAccepted() default true;
20
}
21
```
22
23
**Usage Examples:**
24
25
```java
26
import org.junit.experimental.theories.Theories;
27
import org.junit.experimental.theories.Theory;
28
import org.junit.experimental.theories.DataPoint;
29
import org.junit.runner.RunWith;
30
import static org.junit.Assert.*;
31
import static org.junit.Assume.*;
32
33
@RunWith(Theories.class)
34
public class StringTheoryTest {
35
@DataPoint
36
public static String EMPTY = "";
37
38
@DataPoint
39
public static String SHORT = "a";
40
41
@DataPoint
42
public static String LONG = "hello world";
43
44
@Theory
45
public void lengthIsNonNegative(String str) {
46
assertTrue(str.length() >= 0);
47
}
48
49
@Theory
50
public void concatenationIncreasesLength(String s1, String s2) {
51
int originalLength = s1.length() + s2.length();
52
String concatenated = s1 + s2;
53
assertEquals(originalLength, concatenated.length());
54
}
55
}
56
```
57
58
### DataPoint Annotation
59
60
Marks a field or method as providing a data point for theories. Theories will be executed with each data point.
61
62
```java { .api }
63
/**
64
* Marks a public static field or method as a data point source
65
* Each data point will be used as parameter for theories
66
* @param value - Optional array of names to filter data points
67
* @param ignoredExceptions - Exceptions to ignore when using this data point
68
*/
69
@Retention(RetentionPolicy.RUNTIME)
70
@Target({ElementType.FIELD, ElementType.METHOD})
71
public @interface DataPoint {
72
String[] value() default {};
73
Class<? extends Throwable>[] ignoredExceptions() default {};
74
}
75
```
76
77
**Usage Examples:**
78
79
```java
80
import org.junit.experimental.theories.Theories;
81
import org.junit.experimental.theories.Theory;
82
import org.junit.experimental.theories.DataPoint;
83
import org.junit.runner.RunWith;
84
import static org.junit.Assert.*;
85
86
@RunWith(Theories.class)
87
public class NumberTheoryTest {
88
@DataPoint
89
public static int ZERO = 0;
90
91
@DataPoint
92
public static int POSITIVE = 5;
93
94
@DataPoint
95
public static int NEGATIVE = -5;
96
97
@DataPoint
98
public static int MAX = Integer.MAX_VALUE;
99
100
@Theory
101
public void additionIsCommutative(int a, int b) {
102
assumeTrue(canAdd(a, b)); // Skip if overflow would occur
103
assertEquals(a + b, b + a);
104
}
105
106
@Theory
107
public void absoluteValueIsNonNegative(int x) {
108
assertTrue(Math.abs(x) >= 0);
109
}
110
111
private boolean canAdd(int a, int b) {
112
try {
113
Math.addExact(a, b);
114
return true;
115
} catch (ArithmeticException e) {
116
return false;
117
}
118
}
119
}
120
121
// Data point methods
122
@RunWith(Theories.class)
123
public class MethodDataPointTest {
124
@DataPoint
125
public static String generateEmptyString() {
126
return "";
127
}
128
129
@DataPoint
130
public static String generateShortString() {
131
return "test";
132
}
133
134
@Theory
135
public void everyStringHasDefinedLength(String str) {
136
assertNotNull(str);
137
assertTrue(str.length() >= 0);
138
}
139
}
140
```
141
142
### DataPoints Annotation
143
144
Marks a field or method as providing multiple data points as an array or iterable.
145
146
```java { .api }
147
/**
148
* Marks a public static field or method as providing multiple data points
149
* The field must be an array or Iterable
150
* @param value - Optional array of names to filter data points
151
* @param ignoredExceptions - Exceptions to ignore when using these data points
152
*/
153
@Retention(RetentionPolicy.RUNTIME)
154
@Target({ElementType.FIELD, ElementType.METHOD})
155
public @interface DataPoints {
156
String[] value() default {};
157
Class<? extends Throwable>[] ignoredExceptions() default {};
158
}
159
```
160
161
**Usage Examples:**
162
163
```java
164
import org.junit.experimental.theories.Theories;
165
import org.junit.experimental.theories.Theory;
166
import org.junit.experimental.theories.DataPoints;
167
import org.junit.runner.RunWith;
168
import static org.junit.Assert.*;
169
import static org.junit.Assume.*;
170
171
@RunWith(Theories.class)
172
public class CollectionTheoryTest {
173
@DataPoints
174
public static int[] NUMBERS = {0, 1, -1, 5, -5, 100};
175
176
@DataPoints
177
public static String[] STRINGS = {"", "a", "hello", "world"};
178
179
@Theory
180
public void multiplicationByZeroIsZero(int x) {
181
assertEquals(0, x * 0);
182
}
183
184
@Theory
185
public void stringConcatenationIsNotNull(String s1, String s2) {
186
assertNotNull(s1 + s2);
187
}
188
}
189
190
// DataPoints with List
191
@RunWith(Theories.class)
192
public class ListDataPointsTest {
193
@DataPoints
194
public static List<Integer> PRIMES = Arrays.asList(2, 3, 5, 7, 11, 13);
195
196
@Theory
197
public void primesAreGreaterThanOne(int prime) {
198
assertTrue(prime > 1);
199
}
200
201
@Theory
202
public void productOfPrimesIsComposite(int p1, int p2) {
203
assumeTrue(p1 != p2);
204
int product = p1 * p2;
205
assertTrue(product > p1);
206
assertTrue(product > p2);
207
}
208
}
209
```
210
211
### FromDataPoints Annotation
212
213
References named data points to be used for a specific parameter. Allows selective use of data points.
214
215
```java { .api }
216
/**
217
* Indicates which named data points should be used for a parameter
218
* @param value - Name of the data point group to use
219
*/
220
@Retention(RetentionPolicy.RUNTIME)
221
@Target(ElementType.PARAMETER)
222
public @interface FromDataPoints {
223
String value();
224
}
225
```
226
227
**Usage Examples:**
228
229
```java
230
import org.junit.experimental.theories.Theories;
231
import org.junit.experimental.theories.Theory;
232
import org.junit.experimental.theories.DataPoints;
233
import org.junit.experimental.theories.FromDataPoints;
234
import org.junit.runner.RunWith;
235
import static org.junit.Assert.*;
236
237
@RunWith(Theories.class)
238
public class NamedDataPointsTest {
239
@DataPoints("positiveNumbers")
240
public static int[] POSITIVE = {1, 2, 5, 10, 100};
241
242
@DataPoints("negativeNumbers")
243
public static int[] NEGATIVE = {-1, -2, -5, -10, -100};
244
245
@DataPoints("validStrings")
246
public static String[] VALID = {"hello", "world", "test"};
247
248
@DataPoints("emptyStrings")
249
public static String[] EMPTY = {"", null};
250
251
@Theory
252
public void positiveTimesPositiveIsPositive(
253
@FromDataPoints("positiveNumbers") int a,
254
@FromDataPoints("positiveNumbers") int b
255
) {
256
assertTrue(a * b > 0);
257
}
258
259
@Theory
260
public void positiveTimesNegativeIsNegative(
261
@FromDataPoints("positiveNumbers") int positive,
262
@FromDataPoints("negativeNumbers") int negative
263
) {
264
assertTrue(positive * negative < 0);
265
}
266
267
@Theory
268
public void validStringsAreNotEmpty(
269
@FromDataPoints("validStrings") String str
270
) {
271
assertFalse(str.isEmpty());
272
}
273
}
274
```
275
276
### Theories Runner
277
278
The runner that executes theories with all valid parameter combinations.
279
280
```java { .api }
281
/**
282
* Runner for executing theories
283
* Runs theory methods with all possible parameter combinations
284
*/
285
public class Theories extends BlockJUnit4ClassRunner {
286
/**
287
* Creates Theories runner
288
* @param klass - Test class containing theories
289
* @throws InitializationError if initialization fails
290
*/
291
public Theories(Class<?> klass) throws InitializationError;
292
}
293
```
294
295
**Usage Examples:**
296
297
```java
298
import org.junit.experimental.theories.Theories;
299
import org.junit.experimental.theories.Theory;
300
import org.junit.experimental.theories.DataPoint;
301
import org.junit.runner.RunWith;
302
import static org.junit.Assert.*;
303
304
@RunWith(Theories.class)
305
public class MathTheories {
306
@DataPoint public static int ZERO = 0;
307
@DataPoint public static int ONE = 1;
308
@DataPoint public static int TWO = 2;
309
@DataPoint public static int MINUS_ONE = -1;
310
311
@Theory
312
public void additionIsCommutative(int a, int b) {
313
assertEquals(a + b, b + a);
314
}
315
316
@Theory
317
public void addingZeroDoesNotChange(int x) {
318
assertEquals(x, x + 0);
319
assertEquals(x, 0 + x);
320
}
321
322
@Theory
323
public void multiplyingByOneDoesNotChange(int x) {
324
assertEquals(x, x * 1);
325
assertEquals(x, 1 * x);
326
}
327
}
328
```
329
330
### ParameterSupplier
331
332
Base class for custom parameter suppliers. Allows programmatic generation of data points.
333
334
```java { .api }
335
/**
336
* Supplies values for theory parameters
337
* Extend this to create custom data point sources
338
*/
339
public abstract class ParameterSupplier {
340
/**
341
* Get potential values for a parameter
342
* @param signature - Parameter signature
343
* @return List of potential assignments
344
*/
345
public abstract List<PotentialAssignment> getValueSources(ParameterSignature signature) throws Throwable;
346
}
347
```
348
349
**Usage Examples:**
350
351
```java
352
import org.junit.experimental.theories.ParameterSupplier;
353
import org.junit.experimental.theories.PotentialAssignment;
354
import org.junit.experimental.theories.ParameterSignature;
355
import org.junit.experimental.theories.ParametersSuppliedBy;
356
import org.junit.experimental.theories.Theories;
357
import org.junit.experimental.theories.Theory;
358
import org.junit.runner.RunWith;
359
import java.util.ArrayList;
360
import java.util.List;
361
import static org.junit.Assert.*;
362
363
// Custom supplier for ranges
364
public class BetweenSupplier extends ParameterSupplier {
365
@Override
366
public List<PotentialAssignment> getValueSources(ParameterSignature sig) {
367
Between annotation = sig.getAnnotation(Between.class);
368
List<PotentialAssignment> values = new ArrayList<>();
369
370
for (int i = annotation.first(); i <= annotation.last(); i++) {
371
values.add(PotentialAssignment.forValue(String.valueOf(i), i));
372
}
373
return values;
374
}
375
}
376
377
// Custom annotation
378
@Retention(RetentionPolicy.RUNTIME)
379
@ParametersSuppliedBy(BetweenSupplier.class)
380
public @interface Between {
381
int first();
382
int last();
383
}
384
385
// Usage
386
@RunWith(Theories.class)
387
public class RangeTheoryTest {
388
@Theory
389
public void numbersInRangeAreValid(@Between(first = 1, last = 10) int x) {
390
assertTrue(x >= 1 && x <= 10);
391
}
392
393
@Theory
394
public void sumOfSmallNumbersIsSmall(
395
@Between(first = 1, last = 5) int a,
396
@Between(first = 1, last = 5) int b
397
) {
398
assertTrue(a + b <= 10);
399
}
400
}
401
```
402
403
### ParametersSuppliedBy Annotation
404
405
Specifies a custom parameter supplier for a parameter.
406
407
```java { .api }
408
/**
409
* Indicates which ParameterSupplier should provide values
410
* @param value - ParameterSupplier class
411
*/
412
@Retention(RetentionPolicy.RUNTIME)
413
@Target({ElementType.ANNOTATION_TYPE, ElementType.PARAMETER})
414
public @interface ParametersSuppliedBy {
415
Class<? extends ParameterSupplier> value();
416
}
417
```
418
419
### TestedOn Annotation
420
421
Built-in annotation for specifying integer values to test on.
422
423
```java { .api }
424
/**
425
* Specifies integer values to test a parameter with
426
* @param ints - Array of integer values
427
*/
428
@Retention(RetentionPolicy.RUNTIME)
429
@Target(ElementType.PARAMETER)
430
@ParametersSuppliedBy(TestedOnSupplier.class)
431
public @interface TestedOn {
432
int[] ints();
433
}
434
```
435
436
**Usage Examples:**
437
438
```java
439
import org.junit.experimental.theories.Theories;
440
import org.junit.experimental.theories.Theory;
441
import org.junit.experimental.theories.suppliers.TestedOn;
442
import org.junit.runner.RunWith;
443
import static org.junit.Assert.*;
444
445
@RunWith(Theories.class)
446
public class TestedOnExample {
447
@Theory
448
public void multiplyingByOneDoesNotChange(
449
@TestedOn(ints = {0, 1, 2, -1, -5, 100}) int x
450
) {
451
assertEquals(x, x * 1);
452
}
453
454
@Theory
455
public void squareIsNonNegative(
456
@TestedOn(ints = {0, 1, 2, 5, -1, -5}) int x
457
) {
458
assertTrue(x * x >= 0);
459
}
460
461
@Theory
462
public void divisionByPowerOfTwo(
463
@TestedOn(ints = {0, 16, 32, 64, 128}) int x
464
) {
465
assertTrue(x % 2 == 0);
466
assertTrue(x / 2 <= x);
467
}
468
}
469
```
470
471
## Advanced Theory Patterns
472
473
### Using Assumptions to Constrain Theories
474
475
```java
476
import org.junit.experimental.theories.Theories;
477
import org.junit.experimental.theories.Theory;
478
import org.junit.experimental.theories.DataPoints;
479
import org.junit.runner.RunWith;
480
import static org.junit.Assert.*;
481
import static org.junit.Assume.*;
482
483
@RunWith(Theories.class)
484
public class ConstrainedTheories {
485
@DataPoints
486
public static int[] NUMBERS = {-10, -1, 0, 1, 10, 100};
487
488
@Theory
489
public void divisionWorks(int a, int b) {
490
assumeTrue(b != 0); // Skip when b is zero
491
int result = a / b;
492
assertEquals(a, result * b + (a % b));
493
}
494
495
@Theory
496
public void squareRootOfSquareIsOriginal(int x) {
497
assumeTrue(x >= 0); // Only test non-negative numbers
498
double sqrt = Math.sqrt(x * x);
499
assertEquals(x, sqrt, 0.0001);
500
}
501
502
@Theory
503
public void sortedPairIsOrdered(int a, int b) {
504
assumeTrue(a <= b); // Only test when a <= b
505
int[] sorted = sort(a, b);
506
assertTrue(sorted[0] <= sorted[1]);
507
}
508
}
509
```
510
511
### Combining Theories with Regular Tests
512
513
```java
514
import org.junit.experimental.theories.Theories;
515
import org.junit.experimental.theories.Theory;
516
import org.junit.experimental.theories.DataPoints;
517
import org.junit.Test;
518
import org.junit.runner.RunWith;
519
import static org.junit.Assert.*;
520
521
@RunWith(Theories.class)
522
public class MixedTests {
523
@DataPoints
524
public static int[] NUMBERS = {0, 1, 2, -1};
525
526
// Regular test - runs once
527
@Test
528
public void testSpecificCase() {
529
assertEquals(4, 2 + 2);
530
}
531
532
// Theory - runs for all data point combinations
533
@Theory
534
public void additionIsCommutative(int a, int b) {
535
assertEquals(a + b, b + a);
536
}
537
538
// Another regular test
539
@Test
540
public void testAnotherCase() {
541
assertTrue(true);
542
}
543
}
544
```
545
546
### Complex Object Theories
547
548
```java
549
import org.junit.experimental.theories.Theories;
550
import org.junit.experimental.theories.Theory;
551
import org.junit.experimental.theories.DataPoints;
552
import org.junit.runner.RunWith;
553
import static org.junit.Assert.*;
554
555
@RunWith(Theories.class)
556
public class ComplexObjectTheories {
557
public static class User {
558
String name;
559
int age;
560
561
User(String name, int age) {
562
this.name = name;
563
this.age = age;
564
}
565
}
566
567
@DataPoints
568
public static User[] USERS = {
569
new User("Alice", 25),
570
new User("Bob", 30),
571
new User("Charlie", 35)
572
};
573
574
@DataPoints
575
public static String[] NAMES = {"Alice", "Bob", "Charlie", "David"};
576
577
@Theory
578
public void userNamesAreNotNull(User user) {
579
assertNotNull(user.name);
580
}
581
582
@Theory
583
public void agesArePositive(User user) {
584
assertTrue(user.age > 0);
585
}
586
587
@Theory
588
public void namesAreNotEmpty(String name) {
589
assertFalse(name.isEmpty());
590
}
591
}
592
```
593
594
## Types
595
596
```java { .api }
597
/**
598
* Represents a potential parameter value assignment
599
*/
600
public class PotentialAssignment {
601
/**
602
* Create assignment with value
603
* @param name - Description of value
604
* @param value - The value
605
* @return PotentialAssignment
606
*/
607
public static PotentialAssignment forValue(String name, Object value);
608
609
/**
610
* Get the value
611
* @return Value object
612
*/
613
public Object getValue() throws CouldNotGenerateValueException;
614
615
/**
616
* Get description
617
* @return Description string
618
*/
619
public String getDescription();
620
}
621
622
/**
623
* Describes a parameter's type and annotations
624
*/
625
public class ParameterSignature {
626
/**
627
* Get parameter type
628
* @return Parameter class
629
*/
630
public Class<?> getType();
631
632
/**
633
* Get parameter annotations
634
* @return List of annotations
635
*/
636
public List<Annotation> getAnnotations();
637
638
/**
639
* Get specific annotation
640
* @param annotationType - Annotation class
641
* @return Annotation or null
642
*/
643
public <T extends Annotation> T getAnnotation(Class<T> annotationType);
644
645
/**
646
* Check if parameter has annotation
647
* @param type - Annotation class
648
* @return true if annotation present
649
*/
650
public boolean hasAnnotation(Class<? extends Annotation> type);
651
652
/**
653
* Get parameter name
654
* @return Parameter name
655
*/
656
public String getName();
657
658
/**
659
* Check if type can accept value
660
* @param value - Value to check
661
* @return true if compatible
662
*/
663
public boolean canAcceptValue(Object value);
664
665
/**
666
* Check if type can potentially accept another type
667
* @param type - Type to check
668
* @return true if potentially compatible
669
*/
670
public boolean canPotentiallyAcceptType(Class<?> type);
671
}
672
673
/**
674
* Thrown when parameter value cannot be generated
675
*/
676
public class CouldNotGenerateValueException extends Exception {
677
public CouldNotGenerateValueException();
678
public CouldNotGenerateValueException(Throwable cause);
679
}
680
681
/**
682
* Exception indicating parameterized assertion failure
683
*/
684
public class ParameterizedAssertionError extends AssertionError {
685
public ParameterizedAssertionError(Throwable targetException, String methodName, Object... params);
686
}
687
```
688
689
## Theories vs Parameterized Tests
690
691
| Aspect | Theories | Parameterized Tests |
692
|--------|----------|---------------------|
693
| Purpose | Property-based testing | Data-driven testing |
694
| Data definition | Flexible data points | Explicit parameter sets |
695
| Combinations | Automatic combinations | Manual specification |
696
| Assumptions | Use assumeXxx to skip | Not available |
697
| Use case | General properties | Specific test cases |
698
| Runner | @RunWith(Theories.class) | @RunWith(Parameterized.class) |
699
| Failure reporting | Shows failing combination | Shows failing parameters |
700
701
**When to use Theories:**
702
- Testing general properties that should hold for all inputs
703
- Exploring edge cases automatically
704
- Property-based testing approach
705
- When you want automatic parameter combinations
706
707
**When to use Parameterized:**
708
- Testing specific known scenarios
709
- Explicit test case data
710
- When each parameter set is independent
711
- More readable test output needed
712