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[event.getMarkers().size()]))
212                 .withKeyValuePairs(
213                         event.getKeyValuePairs().toArray(new KeyValuePair[event.getKeyValuePairs().size()]))
214                 .withThrowable(event.getThrowable().orElse(null))
215                 .withLevel(event.getLevel())
216                 .withMessage(event.getMessage())
217                 .withArguments(event.getArguments().toArray())
218                 .withMdc(event.getMdc(), mdcComparator)
219                 .build();
220     }
221 
222     private Optional<LoggingEvent> findEvent(Predicate<LoggingEvent> predicate) {
223         return loggingEventsSupplier.get().stream().filter(predicate).findFirst();
224     }
225 
226     public static class PredicateBuilder {
227 
228         private static final Predicate<LoggingEvent> IGNORE_PREDICATE = event -> true;
229 
230         private Predicate<LoggingEvent> messagePredicate = IGNORE_PREDICATE;
231         private Predicate<LoggingEvent> argumentsPredicate = IGNORE_PREDICATE;
232         private Predicate<LoggingEvent> markerPredicate = IGNORE_PREDICATE;
233         private Predicate<LoggingEvent> keyValuePairsPredicate = IGNORE_PREDICATE;
234         private Predicate<LoggingEvent> mdcPredicate = IGNORE_PREDICATE;
235         private Predicate<LoggingEvent> throwablePredicate = IGNORE_PREDICATE;
236         private Predicate<LoggingEvent> levelPredicate = IGNORE_PREDICATE;
237 
238         public static PredicateBuilder aLog() {
239             return new PredicateBuilder();
240         }
241 
242         public PredicateBuilder withLevel(Level level) {
243             levelPredicate = event -> event.getLevel().equals(level);
244             return this;
245         }
246 
247         /**
248          * @deprecated There can be more than one marker in a logging event. Use {@link #withMarkers}
249          *     instead.
250          */
251         @Deprecated
252         public PredicateBuilder withMarker(Marker marker) {
253             return withMarkers(marker == null ? new Marker[] {} : new Marker[] {marker});
254         }
255 
256         public PredicateBuilder withMarkers(Marker... markers) {
257             markerPredicate = event -> event.getMarkers().equals(Arrays.asList(markers));
258             return this;
259         }
260 
261         public PredicateBuilder withKeyValuePairs(KeyValuePair... keyValuePair) {
262             keyValuePairsPredicate =
263                     event -> event.getKeyValuePairs().equals(Arrays.asList(keyValuePair));
264             return this;
265         }
266 
267         public PredicateBuilder withMessage(String message) {
268             return withMessage(message::equals);
269         }
270 
271         public PredicateBuilder withMessage(Predicate<String> predicate) {
272             this.messagePredicate = event -> predicate.test(event.getMessage());
273             return this;
274         }
275 
276         public PredicateBuilder withArguments(Object... arguments) {
277             return withArguments(actualArgs -> actualArgs.equals(Arrays.asList(arguments)));
278         }
279 
280         public PredicateBuilder withArguments(Predicate<Collection<Object>> predicate) {
281             this.argumentsPredicate = event -> predicate.test(event.getArguments());
282             return this;
283         }
284 
285         public PredicateBuilder withThrowable(Throwable throwable) {
286             return withThrowable(t -> t.equals(Optional.ofNullable(throwable)));
287         }
288 
289         public PredicateBuilder withThrowable(Predicate<Optional<Throwable>> predicate) {
290             this.throwablePredicate = event -> predicate.test(event.getThrowable());
291             return this;
292         }
293 
294         public PredicateBuilder withMdc(Map<String, String> mdc, MdcComparator comparator) {
295             this.mdcPredicate = event -> comparator.compare(event.getMdc(), mdc);
296             return this;
297         }
298 
299         public Predicate<LoggingEvent> build() {
300             return levelPredicate
301                     .and(markerPredicate)
302                     .and(keyValuePairsPredicate)
303                     .and(messagePredicate)
304                     .and(argumentsPredicate)
305                     .and(throwablePredicate)
306                     .and(mdcPredicate);
307         }
308     }
309 
310     public static final class MdcComparator {
311 
312         /** Disables verification of the MDC contents. */
313         public static final MdcComparator IGNORING = new MdcComparator((a, b) -> true);
314 
315         /**
316          * Validates the contents of the MDC on the logging event exactly match the specified values.
317          */
318         public static final MdcComparator EXACT =
319                 new MdcComparator((a, b) -> a.size() == b.size() && a.entrySet().containsAll(b.entrySet()));
320 
321         /**
322          * Validates the MDC contains all specified entries, but will not fail if additional entries
323          * exist.
324          */
325         public static final MdcComparator CONTAINING =
326                 new MdcComparator((a, b) -> a.entrySet().containsAll(b.entrySet()));
327 
328         private final BiPredicate<Map<String, String>, Map<String, String>> compareFunction;
329 
330         private MdcComparator(BiPredicate<Map<String, String>, Map<String, String>> compareFunction) {
331             this.compareFunction = compareFunction;
332         }
333 
334         public boolean compare(Map<String, String> actual, Map<String, String> expected) {
335             return compareFunction.test(actual, expected);
336         }
337     }
338 }