View Javadoc
1   package com.github.valfirst.slf4jtest;
2   
3   import java.util.*;
4   import java.util.function.BiPredicate;
5   import java.util.function.Predicate;
6   import org.slf4j.Marker;
7   import org.slf4j.event.KeyValuePair;
8   import org.slf4j.event.Level;
9   
10  /**
11   * A set of assertions to validate that logs have been logged to a {@link TestLogger}.
12   *
13   * <p>Should be thread safe, as this uses <code>testLogger.getLoggingEvents()</code> by default. The
14   * assertion mode can be switched to use <code>testLogger.getAllLoggingEvents()</code> by calling
15   * {@link #anyThread()}.
16   */
17  public class TestLoggerAssert extends AbstractTestLoggerAssert<TestLoggerAssert> {
18  
19      private MdcComparator mdcComparator = MdcComparator.EXACT;
20  
21      protected TestLoggerAssert(TestLogger testLogger) {
22          super(testLogger, TestLoggerAssert.class);
23      }
24  
25      /**
26       * Changes the assertion mode to verify that log messages have been logged regardless of which
27       * thread actually logged the message.
28       *
29       * @return a {@link TestLoggerAssert} for chaining
30       */
31      public TestLoggerAssert anyThread() {
32          loggingEventsSupplier = actual::getAllLoggingEvents;
33          return this;
34      }
35  
36      /**
37       * Allows the comparison strategy for verifying the MDC contents to be configured.
38       *
39       * <p>The default behaviour is to verify the contents of the MDC are exactly equal to those on the
40       * logging event.
41       *
42       * @param mdcComparator the comparator to use when verifying the contents of the MDC captured by
43       *     the logging event
44       * @return a {@link TestLoggerAssert} for chaining
45       * @see MdcComparator
46       */
47      public TestLoggerAssert usingMdcComparator(MdcComparator mdcComparator) {
48          this.mdcComparator = mdcComparator;
49          return this;
50      }
51  
52      /**
53       * Verify that a log message, at a specific level, has been logged by the test logger.
54       *
55       * @param level the level of the log message to look for
56       * @param message the expected message
57       * @param arguments any optional arguments that may be provided to the log message
58       * @return a {@link TestLoggerAssert} for chaining
59       */
60      public TestLoggerAssert hasLogged(Level level, String message, Object... arguments) {
61          return hasLogged(event(level, message, arguments));
62      }
63  
64      /**
65       * Verify that a log message, at a specific level, has been logged by the test logger in the
66       * presence of a {@link Throwable}.
67       *
68       * @param throwable the throwable that is attached to the log message
69       * @param level the level of the log message to look for
70       * @param message the expected message
71       * @param arguments any optional arguments that may be provided to the log message
72       * @return a {@link TestLoggerAssert} for chaining
73       */
74      public TestLoggerAssert hasLogged(
75              Throwable throwable, Level level, String message, Object... arguments) {
76          return hasLogged(event(throwable, level, message, arguments));
77      }
78  
79      /**
80       * Verify that a {@link LoggingEvent} has been logged by the test logger.
81       *
82       * @param event the event to verify presence of
83       * @return a {@link TestLoggerAssert} for chaining
84       */
85      public TestLoggerAssert hasLogged(LoggingEvent event) {
86          return hasLogged(buildPredicate(event), "Failed to find event:%n  %s", event);
87      }
88  
89      /**
90       * Verify that a {@link LoggingEvent} satisfying the provided predicate has been logged by the
91       * test logger.
92       *
93       * @param predicate the predicate to test against
94       * @return a {@link TestLoggerAssert} for chaining
95       */
96      public TestLoggerAssert hasLogged(Predicate<LoggingEvent> predicate) {
97          return hasLogged(predicate, "Failed to find log matching predicate");
98      }
99  
100     /**
101      * Uses the supplied {@link PredicateBuilder} to construct the predicate with which to Verify that
102      * a matching {@link LoggingEvent} has been logged by the test logger.
103      *
104      * @param predicate the {@link PredicateBuilder} to use to construct the test predicate
105      * @return a {@link TestLoggerAssert} for chaining
106      */
107     public TestLoggerAssert hasLogged(PredicateBuilder predicate) {
108         return hasLogged(predicate.build());
109     }
110 
111     /**
112      * Verify that a log message, at a specific level, has not been logged by the test logger.
113      *
114      * @param level the level of the log message to look for
115      * @param message the expected message
116      * @param arguments any optional arguments that may be provided to the log message
117      * @return a {@link TestLoggerAssert} for chaining
118      */
119     public TestLoggerAssert hasNotLogged(Level level, String message, Object... arguments) {
120         return hasNotLogged(event(level, message, arguments));
121     }
122 
123     /**
124      * Verify that a log message, at a specific level, has not been logged by the test logger in the
125      * presence of a {@link Throwable}.
126      *
127      * @param throwable the throwable that is attached to the log message
128      * @param level the level of the log message to look for
129      * @param message the expected message
130      * @param arguments any optional arguments that may be provided to the log message
131      * @return a {@link TestLoggerAssert} for chaining
132      */
133     public TestLoggerAssert hasNotLogged(
134             Throwable throwable, Level level, String message, Object... arguments) {
135         return hasNotLogged(event(throwable, level, message, arguments));
136     }
137 
138     /**
139      * Verify that a {@link LoggingEvent} has not been logged by the test logger.
140      *
141      * @param event the event to verify absence of
142      * @return a {@link TestLoggerAssert} for chaining
143      */
144     public TestLoggerAssert hasNotLogged(LoggingEvent event) {
145         return hasNotLogged(buildPredicate(event));
146     }
147 
148     /**
149      * Verify that a {@link LoggingEvent} satisfying the provided predicate has _not_ been logged by
150      * the test logger.
151      *
152      * @param predicate the predicate to test against
153      * @return a {@link TestLoggerAssert} for chaining
154      */
155     public TestLoggerAssert hasNotLogged(Predicate<LoggingEvent> predicate) {
156         findEvent(predicate)
157                 .ifPresent(
158                         loggingEvent ->
159                                 failWithMessage("Found %s, even though we expected not to", loggingEvent));
160 
161         return this;
162     }
163 
164     /**
165      * Uses the supplied {@link PredicateBuilder} to construct the predicate with which to Verify that
166      * a matching {@link LoggingEvent} has _not_ been logged by the test logger.
167      *
168      * @param predicate the {@link PredicateBuilder} to use to construct the test predicate
169      * @return a {@link TestLoggerAssert} for chaining
170      */
171     public TestLoggerAssert hasNotLogged(PredicateBuilder predicate) {
172         findEvent(predicate.build())
173                 .ifPresent(
174                         loggingEvent ->
175                                 failWithMessage("Found %s, even though we expected not to", loggingEvent));
176 
177         return this;
178     }
179 
180     /**
181      * Convenience method for a {@link LevelAssert} from a provided test logger.
182      *
183      * @param level the {@link Level} to assert against
184      * @return the {@link LevelAssert} bound to the given {@link Level}
185      */
186     public LevelAssert hasLevel(Level level) {
187         LevelAssert levelAssert = new LevelAssert(actual, level);
188         levelAssert.loggingEventsSupplier = loggingEventsSupplier;
189         return levelAssert;
190     }
191 
192     private TestLoggerAssert hasLogged(
193             Predicate<LoggingEvent> predicate, String failureMessage, Object... arguments) {
194         if (!findEvent(predicate).isPresent()) {
195             String allEvents =
196                     loggingEventsSupplier.get().stream()
197                             .map(Objects::toString)
198                             .reduce((first, second) -> first + "\n  - " + second)
199                             .map(output -> "  - " + output)
200                             .orElse("  <none>");
201             Object[] newArguments = Arrays.copyOf(arguments, arguments.length + 1);
202             newArguments[arguments.length] = allEvents;
203             failWithMessage(
204                     failureMessage + "%n%nThe logger contained the following events:%n%s", newArguments);
205         }
206         return this;
207     }
208 
209     private Predicate<LoggingEvent> buildPredicate(LoggingEvent event) {
210         return new PredicateBuilder()
211                 .withMarkers(event.getMarkers().toArray(new Marker[0]))
212                 .withKeyValuePairs(event.getKeyValuePairs().toArray(new KeyValuePair[0]))
213                 .withThrowable(event.getThrowable().orElse(null))
214                 .withLevel(event.getLevel())
215                 .withMessage(event.getMessage())
216                 .withArguments(event.getArguments().toArray())
217                 .withMdc(event.getMdc(), mdcComparator)
218                 .build();
219     }
220 
221     private Optional<LoggingEvent> findEvent(Predicate<LoggingEvent> predicate) {
222         return loggingEventsSupplier.get().stream().filter(predicate).findFirst();
223     }
224 
225     public static class PredicateBuilder {
226 
227         private static final Predicate<LoggingEvent> IGNORE_PREDICATE = event -> true;
228 
229         private Predicate<LoggingEvent> messagePredicate = IGNORE_PREDICATE;
230         private Predicate<LoggingEvent> argumentsPredicate = IGNORE_PREDICATE;
231         private Predicate<LoggingEvent> markerPredicate = IGNORE_PREDICATE;
232         private Predicate<LoggingEvent> keyValuePairsPredicate = IGNORE_PREDICATE;
233         private Predicate<LoggingEvent> mdcPredicate = IGNORE_PREDICATE;
234         private Predicate<LoggingEvent> throwablePredicate = IGNORE_PREDICATE;
235         private Predicate<LoggingEvent> levelPredicate = IGNORE_PREDICATE;
236 
237         public static PredicateBuilder aLog() {
238             return new PredicateBuilder();
239         }
240 
241         public PredicateBuilder withLevel(Level level) {
242             levelPredicate = event -> event.getLevel().equals(level);
243             return this;
244         }
245 
246         /**
247          * @deprecated There can be more than one marker in a logging event. Use {@link #withMarkers}
248          *     instead.
249          */
250         @Deprecated
251         public PredicateBuilder withMarker(Marker marker) {
252             return withMarkers(marker == null ? new Marker[] {} : new Marker[] {marker});
253         }
254 
255         public PredicateBuilder withMarkers(Marker... markers) {
256             markerPredicate = event -> event.getMarkers().equals(Arrays.asList(markers));
257             return this;
258         }
259 
260         public PredicateBuilder withKeyValuePairs(KeyValuePair... keyValuePair) {
261             keyValuePairsPredicate =
262                     event -> event.getKeyValuePairs().equals(Arrays.asList(keyValuePair));
263             return this;
264         }
265 
266         public PredicateBuilder withMessage(String message) {
267             return withMessage(message::equals);
268         }
269 
270         public PredicateBuilder withMessage(Predicate<String> predicate) {
271             this.messagePredicate = event -> predicate.test(event.getMessage());
272             return this;
273         }
274 
275         public PredicateBuilder withArguments(Object... arguments) {
276             return withArguments(actualArgs -> actualArgs.equals(Arrays.asList(arguments)));
277         }
278 
279         public PredicateBuilder withArguments(Predicate<Collection<Object>> predicate) {
280             this.argumentsPredicate = event -> predicate.test(event.getArguments());
281             return this;
282         }
283 
284         public PredicateBuilder withThrowable(Throwable throwable) {
285             return withThrowable(t -> t.equals(Optional.ofNullable(throwable)));
286         }
287 
288         public PredicateBuilder withThrowable(Predicate<Optional<Throwable>> predicate) {
289             this.throwablePredicate = event -> predicate.test(event.getThrowable());
290             return this;
291         }
292 
293         public PredicateBuilder withMdc(Map<String, String> mdc, MdcComparator comparator) {
294             this.mdcPredicate = event -> comparator.compare(event.getMdc(), mdc);
295             return this;
296         }
297 
298         public Predicate<LoggingEvent> build() {
299             return levelPredicate
300                     .and(markerPredicate)
301                     .and(keyValuePairsPredicate)
302                     .and(messagePredicate)
303                     .and(argumentsPredicate)
304                     .and(throwablePredicate)
305                     .and(mdcPredicate);
306         }
307     }
308 
309     public static final class MdcComparator {
310 
311         /** Disables verification of the MDC contents. */
312         public static final MdcComparator IGNORING = new MdcComparator((a, b) -> true);
313 
314         /**
315          * Validates the contents of the MDC on the logging event exactly match the specified values.
316          */
317         public static final MdcComparator EXACT =
318                 new MdcComparator((a, b) -> a.size() == b.size() && a.entrySet().containsAll(b.entrySet()));
319 
320         /**
321          * Validates the MDC contains all specified entries, but will not fail if additional entries
322          * exist.
323          */
324         public static final MdcComparator CONTAINING =
325                 new MdcComparator((a, b) -> a.entrySet().containsAll(b.entrySet()));
326 
327         private final BiPredicate<Map<String, String>, Map<String, String>> compareFunction;
328 
329         private MdcComparator(BiPredicate<Map<String, String>, Map<String, String>> compareFunction) {
330             this.compareFunction = compareFunction;
331         }
332 
333         public boolean compare(Map<String, String> actual, Map<String, String> expected) {
334             return compareFunction.test(actual, expected);
335         }
336     }
337 }