Static methods have one powerful, but at the same time highly undesirable feature: they can be called from any place in the code, without really being able to regulate the order of their call. Often such control is very important, but sometimes the order does not make very much sense. For example, checks in unit tests can often not be done in a very strict order. And in order to ensure that all checks are completed in the tested unit, the same verifyNoMoreInteractions(...)
static method verifyNoMoreInteractions(...)
. Sometimes you can mistakenly call such a method even before the last verify(...)
and then with chagrin observe the "red" test. But what if to shift the care of the procedure for performing checks on the compiler itself?
Suppose a test module has the following test:
public abstract class AbstractStructuredLoggingTest<T> { private final IStructuredLogger mockStructuredLogger = mock(IStructuredLogger.class); private T unit; @Nonnull protected abstract T createUnit(@Nonnull IStructuredLogger logger); protected final IStructuredLogger getMockStructuredLogger() { return mockStructuredLogger; } protected final T getUnit() { return unit; } @Before public void initializeMockStructuredLogger() { // . `selfAnswer()` -- , fluent- when(mockStructuredLogger.begin()).thenAnswer(selfAnswer()); when(mockStructuredLogger.put(any(LogEntryKey.class), any(Object.class))).thenAnswer(selfAnswer()); when(mockStructuredLogger.log(any(Scope.class), any(Severity.class), any(String.class))).thenAnswer(selfAnswer()); when(mockStructuredLogger.end()).thenAnswer(selfAnswer()); // ( , ). -- . unit = createUnit(mockStructuredLogger); } @After public void resetMockStructuredLogger() { try { // , verifyNoMoreInteractions(mockStructuredLogger); } finally { // , , ... reset(mockStructuredLogger); } } }
public final class AdministratorServiceStructuredLoggingTest extends AbstractStructuredLoggingTest<IAdministratorService> { private static final String USERNAME = "john.doe"; private static final String PASSWORD = "opZK2lkXa"; private static final String FIRST_NAME = "john"; private static final String LAST_NAME = "doe"; private static final String EMAIL = "john.doe@acme.com"; @Nonnull protected IAdministratorService createUnit(@Nonnull final IStructuredLogger logger) { return createAdministratorService(logger); } @Test public void testCreate() { final T unit = getUnit(); unit.create(USERNAME, PASSWORD, FIRST_NAME, LAST_NAME, EMAIL); final IStructuredLogger mockStructuredLogger = getMockStructuredLogger(); verify(mockStructuredLogger).put(eq(OPERATION_CALLER_CLASS), any(IAdministratorService.class)); verify(mockStructuredLogger).put(eq(OPERATION_CALLER_METHOD), any(Method.class)); verify(mockStructuredLogger).put(eq(OPERATION_TYPE), eq(CREATE)); verify(mockStructuredLogger).put(eq(OPERATION_OBJECT_TYPE), eq(ADMINISTRATOR)); verify(mockStructuredLogger).put(eq(VALUE_ADMINISTRATOR_NAME), eq(USERNAME)); verify(mockStructuredLogger).put(eq(VALUE_FIRST_NAME), eq(FIRST_NAME)); verify(mockStructuredLogger).put(eq(VALUE_LAST_NAME), eq(LAST_NAME)); verify(mockStructuredLogger).put(eq(VALUE_EMAIL), eq(EMAIL)); verify(mockStructuredLogger).log(eq(APP_DEV), eq(INFO), any(String.class)); verifyNoMoreInteractions(mockStructuredLogger); } }
It’s easy to guess exactly what this test is testing: it checks whether the tested method of the unit has called all the important methods of the structured logger. Since at the end of the test there is also called verifyNoMockInteractions(...)
, it is guaranteed that the mock has no methods left for which no checks have been written. By the way, the interface of the structured logger is extremely simple, but I will bring it here in a somewhat truncated form, because the code is taken from a real project.
public interface IStructuredLogger { // , , . @Nonnull IStructuredLogger begin() throws IllegalStateException; // , . // key -- (enum) (OPERATION_CALLER_CLASS, VALUE_FIRST_NAME ..) // value -- @Nonnull IStructuredLogger put(@Nonnull LogEntryKey key, @Nullable Object value) throws IllegalStateException; // . // scope -- (, APP_DEV -- ) // severity -- , , (ERROR, INFO ..) // message -- , @Nonnull IStructuredLogger log(@Nonnull Scope scope, @Nonnull Severity severity, @Nonnull String message) throws IllegalStateException; // begin() @Nonnull IStructuredLogger end() throws IllegalStateException; }
As mentioned above, the static methods with which the test is littered do not guarantee that all types of checks will be implemented. And, for sure, such a test will end with an error. By types of checks in this test, I mean the ability to determine:
In fact, there is a finite set of requirements that could be made to execute in a certain order.
To solve this problem, you can consider options using the strategy template , where there would be a certain interface with methods for each type of check, and each method would be responsible for its own aspect of logging. Or, for example, a template method . But it is obvious that such approaches would be very cumbersome, unreliable in terms of guaranteeing the separation of aspects according to their respective methods. Yes, and readability would have to sacrifice, which certainly would not want to do.
Five years ago, I remember, I came across an article on the Internet describing the implementation of a builder pattern , which, using some not entirely obvious techniques, ensured that the creation of a complex object would be carried out in the correct order. The following is meant: for a certain object-builder, you can first call only the setFoo()
method, and only then setBar()
followed by build()
. And in no way in another order, because the compiler keeps an eye on the order!
A similar approach, but with a different implementation, can be used to simplify the writing of tests according to the rules described above strictly in the same order, without using the template method. Given the somehow formalized features of testing in this case, you can create a set of such interfaces, which will deal with the coupling of such transitions. And for convenience, you can use a fluid interface that allows you to build an elegant chain of checks.
// , , @FunctionalInterface public interface IOperationCallerVerificationStep { // unitMatcherSupplier -- // methodMatcherSupplier -- , // , -- @Nonnull IOperationTypeVerificationStep withOperationCaller( @Nonnull Supplier<?> unitMatcherSupplier, @Nonnull Supplier<Method> methodMatcherSupplier ); // , , . // , -. // (, , [APT](http://docs.oracle.com/javase/7/docs/technotes/guides/apt/)) // . , __ // Method, . @Nonnull default IOperationTypeVerificationStep withOperationCaller( @Nonnull final Supplier<?> unitMatcherSupplier ) { return withOperationCaller(unitMatcherSupplier, () -> any(Method.class)); } }
// , @FunctionalInterface public interface IOperationTypeVerificationStep { // operationTypeMatcherSupplier -- // objectTypeMatcherSupplier -- , @Nonnull IValueVerificationStep withOperationType( @Nonnull Supplier<OperationType> operationTypeMatcherSupplier, @Nonnull Supplier<ObjectType> objectTypeMatcherSupplier ); }
// , (.., , ) public interface IValueVerificationStep { // logEntryKeyMatcherSupplier -- // valueMatcherSupplier -- // , , // -- .., , // . @Nonnull IValueVerificationStep withValue( @Nonnull Supplier<LogEntryKey> logEntryKeyMatcherSupplier, @Nonnull Supplier<?> valueMatcherSupplier ); // . , . @Nonnull ILogVerificationStep then(); }
// , , @FunctionalInterface public interface ILogVerificationStep { // scopeMatcherSupplier -- // severityMatcherSupplier -- // messageMatcherSupplier -- // , void withLog( @Nonnull Supplier<Scope> scopeMatcherSupplier, @Nonnull Supplier<Severity> severityMatcherSupplier, @Nonnull Supplier<String> messageMatcherSupplier ); // default void withLog( @Nonnull final Supplier<Scope> scopeMatcherSupplier, @Nonnull final Supplier<Severity> severityMatcherSupplier ) { withLog(scopeMatcherSupplier, severityMatcherSupplier, () -> any(String.class)); } // , ( APP, DEV) default void withLog( @Nonnull final Supplier<Severity> severityMatcherSupplier ) { withLog(() -> eq(APP_DEV), severityMatcherSupplier, () -> any(String.class)); } }
Almost all interfaces turned out to be annotated as @FunctionalInterface
, although this is not a necessity. However, the variadic interface has two methods, since you need to somehow report the completion of the verification of the logging of the operation arguments. So, the original test code can now take the following form:
public abstract class AbstractStructuredLoggingTest<T> { private final IStructuredLogger mockStructuredLogger = mock(IStructuredLogger.class); private T unit; @Nonnull protected abstract T createUnit(@Nonnull IStructuredLogger logger); // -! , /*protected final IStructuredLogger getMockStructuredLogger() { return mockStructuredLogger; }*/ protected final T getUnit() { return unit; } @Before public void initializeMockStructuredLogger() { when(mockStructuredLogger.begin()).thenAnswer(selfAnswer()); when(mockStructuredLogger.put(any(LogEntryKey.class), any(Object.class))).thenAnswer(selfAnswer()); when(mockStructuredLogger.log(any(Scope.class), any(Severity.class), any(String.class))).thenAnswer(selfAnswer()); when(mockStructuredLogger.end()).thenAnswer(selfAnswer()); unit = createUnit(mockStructuredLogger); } @After public void resetMockStructuredLogger() { try { verifyNoMoreInteractions(mockStructuredLogger); } finally { reset(mockStructuredLogger); } } // , . . // , . // -. , verify(...), // . verifyNoMoreInteractions , // . protected final IOperationCallerVerificationStep verifyLog() { return (unitMatcherSupplier, methodMatcherSupplier) -> { verify(mockStructuredLogger).put(eq(OPERATION_CALLER_CLASS), unitMatcherSupplier.get()); verify(mockStructuredLogger).put(eq(OPERATION_CALLER_METHOD), methodMatcherSupplier.get()); return (IOperationTypeVerificationStep) (operationTypeMatcherSupplier, objectTypeMatcherSupplier) -> { verify(mockStructuredLogger).put(eq(OPERATION_TYPE), operationTypeMatcherSupplier.get()); verify(mockStructuredLogger).put(eq(OPERATION_OBJECT_TYPE), objectTypeMatcherSupplier.get()); return new IValueVerificationStep() { @Nonnull @Override public IValueVerificationStep withValue(@Nonnull final Supplier<LogEntryKey> logEntryKeyMatcherSupplier, @Nonnull final Supplier<?> valueMatcherSupplier) { verify(mockStructuredLogger).put(logEntryKeyMatcherSupplier.get(), valueMatcherSupplier.get()); return this; } @Nonnull @Override public ILogVerificationStep then() { return (scopeMatcherSupplier, severityMatcherSupplier, messageMatcherSupplier) -> verify(mockStructuredLogger).log(scopeMatcherSupplier.get(), severityMatcherSupplier.get(), messageMatcherSupplier.get()); } }; }; }; } }
And, actually, the simplification itself, for the sake of which the basic functionality of the tests was complicated:
public final class AdministratorServiceStructuredLoggingTest extends AbstractStructuredLoggingTest { private static final String USERNAME = "usr"; private static final String PASSWORD = "qwerty"; private static final String FIRST_NAME = "john"; private static final String LAST_NAME = "doe"; private static final String EMAIL = "usr@mail.com"; @Nonnull protected IAdministratorService createUnit(@Nonnull final IStructuredLogger logger) { return createAdministratorService(logger); } @Test public void testCreate() { final T unit = getUnit(); unit.create(USERNAME, PASSWORD, FIRST_NAME, LAST_NAME, EMAIL); verifyLog() .withOperationCaller(() -> any(IAdministratorService.class)) .withOperationType(() -> eq(CREATE), () -> eq(ADMINISTRATOR)) .withValue(() -> eq(VALUE_ADMINISTRATOR_NAME), () -> eq(USERNAME)) .withValue(() -> eq(VALUE_FIRST_NAME), () -> eq(FIRST_NAME)) .withValue(() -> eq(VALUE_LAST_NAME), () -> eq(LAST_NAME)) .withValue(() -> eq(VALUE_EMAIL), () -> eq(EMAIL)) .then() .withLog(() -> eq(INFO)); } }
As for me, the code has become more reliable and very beautiful. Yes, and more convenient too. And most importantly - any smart IDE when you click on a dot itself tells you what the next step should be. Thus, both the compiler and IDE add a little more confidence in how well the test is written. By the way, why are Supplier
and lambda expressions used? The fact is that the Mockito checks whether stubs are transmitted directly to moki, and if not, throws an exception. In fact, here, as far as I know, the rules are a bit more complicated, and, for example, Mockito ignores anonymous classes. And because of this fact, there is a small loophole: Mockito does not track the transfer of matchrs through return
, which opens the way to the use of lambda. This complicates the code and readability a bit, but lambdas do a pretty good job of it.
The result was the following result:
Source: https://habr.com/ru/post/309752/