There's a (mis)conception that you don't have to test configuration
But what if your configuration is in the form of runnable code? What if it's not just static values?
I wrote an abstraction for notification of our desktop app's users. Basically, it's supposed to abstract away all modal window invocations scattered throughout the code. I wanted to write tests for that. The problem is our CI server may not have a graphical environment (my understanding is it depends on the build agent used). So I discovered a "trick": I gutted the notifier class so that it delegates actual notification to an externally injected Consumer. The consumer, essentially, takes a message string, puts it in a dialog window, and shows it. But that consumer is created and supplied to my notifier by a factory now! So I'm all good, I don't have to test it, it's configuration now. At the same time, I could write (and push) tests for my notifier which still contained (a bit of) logic
But what if I tricked myself? After all, the "configuration" code is still code that may or may not work correctly. Should I have just accepted the fact that I couldn't test my notifier class (or at least, couldn't push the tests)? Would it be more honest and professional than "copping out" of proper testing on a technicality?
At any rate, where does the boundary between code (should be tested) and configuration (shouldn't be tested) lie? Or maybe any runnable (as opposed to parsable) configuration should still be tested, like any other code, shouldn't it?
/* I removed all the javadocs since they were not in English,
and I didn't want to spend time translating them */
public interface Notifier {
void notifyUnlessSuccess(Report report);
void notify(Report report);
void notify(Report report, Report.Status statusThreshold);
}
public abstract class BaseNotifier implements Notifier {
@Override
public void notifyUnlessSuccess(Report report) {
Report.Status leastSevereNonSuccessStatus = Report.Status.WARNING;
notify(report, leastSevereNonSuccessStatus);
}
@Override
public void notify(Report report) {
Report.Status leastSevereStatus = Report.Status.SUCCESS;
notify(report, leastSevereStatus);
}
@Override
public void notify(Report report, Report.Status statusThreshold) {
if (report == null) return;
boolean isStatusThresholdReached = report.getStatus().getSeverity() >= statusThreshold.getSeverity();
if (isStatusThresholdReached) doNotify(report);
}
protected abstract void doNotify(Report report);
}
public class SimpleNotifier extends BaseNotifier {
private final Consumer<Report> defaultConsumer;
public SimpleNotifier(Consumer<Report> defaultConsumer) {
this.defaultConsumer = defaultConsumer;
}
@Override
protected void doNotify(Report report) {
Optional<Consumer<Report>> customReportConsumer = report.getCustomReportConsumer();
boolean hasCustomReportConsumer = customReportConsumer.isPresent();
if (hasCustomReportConsumer) customReportConsumer.get().accept(report);
else defaultConsumer.accept(report);
}
}
// "configuration" :)
public class Notifiers {
private static final Map<Report.Status, String> defaultSummaryMap = new HashMap<>();
private static final Map<Report.Status, Function<String, ? extends BaseMsg>> statusToDialogFunctionMap = new HashMap<>();
static {
defaultSummaryMap.put(Report.Status.SUCCESS, Common.getResource("MSG_ERRS_NO"));
defaultSummaryMap.put(Report.Status.WARNING, Common.getResource("MSG_ERR_WRN"));
defaultSummaryMap.put(Report.Status.FAILURE, Common.getResource("MSG_ERR_CRIT"));
statusToDialogFunctionMap.put(Report.Status.SUCCESS, InfoMsg::new);
statusToDialogFunctionMap.put(Report.Status.WARNING, WarningMsg::new);
statusToDialogFunctionMap.put(Report.Status.FAILURE, ErrorMsg::new);
}
public static Notifier notifier() {
return new SimpleNotifier(defaultConsumer());
}
private static Consumer<Report> defaultConsumer() {
return report -> {
Report.Status reportStatus = report.getStatus();
String summary = Optional.ofNullable(report.getSummary())
.filter(StringUtils::isNotBlank)
.orElse(defaultSummary(reportStatus));
Function<String, ? extends BaseMsg> dialogFunction = statusToDialogFunctionMap.get(reportStatus);
BaseMsg dialog = dialogFunction.apply(summary);
dialog.show();
};
}
private static String defaultSummary(Report.Status reportStatus) {
return defaultSummaryMap.get(reportStatus);
}
}
public interface Report {
String getSummary();
default boolean isSuccess() {
return getStatus() == Status.SUCCESS;
}
Status getStatus();
default Optional<Consumer<Report>> getCustomReportConsumer() {
return Optional.empty();
}
enum Status {
SUCCESS(0), WARNING(10), FAILURE(20);
private final int severity;
Status(int severity) {
this.severity = severity;
}
public int getSeverity() {
return severity;
}
}
}
// the advantage is I can now write and push tests for my Notifier implementation
class SimpleNotifierTest {
@Test
@SuppressWarnings("unchecked")
void notify_ifReportNotSevereEnough_doesNotInvokeReportConsumer() {
Consumer<Report> defaultReportConsumerMock = mock();
willDoNothing().given(defaultReportConsumerMock).accept(any());
SimpleNotifier notifier = new SimpleNotifier(defaultReportConsumerMock);
Report successReport = createReport(Report.Status.SUCCESS);
notifier.notify(successReport, Report.Status.FAILURE);
then(defaultReportConsumerMock).shouldHaveNoInteractions();
}
// more test methods...
public staticmethods. To me, those classes (I wrote them, btw) are somewhat akin to Spring's@Configurationclasses, so I consider them configuration classes