The following is what I consider an extension of @OldCurmudgeon's ansswer with three significant deviations/diclaimers:
- It's uses Java 8's
Stream
and Predicate
.
- It turns your
Response
class into an enum
.
- Each
Response
enum
value interacts solely with an Item
object, i.e. not to be used as a 'generic' Response
object.
If you are not fine with any of the above, then Response.fail()...
just take the following as cursory reading... :)
So, the enum
approach isn't too bad an idea, since your original implementation did impose some kind of ordering for the validation steps. Seeing how each validation is chained to an Item
object, and seeing how a Response
object constructed with a description is returned from within each if
statement when the validation fails, got me thinking...
- What if a
Response
can validate itself against an Item
in an orderly fashion?
- In Java 8 lingo, what if
Response
implements the Predicate
interface so that it can test()
an Item
?
- Also, what if the description is simply the textual representation of my
Response
values?
- What would all these ideas look like?
public enum Response implements Predicate<Item> {
NOT_FOUND(i -> i != null, "Can't find item"),
NOT_NEW(i -> i.getState() == State.NEW, "Can't send first inquiry to customer for items not in 'NEW' state"),
NO_TICKET_NUMBER(Item::hasNumber, "Can't find ticket in OTRS"),
NOT_ACTIVE(Item::isActive, "Item is not active anymore"),
NOT_READY(Item::isReady, "Item is not finalized, first inquiry can be send only for processed items"),
SUCCESS(i -> true, "Validation OK");
private final Predicate<? super Item> successCase;
private final String failReason;
private Response(Predicate<? super Item> successCase, String failReason) {
this.successCase = successCase;
this.failReason = failReason;
}
@Override
public boolean test(Item t) {
return successCase.negate().test(t);
}
@Override
public String toString() {
return failReason;
}
}
Returning failReason
from toString()
is self-explanatory, so I'll explain why we take in the successful condition aka Predicate
, but negate()
it when we do our test()
-ing...
- Readability, in an unconventional sense. The constructor arguments can be read as "test for this condition, if it fails use this reason". That's why the arguments are named
successCase
and failReason
(ok, only the SUCCESS
case doesn't make much sense for failReason
).
- So that I can use method references for half the cases.
- So that I can clearly distinguish the
SUCCESS
case through the negation, by making sure we will not preemptively return it first as described below.
The entire sendFirstInquiryEmail()
then becomes just:
private static Response sendFirstInquiryEmail(long itemId) {
Item item = itemService.findById(itemId);
Response result = EnumSet.allOf(Response.class).stream()
.filter(r -> r.test(item)).findAny().orElse(Response.SUCCESS);
if (result == Response.SUCCESS) {
send(item);
}
return result;
}
In steps:
- Stream on our
Response
values.
- Filter for those where doing our
test()
returns true
since we are checking for any validation failures ('failure') first.
findAny()
... find any Response
value corresponding to the failure. This can be findFirst()
too.
- If there are no failures,
findAny()
will yield an Optional.empty()
, so this is when we do an orElse()
to return our Response.SUCCESS
value.
Can we test this? Of course! Just one more thing... we started with three significant deviations/disclaimers, and I need a fourth for the test to work properly:
Item
is an interface
If it's not, then consider the following as more cursory reading too if you have not yet done so. :D
public class ResponseTest {
enum TestItem implements Item {
NOT_FOUND(null),
NOT_NEW(State.OLD), // assuming State.OLD exists
NO_TICKET_NUMBER(false, false, false),
NOT_ACTIVE(true, false, false),
NOT_READY(true, true, false),
SUCCESS(true, true, true);
private final State state;
private final boolean hasNumber;
private final boolean isActive;
private final boolean isReady;
private TestItem(State state) {
this(state, false, false, false);
}
private TestItem(boolean hasNumber, boolean isActive, boolean isReady) {
this(State.NEW, hasNumber, isActive, isReady);
}
private TestItem(State state, boolean hasNumber, boolean isActive,
boolean isReady) {
this.state = state;
this.hasNumber = hasNumber;
this.isActive = isActive;
this.isReady = isReady;
}
@Override
public State getState() {
return state;
}
@Override
public boolean hasNumber() {
return hasNumber;
}
@Override
public boolean isActive() {
return isActive;
}
@Override
public boolean isReady() {
return isReady;
}
public void verify() {
assertThat(testSendFirstInquiryEmail(state == null ? null : this),
equalTo(Response.valueOf(name())));
System.out.println("Verified for: " + Response.valueOf(name()));
}
}
@DataProvider(name = "test-cases")
public Iterator<Object[]> getTestCases() {
return EnumSet.allOf(TestItem.class).stream().map(v -> new Object[] { v })
.iterator();
}
@Test(dataProvider = "test-cases")
public void test(TestItem item) {
item.verify();
}
private static Response testSendFirstInquiryEmail(Item item) {
return EnumSet.allOf(Response.class).stream()
.filter(r -> r.test(item)).findAny().orElse(Response.SUCCESS);
}
}
The use of the TestItem
enum
is to easily generate different Item
values that will cause the validation to fail at a rule. More importantly, the names on TestItem
and Response
matches so that we can do a nifty name()
trick in our assertion:
// Hamcrest Matchers library
assertThat(testSendFirstInquiryEmail(state == null ? null : this),
equalTo(Response.valueOf(name())));
What that describes is to call our test method testSendFirstInquiryEmail()
with the current TestItem
item (or null
- the checking of state == null
is just a convenient way of testing this), and assert that the Response
value returned has the same name.
@Test
and @DataProvider
annotations come from TestNG to handle parameterized testing, and what they do are to iteratively test each TestItem
by calling its verify()
method.
Test output snippet:
Verified for: Can't find item
Verified for: Can't send first inquiry to customer for items not in 'NEW' state
Verified for: Can't find ticket in OTRS
Verified for: Item is not active anymore
Verified for: Item is not finalized, first inquiry can be send only for processed items
Verified for: Validation OK
PASSED: test(NOT_FOUND)
PASSED: test(NOT_NEW)
PASSED: test(NO_TICKET_NUMBER)
PASSED: test(NOT_ACTIVE)
PASSED: test(NOT_READY)
PASSED: test(SUCCESS)