CallbackParams Offers Elegant Parameterization for JUnit

CallbackParams is a JUnit add-on for parameterized testing.

Tutorial Part I - Patterns That Simplify Maintenance

Simple Test Demo - PersonBuilderImpl

In this demo the creation of Person instances will be tested. Person is an immutable representation of user-data:

public interface Person {
    String getUsername();
    String getPassword();
}

It is created by an implementation of PersonBuilder:

public interface PersonBuilder {
    void username(String username);
    void password(String password);

    Person build();
}

PersonBuilder is implemented by the class PersonBuilderImpl, which contains the black-box functionality that will be tested.

Please note that we here deal with a variant of the Builder pattern (as described by Joshua Bloch, not GoF). The pattern fulfills two important criteria that makes it suitable for demonstrating parameterized testing:

  1. Front-End to Back-End:

    Parameter data is specified at the front-end and is expected to show up at the back-end. E.g. username is specified at the front-end with PersonBuilder.username(String) and is expected to show up at the back-end as Person.getUsername().

  2. Validation and Default-Values:

    Validation of parameter data is expected to take place before it reaches the back-end. E.g. if an empty string is specified as password it should result in a validation exception, which is expected to be thrown by PersonBuilder.password(String) or PersonBuilder.build().

These two criteria are very common in situations that are suitable for parameterized testing. E.g. ...

  • Form-data on a front-end servlet-request is expected to show up as method-arguments to the mocked back-end DAO-method (if form-data validation passes).
  • Arguments to the DAO-method implementation, which now constitutes the front-end, is expected to show up in the back-end database.
  • etc

... so in spite of PersonBuilderImpl being a pretty irrelevant test-target as such, fulfilling these criteria still makes it perfect for showing the qualities of CallbackParams.

CallbackParams is context-independent and will hopefully prove useful in many different situations but it has been especially designed to handle situations where these criteria are satisfied.

The Test-Class - Parameterized Versus CallbackParams

JUnit's test-runner Parameterized will here suffer some bashing, in order to motivate the features of CallbackParams. But it is in many aspects an unfair comparison. - The code-base for Parameterized is contained within a single java-file, whereas the CallbackParams jar weighs in at more than 100kb and has a mandatory dependency to BCEL.

@RunWith(Parameterized.class)

@RunWith(Parameterized.class)
public class TestPersonBuilderImplWithParameterized {

  /* ... PARAMETER MODEL ... */
  @Parameterized.Parameter(0) public String username;
  @Parameterized.Parameter(1) public String password;

  /* ... TEST METHOD ... */
  @Test public void testPersonCreation() {
    PersonBuilder builder = new PersonBuilderImpl();                   // 1
    builder.username(username);                                        // 2
    builder.password(password);                                        // 3

    Person person = builder.build();                                   // 4
    assertEquals("Verify username", username, person.getUsername());   // 5
    assertEquals("Verify password", password, person.getPassword());   // 6
  }

  /* ... PARAMETER VALUES ... */
  @Parameterized.Parameters public static List parameters() {
    return Arrays.asList(new Object[][] {
      {"Bar Fool", "s%s2H"},
      {"Full Bar", "sfe87e2&%dfrHUIdw4gD&3d"}
    };
  }
}

The test-class would result in one testrun for each Object[] in the collection returned by parameters() ...

  • testPersonCreation[0]
  • testPersonCreation[1]

... where the strings in the respective Object[] instances have been used as constructor arguments.

@RunWith(CallbackParamsRunner.class)

@RunWith(CallbackParamsRunner.class)
public class TestPersonBuilderImplWithCallbackParams {

  /* ... PARAMETER MODEL ... */
  interface CreationPropertyCallbacks {
    void setValues(PersonBuilder builder);
    void verifyValues(Person person);
  }

  /* ... TEST METHOD ... */
  @Test public void testPersonCreation(
      CreationPropertyCallbacks propertyCallback) {
    PersonBuilder builder = new PersonBuilderImpl();                   // 1
    propertyCallback.setValues(builder);

    Person person = builder.build();                                   // 4
    propertyCallback.verifyValues(person);
  }

  /* ... PARAMETER VALUES ... */

  enum Username implements CreationPropertyCallbacks {
    BAR_FOOL("barfoo"),
    FULL_BAR("foobar");

    private String username;

    Username(String username) { this.username = username; }

    public void setValues(PersonBuilder builder) {
      builder.username(username);                                      // 2
    }

    public void verifyValues(Person person) {
      assertEquals("Verify username", username, person.getUsername()); // 5
    }
  }

  enum Password implements CreationPropertyCallbacks {
    SHORTEST_PWD("s%s2H"),
    LONG_PWD("sfe87e2&%dfrHUIdw4gD&3d");

    private String password;

    Password(String password) { this.password = password; }

    public void setValues(PersonBuilder builder) {
      builder.password(password);                                      // 3
    }

    public void verifyValues(Person person) {
      assertEquals("Verify password", password, person.getPassword()); // 6
    }
  }
}

This test-class is way different from the Parameterized alternative but the test-method testPersonCreation(...) still performs the exact same operations on PersonBuilderImpl and the created Person but only two statements (1 & 4) have been kept inside the test-method. The other statements have been extracted out to the nested enum-classes Username and Password, for which each constant represent one value for username and password respectively. The trick is that the enum Username, on top of specifying username values, also encapsulate all operations that explicitly involve these values (i.e. the statements 2 & 5) and exposes these operations through the callback-methods that are specified by the callback-interface CreationPropertyCallbacks!! - The equivalent goes for enum Password ...
The enums' callback-method implementations (with their statements) are invoked from the test-method when the same method is invoked on the callback-interface instance that the framework passes as an argument to the test-method.

What happens is that the CallbackParams framework manufactures lists (or callback-records) containing parameter values (e.g. enum-constants), which each contains exactly one enum-constant from each nested enum and then have the test-class' tests executed once for each callback-record. The resulting testruns could be ...

  • testPersonCreation[SHORTEST_PWD, BAR_FOOL]
  • testPersonCreation[LONG_PWD, FULL_BAR]
  • testPersonCreation[SHORTEST_PWD, FULL_BAR]
  • testPersonCreation[LONG_PWD, BAR_FOOL]

... where the names of the parameter-values for the testrun's callback-record have been appended to the raw test-name! (The exact CombineStrategy can be configured.)

Whenever a @Before-, @Test- or @After-method takes interface-arguments, the framework will pass an interface-implementation that will work as a composite for all CreationPropertyCallbacks values in the current callback-record. E.g. when the method setValues(...) on the test-method's propertyCallback argument is invoked during the testrun testPersonCreation[BAR_FOOL, SHORTEST_PWD] then the invocation argument (builder) will be passed to the method setValues(...) of all callback-record value that implement the callback-interface CreationPropertyCallbacks, i.e. the setValues(...) methods of both BAR_FOOL and SHORTEST_PWD. Equivalently the assertion-statements 5 & 6 will be executed at the end of the test-method by the statement propertyCallback.verifyValues(person).

It should be clear that parameterization patterns of CallbackParams differs considerably from the traditional approach as the parameter model used by the test-method consists of callbacks, which the parameter values (i.e. the enum constants) need to implement in order to take part in the test-execution.
The unanswered question is what this is good for? - The above example does not indicate any decrease in the amount of code.
The answer is that several maintenance cases will be greatly simplified ...

Happy-Path Maintenance - Parameterized Versus CallbackParams

The term happy-path (as explained at Wikipedia) refers to scenarios where valid input data allows some piece of software to fulfill its purpose. The test-classes in this article perform happy-path testing, because they all test scenarios where PersonBuilderImpl is expected to follow its happy-path and fulfill its purpose by creating the desired Person instance. - Tests that verify functionality outside the happy-path (e.g. exceptions and validation-failures) will be examined in a separate article.

The subsections below present a number of different maintenance cases where only happy-path testing is concerned. - All scenarios will be applied to the test-classes introduced above.

Additional Parameter Values - Usernames With Digits

What if modified specs state that PersonBuilderImpl must stay happy (i.e. must not fire any exception) if username contains one or more digits. Let's check the test-modifications that are needed when ...

... using CallbackParams

Very easy - just add some constants to the Username enum:

  ...
  enum Username implements CreationPropertyCallbacks {
    USER_ZERO("0"),
    USER_ONE("1"),
    USER_37("37"),
    USER_ALPHANUM("user21"),
    BAR_FOOL("barfoo"),
    FULL_BAR("foobar");
  ...

It was in fact so easy we accidentally took several steps and added four new usernames, which all relate to the maintenance case at hand.

With these test-class modifications we get eight additional testruns ...

  • testPersonCreation[SHORTEST_PWD, USER_ZERO]
  • testPersonCreation[LONG_PWD, USER_ONE]
  • testPersonCreation[SHORTEST_PWD, USER_37]
  • testPersonCreation[LONG_PWD, USER_ALPHANUM]
  • testPersonCreation[LONG_PWD, USER_ZERO]
  • testPersonCreation[SHORTEST_PWD, USER_ONE]
  • testPersonCreation[LONG_PWD, USER_37]
  • testPersonCreation[SHORTEST_PWD, USER_ALPHANUM]

... i.e. eight additional unique combinations of Username and Password. - The total number of testruns is now twelve.

... using Parameterized

Pretty easy - add some additional records to method parameters():

  ...
  @Parameterized.Parameters public static List<Object[ parameters() {
    return Arrays.asList(new Object[][] {
      {"0",        "sFe87e2&%d"},
      {"1",        "sFe87e2&%d"},
      {"37",       "sFe87e2&%d"},
      {"user21",   "sFe87e2&%d"},
      {"Bar Fool", "s%s2H"},
      {"Full Bar", "sfe87e2&%dfrHUIdw4gD&3d"}
    };
  }
  ...

Unfortunately it was necessary to manually combine the new username values with valid passwords. Rather then accidentally specify invalid passwords, a password that will hopefully be compatible with future spec modifications was constructed and reused for all of the new usernames. (Did the word DRY pop up in anyone's mind?)

The number of testruns increase to six:

  • testPersonCreation[0]
  • testPersonCreation[1]
  • testPersonCreation[2]
  • testPersonCreation[3]
  • testPersonCreation[4]
  • testPersonCreation[5]

Additional Property - workhoursPerWeek (int)

Let's assume the new property "workhoursPerWeek" (int) is added. It would add these modifications to Person and PersonBuilder:

public interface Person {
    String getUsername();
    String getPassword();
    int getWorkhoursPerWeek();         
}

public interface PersonBuilder {
    void username(String username);
    void password(String password);
    void workhoursPerWeek(int hours);  

    Person build();
}

Some decent happy-path values to test are 0, 1, 20, 30, 40, 67. The following test-class modifications are needed when ...

... using CallbackParams

Easy - just add a nested enum for this property:

  ...
  enum WorkhoursPerWeek
  implements CreationPropertyCallbacks {
    WORK_0h(0),
    WORK_1h(1),
    WORK_20h(20),
    WORK_30h(30),
    WORK_40h(40),
    WORK_67h(67);

    int hours;

    private WorkhoursPerWeek(int hours) {
      this.hours = hours;
    }

    public void setValues(PersonBuilder builder) {
      builder.workhoursPerWeek(hours);
    }

    public void verifyValues(Person person) {
      assertEquals("Workhours",
          hours, person.getWorkhoursPerWeek());
    }
  }
  ...

Adding this enum will result in a total of 36 testruns, which will have test-names similar to testPersonCreation[WORK_0h, SHORTEST_PWD, USER_ZERO] etc.
All was accomplished by adding a single chunk of code without making any other test-class modifications!

... using Parameterized

Not so easy - this pretty normal type of maintenance - and the amount of isolated modifications that arise from added (or removed) parameter values constitute the number one reason to why the CallbackParams framework came about:

@RunWith(Parameterized.class)
public class TestPersonBuilderImplWithParameterized {

  /* ... PARAMETER MODEL ... */
  @Parameterized.Parameter(0) public String username;
  @Parameterized.Parameter(1) public String password;
  @Parameterized.Parameter(2) public Integer workhoursPerWeek;

  /* ... TEST METHOD ... */
  @Test public void testPersonCreation() {
    PersonBuilder builder = new PersonBuilderImpl();
    builder.username(username);
    builder.password(password);
    builder.workhoursPerWeek(workhoursPerWeek);
  
    Person person = builder.build();
    assertEquals("Verify username", username, person.getUsername());
    assertEquals("Verify password", password, person.getPassword());
    assertEquals("Verify weekly workhours",
        workhoursPerWeek, person.getWorkhoursPerWeek());
  }

  /* ... PARAMETER VALUES ... */
  @Parameterized.Parameters public static List parameters() {
    return Arrays.asList(new Object[][] {
      {"0",        "sFe87e2&%d", 0},
      {"1",        "sFe87e2&%d", 1},
      {"37",       "sFe87e2&%d", 20},
      {"user21",   "sFe87e2&%d", 30},
      {"Bar Fool", "s%s2H", 40},
      {"Full Bar", "sfe87e2&%dfrHUIdw4gD&3d", 67}
    };
  }
}

While the CallbackParams test-class requires a single continuous chunk of code for this maintenance case, the Parameterized test-class has its modifications scattered all over ...

  • Add the parameter
  • Test-method modifications
  • Every single array of parameter values must be modified.
    This particular modification is a contradictory dilemma. - The whole point with parameterized testing is the possibility to test more by appending additional arrays with parameter values but a test with many value-arrays demands more modifications for this kind of maintenance. - In this (demo) case the challenge is bearable because there are only six value-arrays.
    CallbackParams' combine-facility makes the framework more or less immune to this dilemma. - Hundreds of callback-records are easily maintained ...

Default Value - workhoursPerWeek = 40

The new property workhoursPerWeek turns out to be of minor importance and will not be supported for all scenarios where Person instances are created - instead a default value of 40 will be used.

The default-value functionality must be verified with the test-class ...

... using CallbackParams

Since individual enum-constants can override methods, only minor modifications are needed:

  ...
  enum WorkhoursPerWeek
  implements CreationPropertyCallbacks {
    WORKHOURS_DEFAULT(40) {
      @Override
      public void setValues(PersonBuilder builder) {
        /* workhoursPerWeek is not set */
      }
    },
    WORK_0h(0),
    WORK_1h(1),
    WORK_20h(20),
  ...

I.e. the method setValues(...) is overridden so that workhoursPerWeek is never specified but the method verifyValues(...) remains unchanged and verifies that person.getWorkhoursPerWeek = 40 ...

This results in six additional testruns ...

  • testPersonCreation[WORKHOURS_DEFAULT, SHORTEST_PWD, USER_ZERO]
  • testPersonCreation[WORKHOURS_DEFAULT, LONG_PWD, USER_ONE]
  • testPersonCreation[WORKHOURS_DEFAULT, SHORTEST_PWD, USER_37]
  • testPersonCreation[WORKHOURS_DEFAULT, LONG_PWD, USER_ALPHANUM]
  • testPersonCreation[WORKHOURS_DEFAULT, SHORTEST_PWD, BAR_FOOL]
  • testPersonCreation[WORKHOURS_DEFAULT, LONG_PWD, FULL_BAR]

... resulting in a total of 42 testruns.

... using Parameterized

A possible strategy is to have a certain workhoursPerWeek value reserved for this circumstance. Since the int-wrapper class Integer is used, the null-value is vacant and can be used to force an alternative execution path:

@RunWith(Parameterized.class)
public class TestPersonBuilderImplWithParameterized {

  /* ... PARAMETER MODEL ... */
  @Parameterized.Parameter(0) public String username;
  @Parameterized.Parameter(1) public String password;
  @Parameterized.Parameter(2) public Integer workhoursPerWeek;

  /* ... TEST METHOD ... */
  @Test public void testPersonCreation() {
    PersonBuilder builder = new PersonBuilderImpl();
    builder.username(username);
    builder.password(password);
    if (null != workhoursPerWeek) {
      builder.workhoursPerWeek(workhoursPerWeek);
    } else {
      workhoursPerWeek = 40; // Expected default value
    }
  
    Person person = builder.build();
    assertEquals("Verify username", username, person.getUsername());
    assertEquals("Verify password", password, person.getPassword());
    assertEquals("Verify weekly workhours",
        workhoursPerWeek, person.getWorkhoursPerWeek());
  }

  /* ... PARAMETER VALUES ... */
  @Parameterized.Parameters public static List parameters() {
    return Arrays.asList(new Object[][] {
      {"whatever", "sFe87e2&%d", null},
      {"0",        "sFe87e2&%d", 0},
      {"1",        "sFe87e2&%d", 1},
      {"37",       "sFe87e2&%d", 20},
      {"user21",   "sFe87e2&%d", 30},
      {"Bar Fool", "s%s2H", 40},
      {"Full Bar", "sfe87e2&%dfrHUIdw4gD&3d", 67}
    };
  }
}

Though we have managed to test the new functionality the test-modification demanded three chunks of code, that all carried some amount of ugly-hack scent.

  • The test-method is slowly getting overweight. (The CallbackParams test-method is still untouched!)
  • Changing any of the existing value-arrays would have forced the replacement of some existing parameter-value. Instead a new value-array was added - and once again a value-array had to be added with some ad-hoc values for the other parameters, a concern that CallbackParams' combine-machinery would have spared the developer.

Remove a Property - workhoursPerWeek

How about removing an existing property ...

public interface Person {
    String getUsername();
    String getPassword();
    int getWorkhoursPerWeek();         
}

public interface PersonBuilder {
    void username(String username);
    void password(String password);
    void workhoursPerWeek(int hours);  

    Person build();
}

... from the test-class when ...

... using CallbackParams

Remove the enum for this property:

  ...
  enum WorkhoursPerWeek
  implements CreationPropertyCallbacks {
    WORKHOURS_DEFAULT(40) {
      @Override
      public void setValues(PersonBuilder builder) {
        /* workhoursPerWeek is not set */
      }
    },
    WORK_0h(0),
    WORK_1h(1),
    WORK_20h(20),
    WORK_30h(30),
    WORK_40h(40),
    WORK_67h(67);

    int hours;

    private WorkhoursPerWeek(int hours) {
      this.hours = hours;
    }

    public void setValues(PersonBuilder builder) {
      builder.workhoursPerWeek(hours);
    }

    public void verifyValues(Person person) {
      assertEquals("Workhours",
          hours, person.getWorkhoursPerWeek());
    }
  }
  ...
... using Parameterized

Remove anything that deals with workhoursPerWeek ...

@RunWith(Parameterized.class)
public class TestPersonBuilderImplWithParameterized {

  /* ... PARAMETER MODEL ... */
  @Parameterized.Parameter(0) public String username;
  @Parameterized.Parameter(1) public String password;
  @Parameterized.Parameter(2) public Integer workhoursPerWeek;
  
  /* ... TEST METHOD ... */
  @Test public void testPersonCreation() {
    PersonBuilder builder = new PersonBuilderImpl();
    builder.username(username);
    builder.password(password);
    if (null != workhoursPerWeek) {
      builder.workhoursPerWeek(workhoursPerWeek);
    } else {
      workhoursPerWeek = 40; // Expected default value
    }
  
    Person person = builder.build();
    assertEquals("Verify username", username, person.getUsername());
    assertEquals("Verify password", password, person.getPassword());
    assertEquals("Verify weekly workhours",
        workhoursPerWeek, person.getWorkhoursPerWeek());
  }

  /* ... PARAMETER VALUES ... */
  @Parameterized.Parameters public static List parameters() {
    return Arrays.asList(new Object[][] {
      {"whatever", "sFe87e2&%d", null},
      {"0",        "sFe87e2&%d", 0},
      {"1",        "sFe87e2&%d", 1},
      {"37",       "sFe87e2&%d", 20},
      {"user21",   "sFe87e2&%d", 30},
      {"Bar Fool", "s%s2H", 40},
      {"Full Bar", "sfe87e2&%dfrHUIdw4gD&3d", 67}
    };
  }
}

... i.e. a total of ten chunks of code.
Seven of these code-chunks are removed from the method parameters() - one code-chunk removal for each value-array. - Fortunately the number of value-arrays were only seven and the removed values were easy to find at the end of each array ...

Unfortunately a (less pedantic) developer (on your team) might not immediately find it necessary to remove all of those obsolete code-chunks. The suboptimal action-plan might look like this:

  1. Compilation Failure? - Oh, better fix the test-method - that workhoursPerWeek-property is history. Yes - now all tests pass!
  2. Hmmm - code sanity warnings - something about unused field workhoursPerWeek? - Oh yes, it is not used any more. - OK remove it then
  3. Hmmm - are there test-errors now? - Oh no, the value arrays from parameters() no longer match the number of parameters. - OK, then remove all the obsolete workhoursPerWeek values.

We have all met at least one developer who would happily commit the modifications after #1.

Most developers that reach #3 would probably not eliminate the entire value-array {"whatever", "sFe87e2&%d", null} but keep the part {"whatever", "sFe87e2&%d"}, since it is not so obvious that it came about as a nonsense add-on to a relevant workhoursPerWeek-value and does not provide anything valuable on its own.

(Biased) Evaluation

Amount of Code

The tests using the Parameterized runner will have less code then the CallbackParams tests, thanks to a more compact syntax.

Some would argue that CallbackParams, through its combine-machinery, generates a greater number of tests per unit of code. Although this is true (and might impress your boss) it is only sort-of-true, because each test-scenario is probably tested several times. The real benefit of the combine-facility is simply that elimination of the combine burden usually helps structure and simplifies maintenance (as discussed above and below).

However, the CallbackParams framework does offer an alternative runner - BddRunner to support Behavior-Driven Develepment (BDD) - which often can decrease the amount of code by eliminating the actual test-method and the callback-interfaces.

Responding to Change

"Responding to Change" is one of the terms used at http://agilemanifesto.org. - The second of the Principles behind the Agile Manifesto states: "Welcome changing requirements, even late in development. Agile processes harness change for the customer's competitive advantage."

Though Parameterized tests might demand less code initially, maintenance due to "changing requirements" is not much fun. Especially if it involves the addition or removal of properties (i.e. form-fields or database columns etc). What makes things worse - if the current Parameterized test offers good test-coverage - by having several value-arrays - the hardships of maintaining the test get even harder.

On the other hand, the combine-machinery and callback-patterns of CallbackParams allow each property's test-values and test-logic to be kept cleanly separated from other properties' ditto. Therewith the maintenance task is greatly simplified, even if the current test has grown quite large!
Please note that the code outside of the properties' enums ...

@RunWith(CallbackParamsRunner.class)
public class TestPersonBuilderImplWithCallbackParams {

  /* ... PARAMETER MODEL ... */
  interface CreationPropertyCallbacks {
    void setValues(PersonBuilder builder);
    void verifyValues(Person person);
  }

  /* ... TEST METHOD ... */
  @Test public void testPersonCreation(
      CreationPropertyCallbacks propertyCallback) {
    PersonBuilder builder = new PersonBuilderImpl();
    propertyCallback.setValues(builder);

    Person person = builder.build();
    propertyCallback.verifyValues(person);
  }
  ...

... was never modified for any of the maintenance tasks above!! In fact none of the maintenance tasks demanded code-modification anywhere outside of the enum of the property of concern ...

It should be said that the maintenance tasks above did only concern happy-path maintenance but, as shall be shown in future articles, it takes only minor modifications to turn the test-class into a harness that supports both happy-path and validation-failure scenarios.

Other Structural Concerns

On top of CallbackParams' ability to respond-to-change, the clean separation of properties helps to make the test-class more readable and well-documented in general. The primary reason is that each parameter-value is specified only once, therewith providing a natural point for documenting the purpose of the particular parameter-value (or perhaps be content with having the name of the parameter-value's enum-constant serve as documentation).
Also note that the return-value of the toString() method of the enum-constant is used when naming the individual test-runs by suffixing the method-names with the toString() value of the callback-record.

Another desirable advantage is the ability to have very robust test-methods that can be heavily reused. In the context of a webapp this could possibly allow the use of only one test-method per form - and have it handle an MVC action-class in a manner similar to how PersonBuilderImpl is handled above.
The dream scenario would be to have one test-method for each valid request-lifecycle and have parameter enums verify the low-level functionality that is related to the individual request-parameters. That would make an enum-method, which takes care of validation of an individual property, look like what has traditionally been regarded as the ideal test-method (one assertion per test etc) with the difference that it takes one or more arguments and would be executed within the actual test-method. The actual test-method would then provide the proper context arguments and make sure the low-level tests are executed at the right moment in the overall lifecycle.

Short Note on the Combine Machinery

The CallbackParams test-examples above have all relied on the default combine configuration ...

@RunWith(CallbackParamsRunner.class)
@CombineConfig( strategy=CombineAllPossible2Combinations.class )
public class TestPersonBuilderImplWithCallbackParams {
  ...

... which kicks in if no combine configuration has been explicitly specified. When the @CombineConfig-annotation is used the "strategy"-value must be set to the desired org.callbackparams.combine.CombineStrategy implementation. Its method combine(...) will be fed with the value-arrays of the test-class' enums and is expected to return a collection of callback-records to use for the testruns.

It is possible to use customized combine-strategies by implementing the CombineStrategy interface and have the @CombineConfig-annotation specify the implementation class as "strategy". Otherwise there are currently two built-in combine-strategies (org.callbackparams.combine.CombineAllPossible2Combinations and org.callbackparams.combine.CombineCompletely) that both attempt to take on the task from the perspective of factorial design by treating the enums as factors and their constants as levels ...

CombineCompletely

The callback-records produced by this strategy are supposed to form a full factorial experiment. E.g. the above example, which have 6 Username constants, 2 Password constants and 7 WorkhoursPerWeek constants, would result in a total of ...

       6 x 2 x 7 = 84

... 84 testruns if CombineCompletely is used. I.e. the total number of (unordered) combinations possible if one constant is picked from each enum.

The obvious advantage of this strategy is that all combinations are tested.
However, the number of callback-records will increase exponentially when more enums are added and quickly reach abnormal numbers, making this strategy relevant only when there are but a few enums.

CombineAllPossible2Combinations - The Default Combine Strategy

Whereas CombineCompletely aims to provide a full factorial experiment, this runner attempts to setup an experiment with a multi-level fractional factorial design, where each enum-constant will be in the same record as each of the other enums' constants at least once.

The consequence of the above is that this strategy will produce the same callback-records as the CombineCompletely strategy when there are only one or two enums but with three or more enums there will be less callback-records. E.g. for the above case CombineCompletely would produce 84 callback-records, whereas CombineAllPossible2Combinations would produce only 42, which is what it takes to make sure each of the 7 Username constants will be combined with each of the 6 WorkhoursPerWeek constants (6 x 7 = 42). Since the Password constants are only two they can just hang around for the ride and thereby be combined with each of the other constants at least three times.

The idea is that if there is one enum-constant that will cause a test-failure when combined with a certain constant from another enum then there will be at least one such callback-record and it will cause a test-failure!
It is regarded to be much less likely that a certain failure occurs only when three specific enum-constants are in the same record, why this strategy takes the risk of overlooking that problem for the benefit of much fewer callback-records.

BddRunner - Embryonic support for Behavior-Driven Development (BDD)

The bartenders that serve Mockito have said that "Behavior Driven Development style of writing tests uses //given //when //then comments as fundamental parts of your test methods.". As an attempt to honor this motto there is the runner-class BddRunner, which is an extension to CallbackParamsRunner. It recognizes methods that are annotated with @Given, @When or @Then and executes those methods in the ~BDD~ order. It would make our test-class look somewhat different:

@RunWith(BddRunner.class)
public class TestPersonBuilderImplWithBddRunner {

  /* ... PARAMETER MODEL ... */
  Person person;
  PersonBuilder builder = new PersonBuilderImpl();                     // 1

  /* ... WHEN METHOD ... */
  @When void personIsBuilt() {
    person = builder.build();                                          // 4
  }

  /* ... PARAMETER VALUES ... */

  enum Username {
    BAR_FOOL("barfoo"),
    FULL_BAR("foobar");

    private String username;

    Username(String username) { this.username = username; }

    @Given void builderUsername(PersonBuilder builder) {
      builder.username(username);                                      // 2
    }

    @Then void verifyPersonUsername(Person person) {
      assertEquals("Verify username", username, person.getUsername()); // 5
    }
  }

  enum Password {
    SHORTEST_PWD("s%s2H"),
    LONG_PWD("sfe87e2&%dfrHUIdw4gD&3d");

    private String password;

    Password(String password) { this.password = password; }

    @Given void builderPassword(PersonBuilder builder) {
      builder.password(password);                                      // 3
    }

    @Then void verifyPersonPassword(Person person) {
      assertEquals("Verify password", password, person.getPassword()); // 6
    }
  }
}

The test-class differs significantly from the examples that are presented near the top of this article (The Test-Class - Parameterized Versus CallbackParams) but it still performs the exact same operations on PersonBuilderImpl and Person. The difference is that the test-execution is no longer controlled by any test-method. (In fact, BddRunner does not allow any @Test-annotated methods.) Instead the @Given-annotated methods are executed first, followed by the @When-annotated ones and finally the @Then-annotated methods, i.e. one could say there is an invisible test-method:

invisible ghostMethod(Username u, Password p) {

  // given
  u.builderUsername(builder); p.builderPassword(builder);

  // when
  personIsBuilt();

  // then
  u.verifyPersonUsername(person); p.verifyPersonPassword(person);
}

It shall be noticed that the fields of the test-class now defines the the "PARAMETER MODEL" and constitutes a sort of context, from which the @Given-, @When- and @Then-methods get their arguments. (Well, being a member of the test-class itself the @When method is able to access the fields directly.)

The test-runs

Just like its super-class, BddRunner composes callback-records by combining the enum-constants. The resulting test-runs:

  • test[SHORTEST_PWD, BAR_FOOL]
  • test[LONG_PWD, FULL_BAR]
  • test[SHORTEST_PWD, FULL_BAR]
  • test[LONG_PWD, BAR_FOOL]

I.e. the invisible test-method has been named 'test' and the method-name has been appended with the callback-record, as usual ...

Advantages

It is easy to spot that BddRunner has a number of advantages compared to CallbackParamsRunner. For one the test-class will have less code because ...

  • No need to specify any callback-interface
  • No test-method
  • The enum-constants do not need to implement any callback-interface and the methods no longer need to be public.

Also - the enum-methods can have arbitrary names, which allows for more comprehensive method-names. - The annotations will be there to define their roles in the overall lifecycle.

Disadvantages

Though demanding more words to explain, there is another side of the coin ...

  • Less help from the IDE and compiler. - Whereas IDEs and compilers will complain about unimplemented interface-methods, there are no development tools alerting you about forgotten @Given-, @When- or @Then-annotations.
  • Enum-constants are less reusable. Every test-class that use a specific enum must keep in mind that all of its annotated methods will be invoked! For CallbackParamsRunner the test-method controls the invocations, therewith allowing the enums to have extra methods that are not executed by every test-method.

Maintenance advantages still apply.

And as can be verified by having the BddRunner test try the maintenance cases above (Additional Parameter Values, Additional Property, Default Value, Remove a Property), all of the maintenance advantages of CallbackParamRunner still apply ...

Beyond the Happy Paths

This article did only come up with test-examples for happy-path testing but at the beginning of the article it was said that functionality that was tested in this article suited the CallbackParams framework because:

  1. Front-End to Back-End:

    Parameter data is specified at the front-end and is expected to show up at the back-end. E.g. username is specified at the front-end with PersonBuilder.username(String) and is expected to show up at the back-end as Person.getUsername().

  2. Validation and Default-Values:

    Validation of parameter data is expected to take place before it reaches the back-end. E.g. if an empty string is specified as password it should result in a validation exception, which is expected to be thrown by PersonBuilder.password(String) or PersonBuilder.build().

The code-examples in this article have X-rayed #1. #2 will be evaluated in part 2 of this article series. For now it will only be said that the patterns that can be used with CallbackParams will allow an individual enum-constant to flag whether it causes a certain validation failure, while making it possible to stick to a single (well-structured) test-method, which is used regardless of whether the testrun's callback-record will stick to happy-path or cause a validation failure.