CallbackParams is a JUnit add-on for parameterized testing.
Part 2 continues where part 1 ended, so make sure you have read part 1 before you continue. - It is mandatory knowledge!
The test-examples in this article will continue to test the class PersonBuilderImpl, which is the black-box implementation of PersonBuilder ...
public interface PersonBuilder { void username(String username); void password(String password); Person build(); }
... which is used to produce instances of Person:
public interface Person { String getUsername(); String getPassword(); }
This article will show how to expand the test-class TestPersonBuilderImplWithCallbackParams so that its single test-method can be used to verify both happy-path and validation-failure scenarios.
Even developers that have been hardcore TDD evangelists for a long time might find themselves surprised by how well CallbackParams and JUnit are able to cooperatively reuse an unmodified happy-path test-method for other (not so happy) execution paths.
The desire in this article is to customize the test-class so that all it takes to add a parameter-value for validation failure is to add the particular enum-constant and decorate it with an annotation that specifies which validation-failure message(s) to expect:
@RunWith(CallbackParamsRunner.class) public class TestPersonBuilderImplWithCallbackParams { /* ... PARAMETER MODEL ... */ interface CreationPropertyCallbacks { void setValues(PersonBuilder builder); void verifyValues(Person person); } /** For specifying which validation-failure * message(s) to expect from an enum-constant */ @Retention(RetentionPolicy.RUNTIME) @interface Invalid { String value(); } ... /* ... PARAMETER VALUES ... */ enum Username implements CreationPropertyCallbacks { ... } enum Password implements CreationPropertyCallbacks { @Invalid("password.tooshort") PWD_TOO_SHORT("s%S2"), SHORTEST_PWD("s%s2H"), LONG_PWD("sfe87e2&%dfrHUIdw4gD&3d"); ... }
In the ~desirable~ example above the validation-failure causing constant PWD_TOO_SHORT has been added to the enum Password. Note how it is possible to declare the annotation class for validation-failure messages within the test-class itself. This is possible because the CallbackParams framework does not hardwire any special annotation class for this matter.
Fact is that the internals of CallbackParams have very little hardwired for these situations and the context of this little deals with default implementations of callback-interfaces rather than validation failures as such.
If we suppose a validation failure reveals itself as an IllegalArgumentException then this can be verified by having the test-class incorporate this section of code ...
... /* ... VALIDATION FAILURE EXPECTATIONS ... */ @Rule public ExpectedExceptionSweeps validationFailure = new ExpectedExceptionSweeps(); @Before public void setupValidationFailure( Collector<Invalid> invalidCollector) { validationFailure .sweep(invalidCollector, IllegalArgumentException.class); } ...
... that makes use of the JUnit Rules API and some framework resources of CallbackParams - the class ExpectedExceptionSweeps and its related callback-interface Collector<A extends Annotation>. These components are all further described later in this article. To benefit the most from these features JUnit-4.7 is needed but there are ways to get by with earlier versions of JUnit as well.
In case the validation-failures do not reveal themselves as exceptions, this article also contains an example on how to verify validation failures when using Struts2. Hopefully, it can spawn some ideas on how this and similar types of failure-path testing (as opposed to happy-path testing) can be simplified by using CallbackParams ...
This section will explain the details of the above code-section "VALIDATION FAILURE EXPECTATIONS" and show how it works. If you care less about how-it-works and prefers to learn how-to-do then you can successfully skip this section and continue your reading at Multiple Validation Sweeps, JUnit 4.6 or Earlier - Adaptive Rules or Other Validation Patterns.
The package org.callbackparams.sandbox provides the convenient callback-interface Collector<A extends Annotation>, which has only one method:
<T> void collect(Collection<T> collection);
This callback-interface could be used for collecting the expected validation-failure messages from the current callback-record, by having the failure-causing enums implement this interface ...
... /* ... VALIDATION FAILURE EXPECTATIONS ... */ ... @Before public void setupValidationFailure( Collector<Invalid> invalidCollector) { Collection<String> expectedMessages = new HashSet<String>(); invalidCollector.collect(expectedMessages); ... } /* ... PARAMETER VALUES ... */ enum Username implements CreationPropertyCallbacks { ... } enum Password implements CreationPropertyCallbacks, Collector<Invalid> { PWD_TOO_SHORT("s%S2", "password.tooshort"), SHORTEST_PWD("s%s2H"), LONG_PWD("sfe87e2&%dfrHUIdw4gD&3d"); private String password; String[] validationFailures; Password(String password, String... validationFailures) { this.password = password; this.validationFailures = validationFailures; } public <T> void collect(Collection<T> expectedMessages) { Collections.addAll(expectedMessages, (T[])validationFailures); } ... }
This way the callback-interface Collector<Invalid> will collect all the possible validation-failure messages of PWD_TOO_SHORT to the collection expectedValidationFailureMessages (for every test that has PWD_TOO_SHORT in its callback-record). This is exactly what also took place under-the-hood in the feature-complete example above. However, for the feature-complete example there was no need for the enum to implement the Collector<Invalid> callback-interface. - All it took was to have the validation-failure messages specified with the annotation @Invalid:
... enum Password implements CreationPropertyCallbacks, Collector<Invalid> { @Invalid("password.tooshort") PWD_TOO_SHORT("s%S2", "password.tooshort"), SHORTEST_PWD("s%s2H"), ...
This magic was possible because Collector<A extends Annotation>, on top of specifying its callback-method, also uses a special API to specify its default implementation ...
The CallbackParams' API for default callback-interface implementations came about as an elegant solution for situations that would otherwise cause separate enums to implement a callback-interface in identical manners. So far the Collector<A extends Annotation> interface seems to be its most useful usage.
The API is still undergoing heavy modifications so third-parties should wait for the 1.0.0-release before they try to make use of it on their own .
However, using the Collector<A extends Annotation> interface should be safe, even though it uses the unstable API. - The interface's usage of the API is hidden from the developer and it will stay up-to-date with future API-modifications.
Version 4.7 of JUnit introduced rules, an API that will here be used in a manner that closely resembles an example near the end of this post.
If the built-in callback-interface Collector<A extends Annotation> is used to collect the expected exception-messages then JUnit's built-in rule ExpectedException can be used to verify the expected validation failure:
... /** For specifying which validation-failure * message(s) to expect from an enum-constant */ @Retention(RetentionPolicy.RUNTIME) @interface Invalid { String value(); } /* ... VALIDATION FAILURE EXPECTATIONS ... */ @Rule public ExpectedException validationFailure = ExpectedException.none(); @Before public void setupValidationFailure( Collector<Invalid> invalidCollector) { Collection<String> expectedMessages = new HashSet<String>(); invalidCollector.collect(expectedMessages); if (false == expectedMessages.isEmpty()) { validationFailure.expect(IllegalArgumentException.class); validationFailure.expectMessage( org.hamcrest.collection.IsIn.isIn(expectedMessages)); } } ...
If the @Before-method setupValidationFailure(...) finds any validation-failures for its expectedMessages collection, it will setup an exception expectation for the @Test-method by telling the rule validationFailure to expect an IllegalArgumentException, which message must be one of the validation failure messages that can be found in the collection expectedMessages, which was populated with possible messages through cunning usage of the callback-interface Collector<Invalid> !
However, the feature-complete example above did not use the rule ExpectedException but the CallbackParams-rule ExpectedExceptionSweeps ...
The above preparation of ExpectedException is somewhat cumbersome. As an attempt to simplify this the CallbackParams framework offers the rule org.callbackparams.sandbox.ExpectedExceptionSweeps, which reproduces the desired functionality of ExpectedException but encapsulates the fine-grained preparations:
... /** For specifying which validation-failure * message(s) to expect from an enum-constant */ @Retention(RetentionPolicy.RUNTIME) @interface Invalid { String value(); } /* ... VALIDATION FAILURE EXPECTATIONS ... */ @Rule public ExpectedExceptionSweeps validationFailure = new ExpectedExceptionSweeps(); @Before public void setupValidationFailure( Collector<Invalid> invalidCollector) { validationFailure .sweep(invalidCollector, IllegalArgumentException.class); } ...
In short - the above code-chunk turns the happy-path test-class into a test-class that can verify both happy-path and validation failure scenarios, by annotating the failure-causing enum constants with @Invalid(<message>).
Please note that the test-method is unchanged and still looks like a happy-path test-method!
From the above code-examples one could get the impression that the benefits of the class ExpectedExceptionSweeps over ExpectedException are about "four code-lines less" but there is more to this. - It concerns scenarios where there are multiple validation sweeps or JUnit rules are not supported (e.g. earlier versions of JUnit) ...
The term "Multiple Validation Sweeps" here refers to that applications commonly do not perform every single validation in one single sweep but usually take a number of sweeps. If all validations of the first sweep pass then the second sweep is made etc.
The first sweep would usually handle some fail-fast scenarios where the validations can be performed based on the input-data alone, such as whether a text-item is too long or too short. A second sweep could perform some more heavy-weight validations, such as having an inappropriate-language-detector parse the text-item. A third sweep could make some database checks etc.
The first validation-sweep on PersonBuilder could perhaps validate the property-method arguments and throw IllegalArgumentException if not valid, i.e. the validation that has been described above. The build() method can be expected to make a second sweep and throw IllegalStateException in case the method was invoked at a bad moment, such as before username and password have been set.
Since messages specified by the annotation @Invalid are for IllegalArgumentException only, a separate annotation is needed for IllegalStateException:
@RunWith(CallbackParamsRunner.class) public class TestPersonBuilderImplWithCallbackParams { /* ... PARAMETER MODEL ... */ interface CreationPropertyCallbacks { void setValues(PersonBuilder builder); void verifyValues(Person person); } @Retention(RetentionPolicy.RUNTIME) @interface Invalid { String value(); } @Retention(RetentionPolicy.RUNTIME) @interface BadState { String value(); } /* ... VALIDATION FAILURE EXPECTATIONS ... */ ... /* ... TEST METHOD ... */ ... /* ... PARAMETER VALUES ... */ enum Username implements CreationPropertyCallbacks { @BadState("notset.username") USER_NOTSET("irrelevant") { @Override public void setValues(PersonBuilder builder) { /* username is not set */ } }, BAR_FOOL("barfoo"), FULL_BAR("foobar"); ... } enum Password implements CreationPropertyCallbacks { @BadState("notset.password") PWD_NOTSET("irrelevant") { @Override public void setValues(PersonBuilder builder) { /* password is not set */ } }, @Invalid("password.tooshort") PWD_TOO_SHORT("s%S2"), SHORTEST_PWD("s%s2H"), LONG_PWD("sfe87e2&%dfrHUIdw4gD&3d"); ... } ...
The enum-constants USER_NOTSET and PWD_NOTSET force IllegalStateException by overriding the setValues(...) method to make sure their respective properties will not be set. This is pretty much what the WorkhoursPerWeek constant WORKHOURS_DEFAULT does in part I. The difference is that neglection to set username or password will not be excused with any default value.
With the rule ExpectedExceptionSweeps, any additional validation sweep is taken care of by an additional invocation of sweep(...) ...
... /* ... VALIDATION FAILURE EXPECTATIONS ... */ @Rule public ExpectedExceptionSweeps validationFailure = new ExpectedExceptionSweeps(); @Before public void setupValidationFailure( Collector<Invalid> invalidCollector, Collector<BadState> badStateCollector) { validationFailure .sweep(invalidCollector, IllegalArgumentException.class) .sweep(badStateCollector, IllegalStateException.class); } ...
If there are any messages collected through invalidCollector on the first sweep then an expectation is setup for the exception class IllegalArgumentException and the second sweep invocation is ignored. Otherwise badStateCollector of the second sweep is queried for messages and if there is something collected an expectation of the exception class IllegalStateException is setup etc ...
For a happy-path test there will not be any messages collected for any of the sweep invocations!
For situations where JUnit-rules cannot be used a CallbackParams test-class can instead rely on "Adaptive Rules", which is an alternative rules API that is available for CallbackParams test-classes. The rule-class ExpectedExceptionSweeps is an implemention of this API:
... /* ... VALIDATION FAILURE EXPECTATIONS ... */ @Rule public ExpectedExceptionSweeps validationFailure = new ExpectedExceptionSweeps(); ...
Adaptive Rules will usually look like JUnit Rules without the annotations. The main difference is that JUnit Rules is enforced by wrapping the test-method invocation whereas Adaptive Rules is enforced by modifying the test-method byte-code. (Most of the CallbackParams functionality is partly enforced through byte-code modifications.) When an Adaptive Rules class is used under JUnit-4.7, it will (thanks to byte-code modifications) also implement the JUnit Rules API, therewith allowing the developer to choose between Adaptive Rules and JUnit Rules.
Upgrading to the latest version of JUnit is certainly recommended but there will still be situations where JUnit Rules is not supported.
The story is that, in addition to CallbackParams' CallbackParamsRunner, there are plenty of third-party JUnit-runners out there and most of these can in fact be used in conjunction with CallbackParamsRunner (by using the annotation @WrappedRunner). Many of these 3rd-party runners do not support JUnit Rules - but CallbackParams' Adaptive Rules will work anyway, no matter which version of JUnit is used ...
(JUnit-4.x is backward-compatible and supports the JUnit-3.8.x API as well.)
Adaptive Rules can be used when the test-class uses the JUnit-3.8.x API but JUnit Rules is only available when the test-class uses the JUnit-4.x API.
Later in this article there is a Struts2 test example, which also illustrates that there are situations where it is desirable to hold on to the JUnit-3.8.x API.
The demonstrated test-example TestPersonBuilderImplWithCallbackParams is unlikely to cause any insurmountable difficulties but what if the composition of invalid-, badstate- and happy-path enum-constants had been more like this:
@RunWith(CallbackParamsRunner.class) public class TestPersonBuilderImplWithCallbackParams { ... enum A implements CreationPropertyCallbacks { VALID_A(...), @Invalid(...) INVALID_A(...), @BadState(...) BADSTATE_A(...); ... } enum B implements CreationPropertyCallbacks { VALID_B(...), @Invalid(...) INVALID_B(...), @BadState(...) BADSTATE_B(...); ... } enum C implements CreationPropertyCallbacks { VALID_C(...), @Invalid(...) INVALID_C(...), @BadState(...) BADSTATE_C(...); ... } enum D implements CreationPropertyCallbacks { VALID_D(...), @Invalid(...) INVALID_D(...), @BadState(...) BADSTATE_D(...); ... } ...
By explicitly specifying CombineCompletely as combine-strategy - @CombineConfig(strategy=CombineCompletely.class) - the above (3 constants per enum) would result in a total of ...
3 * 3 * 3 * 3 = 81
... 81 callback-records but with the implicit default combine-strategy (CombineAllPossible2Combinations) a total of 9 callback-records will be picked - consequently resulting in 9 testruns.
The above enums each contain one happy-path constant, one causing IllegalArgumentException and one causing IllegalStateException. - Given there will be 9 testruns - is it reasonable to expect 3 happy-path testruns, 3 testruns throwing IllegalArgumentException and 3 throwing IllegalStateException?
The answer is a resounding "No!" ...
There is a very simple explanation - have a look at what is expected from the 81 possible callback-records:
The above implies that out of the default combine-strategies' 9 callback-records, there will be 7 that induce IllegalArgumentException (9 * 65 / 81 = 7.22) - and that is probably not the desired test-coverage.
There is a simple reason for this unbalanced test-coverage. - The first validation sweep sort of represents a fail-fast path, so that if any of the callback-record values happens to induce the exception of the first validation sweep (which happens to be IllegalArgumentException) then the testrun is expected to fail-fast with that exception - before any other validations are reached - no matter what kind of execution paths are implied by the rest of the callback-record. E.g. the testrun for callback-record ...
{A.VALID_A, B.BADSTATE_B, C.INVALID_C, D.BADSTATE_D}
... is expected to fail-fast with an IllegalArgumentException because of C.INVALID_C, in spite of all the other values that imply happy-path (A.VALID_A) or IllegalStateException (B.BADSTATE_B and D.BADSTATE_D).
Is there any workaround to this problem?
With a small configuration tweak - all of the 81 possible callback-records will be tested:
@RunWith(CallbackParamsRunner.class) @CombineConfig( strategy=CombineCompletely.class, maxCount=100) public class TestPersonBuilderImplWithCallbackParams { ...
(To refresh knowledge on how combine-strategies work - please reread the tutorial article "Patterns That Simplify Maintenance"!)
This way all 81 possible callback-records will be tested and no execution path is able to remain untested. The distribution of actual execution paths will of course stay as unbalanced as before but with this strategy it only means that some scenarios are tested much more than necessary. - At least all scenarios will for sure be tested.
With as little as 81 callback-records, among which most represent the fail-fast execution path, this is certainly the best solution. But as pointed out where this combine-strategy is explained in the part 1 tutorial, this will only work for a rather moderate number of callback-values (i.e. enum-constants). The number of testruns produced by this strategy could easily get out of hand as additional enums and constants are appended to the test over time.
This road might appeal to some but please find another ASAP! What is the most treacherous is that when a test is equipped with its first few failure-inducing constants everything will work fine. The reason will be the cheer domination of happy-path constants. - I.e. the large amount of happy-path constants is what makes the testruns' distribution of different execution paths stay reasonable.
As time goes by more failure constants are added and the domination of fail-fast scenarios start to increase and the reason will soon enough become evident to everyone. - The default combine-strategy will not work very well when there are too few happy-path constants and one solution is quickly suggested: "Add more happy-path constants!". Initially this seems to work just fine but as test maintenance continues it might be hard to realize that the problem is still there. The test maintenance will stir into a treadmill that time and again will confront the team with some reoccurring problems:
As depicted in a separate article, the purpose of the combine-machinery is to rescue the developers from the combine burden. But by taking this road the combine burden is only replaced by another set of burdens.
Please try any of the workarounds described in the other sections. They are not without problems of their own but those problems are at least more obvious and therewith less treacherous.
As a backup resource, CallbackParams allows manual combining of callback-records by using the annotation @CallbackRecords, which usage is pretty much equivalent to that of @Parameterized.Parameters in JUnit:
@RunWith(CallbackParamsRunner.class) @CombineConfig( strategy = CombineAllPossible2Combinations.class ) public class TestPersonBuilderImplWithCallbackParams { ... @CallbackRecords public static Collection callbackRecordsOfSpecialInterest() { return Arrays.asList(new Object[][] { {A.VALID_A, B.VALID_B, C.VALID_C, D.VALID_D}, {A.BADSTATE_A, B.VALID_B, C.VALID_C, D.VALID_D}, {A.VALID_A, B.BADSTATE_B, C.VALID_C, D.VALID_D}, {A.VALID_A, B.VALID_B, C.BADSTATE_C, D.VALID_D}, {A.VALID_A, B.VALID_B, C.VALID_C, D.BADSTATE_D}, }); } ...
Five callback-records of special interest are defined. The one and only possible happy-path record (order does not matter) plus the four records that can be combined by having a single bad-state inducing constant combined to only happy-path constants.
By specifying callback-records using a @CallbackRecords-annotated static method, the default combine-configuration will not kick-in and no callback-records will be automatically combined. - Only the manually combined records will be used.
However, by explicitly specifying a combine-configuration the manually combined callback-records will be run first and thereafter the specified combine-configuration will kick-in to add some additional records. In the code-chunk above a combine-configuration (which uses the default strategy CombineAllPossible2Combinations) has been specified and nine extra callback-records will be automatically combined. - This will result in a total of 14 testruns.
The reason for specifying the happy-path record is obvious. - By explicitly specifying the one-and-only happy-path record it can be made sure that it will be run.
The bad-state constants each star once as black sheep in what would otherwise be happy-path records. The reason is somewhat far-fetched. - If there is another bad-state inducing constant then the resulting validation failure could quite possibly be the effect of the latter constant and a fail-fast constant would for sure supply the failure effect since it will affect the first validation sweep. Thus the only way to undoubtedly test a badstate constant is to have it surrounded by happy-path constants.
The main drawback of using @CallbackRecords is that it makes maintenance complicated in ways that are recognized from traditional test parameterization, as has been evaluated in the part 1 tutorial.
To workaround the fail-fast dominance when using @CallbackRecords-annotated methods, as described in the previous section, it takes a certain amount of skill to pick the callback-records of specific interest. However, it should not be impossible to design a combine-strategy that is able to combine callback-records that cover the different execution paths in a more well-balanced manner. - Therewith effectively eliminating the problems that have been evaluated in this article-section.
This strategy could perhaps combine records in this manner:
In this case it would pick the only happy-path record available:
{A.VALID_A, B.VALID_B, C.VALID_C, D.VALID_D}
For this case eight records would be picked ...
{A.INVALID_A, B.VALID_B, C.VALID_C, D.VALID_D}
{A.VALID_A, B.INVALID_B, C.VALID_C, D.VALID_D}
{A.VALID_A, B.VALID_B, C.INVALID_C, D.VALID_D}
{A.VALID_A, B.VALID_B, C.VALID_C, D.INVALID_D}
{A.BADSTATE_A, B.VALID_B, C.VALID_C, D.VALID_D}
{A.VALID_A, B.BADSTATE_B, C.VALID_C, D.VALID_D}
{A.VALID_A, B.VALID_B, C.BADSTATE_C, D.VALID_D}
{A.VALID_A, B.VALID_B, C.VALID_C, D.BADSTATE_D}
... i.e. one for each constant that is not a happy-path constant.
For this case it would result in another 24 callback-records:
{A.INVALID_A, B.INVALID_B, C.VALID_C, D.VALID_D}
{A.BADSTATE_A, B.INVALID_B, C.VALID_C, D.VALID_D}
{A.INVALID_A, B.BADSTATE_B, C.VALID_C, D.VALID_D}
{A.BADSTATE_A, B.BADSTATE_B, C.VALID_C, D.VALID_D}
{A.INVALID_A, B.VALID_B, C.INVALID_C, D.VALID_D}
etc ...
These records are important because they are needed to test that two validation-failures do not manage to cancel each other (yes - such bugs do occur) and they also test that multiple validation sweeps take turns properly.
It is also desirable that the happy-path constants and their combinations to other constants are evenly distributed among the chosen callback-records, in case there are more than one happy-path constant per enum - and that is usually the case. The distribution of happy-path constants will probably prove to be the hardest challenge when this combine-strategy is developed.
This is a future combine-strategy that will not be available in the 1.0-release of CallbackParams but hopefully soon thereafter.
The above examples demonstrate situations where a validation failure is expected to result in an exception but there are many situations where that does not apply. Many MVC frameworks collect validation failures of HTTP-form posts in collections, which content can be displayed as error-messages when the user is directed back to the invalid form. This calls for different patterns for validating the failure messages - but these patterns do not necessarily have to be that much different from those of ExpectedExceptionSweeps ...
One example is when an action-class for Struts2/XWork2 produces field-errors. This is done by passing field-name and error-message to the ActionSupport-method addFieldError(<field-name>,<error-message>). When such field-errors are tested with CallbackParams, a field-error inducing enum-constant could perhaps be annotated in this manner:
enum Password implements StrutsFormInput { @FieldError("<field-name>:<error-message>") AN_INVALID_PWD(...), ...
Struts2 action classes can be tested using the class StrutsTestCase of the Struts2 JUnit Plugin, which provide resources that allow developers to test their action-classes by simulating HTTP form requests! This works very well and it is easy to understand the test below without knowledge of neither Struts2 nor the action-class (PersonAction) that is tested. (Some basic knowledge on the servlet-API would be desirable, however.) StrutsTestCase is a subclass of TestCase of the old JUnit-3.8.x API. CallbackParams will detect this and have the runner JUnit38ClassRunner of JUnit-4.4+ take care of the actual test-execution. Therefore the JUnit-4.x API (e.g. @Test, @Before, @Rule etc) cannot be used!
With StrutsTestCase it is possible to simulate HTTP form requests by using MockHttpServletRequest from Spring Framework.
Assume that PersonAction expects the properties username and password and asserts the same validation criteria and same failure messages as the familiar PersonBuilder. - That would allow the enums Username and Password to be reused in this test so that the similarities in section PARAMETER VALUES between TestPersonBuilderImplWithCallbackParams and TestPersonAction would be easy to spot:
@RunWith(CallbackParamsRunner.class) public class TestPersonAction extends StrutsTestCase { /* ... PARAMETER MODEL ... */ interface StrutsFormInput { void prepare(MockHttpServletRequest request); } @Retention(RetentionPolicy.RUNTIME) @interface FieldError { String value(); } ... /* ... PARAMETER VALUES ... */ enum Username implements StrutsFormInput { @FieldError("username:username.notset") USER_NOTSET("irrelevant") { @Override public void prepare(MockHttpServletRequest request) { /* username is not set */ } }, BAR_FOOL("barfoo"), FULL_BAR("foobar"); private String username; Username(String username) { this.username = username; } public void prepare(MockHttpServletRequest request) { request.setParameter("username", username); } } enum Password implements StrutsFormInput { @FieldError("password:password.notset") PWD_NOTSET("irrelevant") { @Override public void prepare(MockHttpServletRequest request) { /* password is not set */ } }, @FieldError("password:password.tooshort") PWD_TOO_SHORT("s%S2"), SHORTEST_PWD("s%s2H"), LONG_PWD("sfe87e2&%dfrHUIdw4gD&3d"); private String password; Password(String password) { this.password = password; } public void prepare(MockHttpServletRequest request) { request.setParameter("password", password); } } }
The enum-constants and their string-values are completely reused. The only differences are the ones that have been forced by the new and Struts2-adjusted PARAMETER MODEL:
Instead of setting their respective PersonBuilder properties, the new callback-interface StrutsFormInput provides a mocked HTTP-request, on which the enums set their respective request parameters.
Instead of the @Invalid and @BadState annotations, failure messages are now communicated through the @FieldError annotation, which messages have been prefixed with the field-names, to which the failure messages are expected to be added in the field-error model of Struts2. Please note that the old annotation classes could have been reused - this is merely a rename-refactoring to better reflect the terminology of Struts2.
Or in lesser words: The only concrete differences are that (1) the enums now set their respective parameter on a mocked HTTP-request and (2) the failure messages have been prefix with their respective field-names!
The test-class is completed by adding the sections VALIDATION FAILURE EXPECTATIONS and TEST METHOD. Most of these could prove highly reusable for other Struts2 tests ...
@RunWith(CallbackParamsRunner.class) public class TestPersonAction extends StrutsTestCase { ... /* ... VALIDATION FAILURE EXPECTATIONS ... */ FailureExpectingActionExecutor executor = new FailureExpectingActionExecutor(); @ParameterizedCallback Collector<FieldError> fieldErrorsCollector; { executor.sweep(fieldErrorsCollector); } /* ... TEST METHOD ... */ public void testPersonForm(StrutsFormInput formInput) throws Exception { formInput.prepare(request); ActionProxy p = getActionProxy("/person.action"); //Wraps PersonAction executor.execute(p); } ...
... because the only giveaway that reveals the PersonAction-relation is the statement getActionProxy("/person.action"), where the string "/person.action" somehow seems to map the class PersonAction - but how?
The answer is that part of StrutsTestCase's greatness lies in how it detects available Struts configuration on class-path, therewith making the ActionProxy instance fully aware on all titbits, e.g. the action mappings.
Please note that the actual simulation and field-error validation has been delegated to the helper class FailureExpectingActionExecutor, which therewith acts as some replacement for ExceptionExceptionSweeps. The common denominator is the method sweep(...), which is used to prepare it with the expected field-errors by passing a Collector instance. (Unlike the ExceptionExceptionSweeps case - the field-errors are not associated with any exception-class, however.) Actual test-execution and failure validation is performed by passing the ActionProxy instance to the method execute(...).
FailureExpectingActionExecutor is a suboptimal but highly reusable hack that is not part of the official CallbackParams release, because it is tightly tied to the Struts2/XWork2 API ...
public class FailureExpectingActionExecutor { private Set<String> expectedFieldErrors = new HashSet<String>(); public FailureExpectingActionExecutor sweep( Collector<?> fieldErrorCollector) { if (expectedFieldErrors.isEmpty()) { fieldErrorCollector.collect(expectedFieldErrors); } else { /*Do nothing - there were expected messages on previous sweeps*/ } return this; } public String execute(ActionProxy proxy) throws Exception { String result = proxy.execute(); List<String> actualFieldErrors = collectActualFieldErrors( ((ActionSupport)proxy.getAction()).getFieldErrors()); if (expectedFieldErrors.isEmpty() && actualFieldErrors.isEmpty()) { assertEquals("Action Execution Result for " + proxy.getActionName(), ActionSupport.SUCCESS, result); } else if (false == actualFieldErrors.isEmpty() && expectedFieldErrors.containsAll(actualFieldErrors)) { assertEquals("Action Execution Result for " + proxy.getActionName(), ActionSupport.INPUT, result); } else { fail(createFailureMessage(proxy, actualFieldErrors)); } return result; } /** * Returns the actual field-errors as strings with the format * "<field-name>:<error-message>", i.e. the format on which * the expected field errors are specified. */ private <M> List<String> collectActualFieldErrors( Map<?, List<M>> fieldErrors) { List<String> actualFieldErrors = new ArrayList<String>(); for (Map.Entry<?, List<M>> fieldMessages : fieldErrors.entrySet()) { for (M msg : fieldMessages.getValue()) { actualFieldErrors.add(fieldMessages.getKey() + ":" + msg); } } return actualFieldErrors; } private String createFailureMessage( ActionProxy proxy, List<String> actualFieldErrors) { ... (READER EXERCISE) ... } }
... but the team behind Struts2 JUnit Plugin are welcome to take the bite.
The above pattens for declarative validation-failure expectations are applicable when using the BddRunner as well. Recall from the previous tutorial article that BddRunner does not support explicit test-methods. It does support @Before and @After methods with callback-interface arguments, however, as well as injection of @ParameterizedCallback-annotated fields. Therefore the above patterns for checking validation failures are fully applicable for BddRunner-tests as well.
The BddRunner-test of the part-one article can be transformed to the equivalent of the above feature-complete example by making the exact same modifications(!):
@RunWith(BddRunner.class) public class TestPersonBuilderImplWithBddRunner { /* ... PARAMETER MODEL ... */ Person person; PersonBuilder builder = new PersonBuilderImpl();} /** For specifying which validation-failure * message(s) to expect from an enum-constant */ @Retention(RetentionPolicy.RUNTIME) @interface Invalid { String value(); } /* ... VALIDATION FAILURE EXPECTATIONS ... */ @Rule public ExpectedExceptionSweeps validationFailure = new ExpectedExceptionSweeps(); @Before public void setupValidationFailure( Collector<Invalid> invalidCollector) { validationFailure .sweep(invalidCollector, IllegalArgumentException.class); } /* ... WHEN METHOD ... */ @When void personIsBuilt() { person = builder.build(); } /* ... PARAMETER VALUES ... */ enum Username { ... } enum Password { @Invalid("password.tooshort") PWD_TOO_SHORT("s%S2"), SHORTEST_PWD("s%s2H"), LONG_PWD("sfe87e2&%dfrHUIdw4gD&3d"); ... }
The Struts2 example can be run with BddRunner after these modifications:
@RunWith(BddRunner.class) public class TestPersonAction extends StrutsTestCase { /* ... PARAMETER MODEL ... */ @Retention(RetentionPolicy.RUNTIME) @interface FieldError { String value(); } /* ... VALIDATION FAILURE EXPECTATIONS ... */ FailureExpectingActionExecutor executor = new FailureExpectingActionExecutor(); @ParameterizedCallback Collector<FieldError> fieldErrorsCollector; { executor.sweep(fieldErrorsCollector); } /* ... WHEN METHOD ... */ @When public void submitPersonForm() throws Exception { ActionProxy p = getActionProxy("/person.action"); //Wraps PersonAction executor.execute(p); } /* ... PARAMETER VALUES ... */ enum Username { @FieldError("username:username.notset") USER_NOTSET("irrelevant") { @Override public void prepare(MockHttpServletRequest request) { /* username is not set */ } }, BAR_FOOL("barfoo"), FULL_BAR("foobar"); private String username; Username(String username) { this.username = username; } @Given public void prepare(MockHttpServletRequest request) { request.setParameter("username", username); } } enum Password { @FieldError("password:password.notset") PWD_NOTSET("irrelevant") { @Override public void prepare(MockHttpServletRequest request) { /* password is not set */ } }, @FieldError("password:password.tooshort") PWD_TOO_SHORT("s%S2"), SHORTEST_PWD("s%s2H"), LONG_PWD("sfe87e2&%dfrHUIdw4gD&3d"); private String password; Password(String password) { this.password = password; } @Given public void prepare(MockHttpServletRequest request) { request.setParameter("password", password); } } }
The former test-method is replaced by a @When-method that no longer knows about the callback-interface StrutsFormInput. The former method-implementations are now annotated with @Given to make sure they are invoked under-the-hood before the @When-method. Therewith the callback-interface StrutsFormInput is no longer used ...
However, the other callback-interface Collector<FieldError> is still used in the same manner as in the CallbackParamsRunner case. This example illustrates that callback-interfaces are still supported when using BddRunner - the only difference being that they no longer can be fed to any test-method for the simple reason that BddRunner disallows test-methods for the benefit of the @Given, @When & @Then methods (which do not accept callback-interfaces directly but all test-class fields are accepted, including those with a @ParameterizedCallback annotation).
The patterns that have been demonstrated in this article do not interfere with those benefits that were demonstrated in part 1 of this article series. For each of the patterns that are demonstrated above it is easy to tell that the maintenance tasks of the part 1 article can still be performed with retained elegance.