More on JUnit Theories

In my last blog post, I described how to use JUnit Theories to create large amounts of test runs, with very limited amount of work, like so:

import static org.junit.Assume.assumeTrue;
  1. @RunWith(Theories.class)
  2. public class TheorieTest {
  3.  
  4.  @DataPoint
  5.  public static String a = "a";
  6.  
  7.  @DataPoint
  8.  public static String b = "bb";
  9.  
  10.  @DataPoint
  11.  public static String c = "ccc";
  12.  
  13.  @Theory
  14.  public void stringTest(String x, String y) {
  15.   assumeTrue(x.length() > 1);
  16.  
  17.   System.out.println(x + " " + y);
  18.  }
  19. }

The trick is simple to provide data points for every parameter type of the test method. The JUnit Theories Runner will call the test method with every possible combination of datapoints. If you think a little about it you will soon realize some of the limitations of this approach:

  • You’ll soon end up with lots of data point fields cluttering your code
  • Parameters of the same type will receive the same set of parameters, even when the usable range of inputs is completely different.

Fortunately the developers of JUnit provided really nice solutions to these problems.

Instead of specifying single data points, you can provide a full array of datapoints using the @Datapoints annotation, like so (add imports for good measure):

@RunWith(Theories.class)
  1. public class TheorieTest {
  2.  
  3.  @DataPoints
  4.  public static String[] a = { "a", "bb", "ccc" };
  5.  
  6.  @DataPoints
  7.  public static Integer[] j = { 1, 2, 3 };
  8.  
  9.  @Theory
  10.  public void someTest(String x, Integer y) {
  11.   assumeTrue(x.length() > 1);
  12.  
  13.   System.out.println(x + " " + y);
  14.  }
  15. }

This of course is much less verbose. Instead of an array you may provide a method returning an array, or at least it looks like this should be possible. But when I tried it JUnit seemed unable to handle the types correctly resulting in IllegalArgumentExceptions. Guess I’ll have to file a bug when finished with this article …

But we still need to take care of parameters which have the same type, but very different meaning and therefore different useful values. The clean OO way of doing things would be to get rid of the generic types like String and use stronger types like CreditCardNumber or Name instead. But then in a perfect world we wouldn’t need tests, because our programs wouldn’t contain any bugs to begin with. So lets try this instead (Again imports omitted):

@Retention(RetentionPolicy.RUNTIME)
  1. @ParametersSuppliedBy(CreditCardSupplier.class)
  2. public @interface AllCreditCards {}
  3.  
  4. //—————————————————————–
  5.  
  6. @Retention(RetentionPolicy.RUNTIME)
  7. @ParametersSuppliedBy(NameSupplier.class)
  8. public @interface AllNames {}
  9.  
  10. //—————————————————————–
  11.  
  12. public class CreditCardSupplier extends ParameterSupplier {
  13.  
  14.  @Override
  15.  public List getValueSources(
  16.    ParameterSignature signature) {
  17.  
  18.   ArrayList result = new ArrayList();
  19.  
  20.   result.add(PotentialAssignment.forValue("Amex", "Amex"));
  21.   result.add(PotentialAssignment.forValue("Master", "Master"));
  22.   result.add(PotentialAssignment.forValue("Visa", "Visa"));
  23.  
  24.   return result;
  25.  }
  26. }
  27.  
  28. //—————————————————————–
  29.  
  30. public class NameSupplier extends ParameterSupplier {
  31.  
  32.  @Override
  33.  public List getValueSources(
  34.    ParameterSignature signature) {
  35.  
  36.   AllNames annotation = signature.getAnnotation(AllNames.class);
  37.   System.out.println("just wanted to show that I can access it "
  38.     + annotation);
  39.  
  40.   ArrayList result = new ArrayList();
  41.  
  42.   result.add(PotentialAssignment.forValue("Alf", "Alf"));
  43.   result.add(PotentialAssignment.forValue("Willie", "Willie"));
  44.   result.add(PotentialAssignment.forValue("Tanner", "Tanner"));
  45.   result.add(PotentialAssignment.forValue("Cat", "Cat"));
  46.  
  47.   return result;
  48.  }
  49. }
  50.  
  51. //—————————————————————–
  52.  
  53. @RunWith(Theories.class)
  54. public class SuppliedByTest {
  55.  
  56.  @Theory
  57.  public void imagineThisIsATest(@AllCreditCards String x, @AllNames String y) {
  58.   System.out.println("consider " + x + " / " + y + " tested.");
  59.  }
  60.  
  61.  @Theory
  62.  public void testIntegers(@TestedOn(ints = { 2, 3, 4, 7, 13, 23, 42 }) int i) {
  63.   System.out.println(i);
  64.  }
  65. }

Wow, thats a lot of code. Just look at the last piece and see what appears in the console when we run it:

just wanted to show that I can access it @de.schauderhaft.junit.theories.AllNames()
consider Amex / Alf tested.
consider Amex / Willie tested.
consider Amex / Tanner tested.
consider Amex / Cat tested.
just wanted to show that I can access it @de.schauderhaft.junit.theories.AllNames()
consider Master / Alf tested.
consider Master / Willie tested.
consider Master / Tanner tested.
consider Master / Cat tested.
just wanted to show that I can access it @de.schauderhaft.junit.theories.AllNames()
consider Visa / Alf tested.
consider Visa / Willie tested.
consider Visa / Tanner tested.
consider Visa / Cat tested.
2
3
4
7
13
23
42

Have a look at the row beginning with: “consider”. Obviously the Theory imagineThisIsATest gets fed with the values from the CreditCardSupplier and NameSupplier. The parameters and the ‘Suppliers’ are connected by the two annotations @AllNames and AllCreditCards. So whenever you have a parameter to a theory where the type alone is not sufficient for identifying the kind of values that should get used, you can simple create an annotation, which itself is annotated with a reference to a ParameterSupplier class and you are all set. You might think this is a lot of code for supplying a handful of parameters. You are right, but remember, that you can reuse your suppliers wherever you need names or credit card values in your tests.

Now let’s look at the first line of the output:
just wanted to show that I can access it @de.schauderhaft.junit.theories.AllNames()
It simply shows of that you get access to the annotation (and actually the signature of the compete test method. This can be very useful, when you want your supplier to behave differently for different theories. Have a look at the NameSupplier above to see how this works.

JUnit actually comes with an example where this is used, and I demonstrated it with the other theory in the demonstration code above. The @TestedOn annotation takes an array of values to be used as data points for the annotated parameter.

Thats it for today. I hope the power of theories became obvious, as well as the power you have as a developer to extend that mechanism. Again be warned: All this nice stuff is in a package named experimental for good reason. If you use it, you might find bugs, and thing will likely change at least in name in an upcoming version. Taking about versions, I am using junit4.8.1 for the examples.

For next week the conclusion of the little series about JUnit theories is planned, with a few thoughts on use and danger of this kind of testing.

Share:
  • DZone
  • Digg
  • del.icio.us
  • Reddit
  • Facebook
  • Twitter