Show last authors
1 {{box cssClass="floatinginfobox" title="**Contents**"}}
2 {{toc/}}
3 {{/box}}
4
5 {{warning}}
6 We are still converting some [[old unit tests written in JUnit3 or JUnit4>>||anchor="HUsingJUnit4"]] (and using JMock), to JUnit5 tests (using Mockito).
7 {{/warning}}
8
9 = Characteristics =
10
11 * A unit test **only tests a single class** in isolation from other classes. Since in the XWiki project we write code using Components, this means a unit test is testing a Component in isolation from other Components.
12 * These are **tests performed in isolation** using [[Mock Objects>>http://www.mockobjects.com/]]. More specifically we're using JUnit 5.x and [[Mockito>>https://github.com/mockito/mockito/]]
13 * These tests **must not interact with the environment** (Database, Container, File System, etc) and thus do not need any environmental setup to execute
14 * These tests **must** not output anything to stdout or stderr (or it'll fail the build).
15 * Your Maven module must depend on the ##xwiki-commons-tool-test-simple## (for tests not testing Components) or ##xwiki-commons-tool-test-component## (for tests testing Components) modules.
16
17 = Writing Unit Tests =
18
19 == Mockito Mocks ==
20
21 You can mock java classes and interfaces using Mockito in several ways:
22
23 * using ##mock(...)##. Example:(((
24 {{code language="java"}}
25 @Test
26 public void something()
27 {
28 VelocityEngine engine = mock(VelocityEngine.class);
29 ...
30 }
31 {{/code}}
32 )))
33 * using the ##@Mock## Mockito annotation. Example:(((
34 {{code language="java"}}
35 @Mock
36 private VelocityEngine engine;
37 {{/code}}
38
39 Note that for this example to work, you need to tell Mockito to inject mocks into ##@Mock## annotations by using ##MockitoAnnotations.initMocks(testInstance);## or you can use the ##@ComponentTest## or ##@OldcoreTest## annotations which automatically call ##MockitoAnnotations.initMocks## (see below to know more about these 2 annotations).
40 )))
41 * You can also use the Mockito [[##@InjectMocks## annotation>>http://static.javadoc.io/org.mockito/mockito-core/2.18.3/org/mockito/Mockito.html#21]] which will inject mocks fields automatically. For example; here's how you could combine both ##@InjectMocks## and ##@InjectMockComponents##:(((
42 {{code language="java"}}
43 @InjectMocks
44 @InjectMockComponents
45 private Component4Impl component4;
46 {{/code}}
47
48 Where ##Component4Impl## is defined as:
49
50 {{code language="java"}}
51 @Component
52 @Singleton
53 public class Component4Impl implements Component4Role
54 {
55 private List<String> list;
56
57 @Inject
58 private Component1Role component1;
59 ...
60 {{/code}}
61
62 In this example, the ##Component4Impl#list## field will be injected with a mock.
63 )))
64
65 == @ComponentTest ==
66
67 If you're testing XWiki Components, annotate your test class with ##@ComponentTest##.
68
69 This will allow you to have the following features.
70
71 === Component Manager ===
72
73 A ##MockitoComponentManager## instance will be created under the hood and initialized. It'll be initialized with no component by default. You'll be able to add components to it with one of the following options:
74
75 * Annotate your test class with ##@AllComponents##. This will automatically register all components found in the class path. For example:(((
76 {{code language="java"}}
77 @ComponentTest
78 @AllComponents
79 public class DefaultConverterManagerTest
80 {
81 ...
82 }
83 {{/code}}
84 )))
85 ** You can exclude some component implementations by using the ##excludes## attribute of the ##@AllComponents## annotation ({{info}}Since 12.1RC1{{/info}}). For example:(((
86 {{code language="java"}}
87 @AllComponents(excludes = {
88 DefaultStringDocumentReferenceResolver.class,
89 DefaultStringEntityReferenceSerializer.class,
90 DefaultOfficeServer.class
91 })
92 {{/code}}
93 )))
94 * Annotate your test class with ##@ComponentList(...)## to explicitly list the component implementation to register in the Component Manager. For example:(((
95 {{code language="java"}}
96 @ComponentTest
97 @ComponentList({
98 ListFilter.class,
99 ListItemFilter.class,
100 FontFilter.class,
101 BodyFilter.class,
102 AttributeFilter.class,
103 UniqueIdFilter.class,
104 DefaultHTMLCleaner.class,
105 LinkFilter.class
106 })
107 public class DefaultHTMLCleanerTest
108 {
109 ...
110 }
111 {{/code}}
112 )))
113 * Programmatically register Components or mock components. For example to register a mock component:(((
114 {{code language="java"}}
115 Environment environment = mockitoComponentManager.registerMockComponent(Environment.class);
116 {{/code}}
117 )))
118 * Using the ##@MockComponent## annotation, see below
119
120 === @MockComponent ===
121
122 Any test class field annotated with ##MockComponent## will have a mock component created and registered in the ComponentManager.
123
124 For example:
125
126 {{code language="java"}}
127 @ComponentTest
128 public class DefaultVelocityContextFactoryTest
129 {
130 @MockComponent
131 private VelocityConfiguration configuration;
132 ...
133 }
134 {{/code}}
135
136 You can also specify a component hint using the ##@Named## annotation. For example:
137
138 {{code language="java"}}
139 @ComponentTest
140 public class DefaultVelocityContextFactoryTest
141 {
142 @MockComponent
143 @Named("somehint")
144 private VelocityConfiguration configuration;
145 ...
146 }
147 {{/code}}
148
149 === @InjectMockComponents ===
150
151 Any test class field annotated with ##InjectMockComponents## and referencing a component implementation will have its fields marked with ##@Inject## be injected with mock component instances.
152
153 For example:
154
155 {{code language="java"}}
156 @ComponentTest
157 public class DefaultVelocityContextFactoryTest
158 {
159 @InjectMockComponents
160 private DefaultVelocityContextFactory factory;
161 {{/code}}
162
163 Note that if the ##@MockComponent## annotations are used, they'll be injected with mocks before the component annotated with ##@InjectMockComponents## is instantiated and set up with mocks.
164
165 For example, in the following case, the mocks for ##VelocityConfiguration## and ##ComponentManager## are created first and since ##DefaultVelocityContextFactory## has corresponding fields with ##@Inject##, they'll be injected with those mocks instead of new mocks being generated automatically.
166
167 {{code language="java"}}
168 @ComponentTest
169 public class DefaultVelocityContextFactoryTest
170 {
171 @MockComponent
172 private VelocityConfiguration configuration;
173
174 @MockComponent
175 private ComponentManager componentManager;
176
177 @InjectMockComponents
178 private DefaultVelocityContextFactory factory;
179 ...
180 {{/code}}
181
182 If your component implements several roles, you'll need to disambiguate that and specify which role to use. For example:
183
184 {{code language="java"}}
185 @InjectMockComponents(role = Component2Role.class)
186 private Component5Impl component5Role1;
187 {{/code}}
188
189 === Accessing the Component Manager ===
190
191 You have 2 ways to access it:
192
193 * Either by annotating a field of the test class with ##@InjectComponentManager##. For example:(((
194 {{code language="java"}}
195 @InjectComponentManager
196 private MockitoComponentManager componentManager;
197 {{/code}}
198 )))
199 * Either by adding a parameter of type ##ComponentManagfer## or ##MockitoComponentManager## to the various JUnit5 test methods. For example:(((
200 {{code language="java"}}
201 @BeforeEach
202 public void before(MockitoComponentManager componentManager)
203 {
204 ....
205 }
206
207 @AfterEach
208 public void after(MockitoComponentManager componentManager)
209 {
210 ....
211 }
212
213 @Test
214 public void something(MockitoComponentManager componentManager)
215 {
216 ...
217 }
218 {{/code}}
219 )))
220
221 === @BeforeComponent / @AfterComponent ===
222
223 If you need to perform some initialization before the mock components are created/set up for the components annotate with ##@InjectMockComponents##, create a method and annotate it with ##@BeforeComponent##.
224
225 For example:
226
227 {{code language="java"}}
228 @ComponentTest
229 public class WikiUIExtensionComponentBuilderTest
230 {
231 ...
232 @InjectMockComponents
233 private WikiUIExtensionComponentBuilder builder;
234
235 @BeforeComponent
236 public void configure() throws Exception
237 {
238 ...
239 }
240 ...
241 }
242 {{/code}}
243
244 {{info}}Since 12.2{{/info}} And if you need to test some component that implements ##Initializable## and you need different test fixtures depending on the test, you'll need to use ##BeforeComponent("<testname>")##, such as:
245
246 {{code language='java'}}
247 @BeforeComponent("normalizeFromRootServletContextAndServletMapping")
248 public void beforeNormalizeFromRootServletContextAndServletMapping()
249 {
250 // Code executed before Initializable#initialize() is called and
251 // after methods annotated with @BeforeComponent have been called
252 }
253
254 @Test
255 public void normalizeFromRootServletContextAndServletMapping()
256 {
257 ...
258 }
259 {{/code}}
260
261 If you need access to the Component Manager, you have 2 solutions:
262
263 * Use a field annotated with ##@InjectComponentManager## as described above
264 * Or add a parameter of type ##ComponentManager## or ##MockitoComponentManager## to the ##@BeforeComponent##-annotated method. For example:(((
265 {{code language="java"}}
266 @ComponentTest
267 public class WikiUIExtensionComponentBuilderTest
268 {
269 ...
270 @InjectMockComponents
271 private WikiUIExtensionComponentBuilder builder;
272
273 @BeforeComponent
274 public void configure(MockitoComponentManager componentManager) throws Exception
275 {
276 ...
277 }
278 ...
279 }
280 {{/code}}
281 )))
282
283 You can use ##@AfterComponent## to perform some setup after the mock components have been setup.
284
285 === Order ===
286
287 It's important to understand the order used to resolve all those annotations:
288
289 1. Create an empty Mockito Component Manager
290 1. Inject ##@InjectComponentManager## fields
291 1. Inject ##@MockComponent## fields
292 1. Call ##@BeforeComponent## methods
293 1. Fill the Mockito Component Manager (with components matching the ##@AllComponent## and ##@ComponentList## annotations)
294 1. Call ##@AfterComponent## methods
295 1. Inject ##@InjectMockComponents## fields
296
297 === Providers ===
298
299 If the component being tested is injecting ##Provider##s then the test framework will automatically register a mock Component of the provided type, as a singleton component. For example:
300
301 {{code language = 'java'}}
302 // Component under test
303 ...
304 @Inject
305 @Named("hint")
306 private Provider<MyComponent> myComponentProvider;
307 ...
308
309 // Test
310 @ComponentTest
311 class MyTest
312 {
313 ...
314 @InjectComponentManager
315 private MockitoComponentManager componentManager;
316 ...
317 @Test
318 void test()
319 {
320 MyComponent myCompnent = this.componentManager.getInstance(MyComponent.class, "hint");
321 ...
322 }
323 {{/code}}
324
325 However if the provided component is not a singleton, you'll need to register a Provider yourself to override this default behavior. For example:
326
327 {{code language = 'java'}}
328 // Component under test
329 ...
330 // Note: DatabaseMailListener is using: @InstantiationStrategy(ComponentInstantiationStrategy.PER_LOOKUP)
331 @Inject
332 @Named("database")
333 private Provider<MailListener> databaseMailListenerProvider;
334 ...
335 // Test
336 @BeforeComponent("resendSynchronouslySeveralMessages")
337 void setupResendSynchronouslySeveralMessages() throws Exception
338 {
339 this.componentManager.registerMockComponent(
340 new DefaultParameterizedType(null, Provider.class, MailListener.class), "database");
341 }
342
343 @Test
344 void resendSynchronouslySeveralMessages() throws Exception
345 {
346 ...
347 Provider<MailListener> databaseMailListenerProvider = this.componentManager.getInstance(
348 new DefaultParameterizedType(null, Provider.class, MailListener.class), "database");
349 DatabaseMailListener databaseMailListener1 = mock(DatabaseMailListener.class);
350 DatabaseMailListener databaseMailListener2 = mock(DatabaseMailListener.class);
351 when(databaseMailListenerProvider.get()).thenReturn(databaseMailListener1, databaseMailListener2);
352 ...
353 }
354 {{/code}}
355
356 === @Inject ===
357
358 Any field annotated with ##@Inject## (and optionally with ##@Named##) will get injected with an implementation from the Component Manager if one exists (if not, an exception will be raised).
359
360 Example:
361
362 {{code language="java"}}
363 @ComponentTest
364 @AllComponents
365 class SomeTest
366 {
367 @Inject
368 private SomeComponentRole someComponent;
369 ...
370 }
371 {{/code}}
372
373 == Capturing and Asserting Logs ==
374
375 If the code under tests output logs, they need to be captured and possibly asserted. For example:
376
377 {{code language="java"}}
378 ...
379 /**
380 * Capture logs.
381 */
382 @RegisterExtension
383 LogCaptureExtension logCapture = new LogCaptureExtension(LogLevel.WARN);
384 ...
385 assertEquals("Error getting resource [bad resource] because of invalid path format. Reason: [invalid url]",
386 logCapture.getMessage(0));
387 ...
388 {{/code}}
389
390 {{info}}Since 12.1RC1{{/info}} If we have some code that emits logs during a ##@BeforeAll## (for example in some component ##initializable()## method), you should register the ##LogCaptureExtension## statically as in:
391
392 {{code}}
393 @RegisterExtension
394 static LogCaptureExtension logCapture = new LogCaptureExtension(LogLevel.INFO);
395 {{/code}}
396
397 And perform the verification in a ##@AfterAll## method, as in:
398
399 {{code language='java'}}
400 @AfterAll
401 static void verifyLog() throws Exception
402 {
403 // Assert log happening in the first initialize() call.
404 assertEquals(String.format("Using filesystem store directory [%s]",
405 new File(new File("."), "store/file").getCanonicalFile()), logCapture.getMessage(0));
406 }
407 {{/code}}
408
409 == @OldcoreTest ==
410
411 If you're writing a unit test for some old core code, you should use the ##@OldcoreTest## annotation instead of the ##@ComponentTest## one described above. Tests annotated with ##@OldcoreTest## will behave the same as with ##@ComponentTest## but, in addition, it'll automatically create and configure a helper ##MockitoOldcore## object that you can use to help you write your unit test.
412
413 === Accessing MockitoOldcore ===
414
415 You have several ways to access it:
416
417 * Either by annotating a field of the test class with ##@InjectMockitoOldcore##. For example:(((
418 {{code language="java"}}
419 @InjectMockitoOldcore
420 private MockitoOldcore oldCore;
421 {{/code}}
422 )))
423 * Either by adding a parameter of type ##MockitoOldcore## to the various JUnit5 test methods. For example:(((
424 {{code language="java"}}
425 @BeforeEach
426 public void before(MockitoOldcore oldcore)
427 {
428 ....
429 }
430
431 @AfterEach
432 public void after(MockitoOldcore oldcore)
433 {
434 ....
435 }
436
437 @Test
438 public void something(MockitoOldcore oldcore)
439 {
440 ...
441 }
442 {{/code}}
443
444 Note that this can be combined with the ##MockitoComponentManager## parameter type too, as in:
445
446 {{code language="java"}}
447 @Test
448 public void something(MockitoComponentManager componentManager, MockitoOldcore oldcore)
449 {
450 ...
451 }
452 {{/code}}
453 )))
454 * You can also pass a ##MockitoOldcore## parameter to methods annotated with ##@BeforeComponent## and ##@AfterComponent##. For example:(((
455 {{code language="java"}}
456 @BeforeComponent
457 public void configure(MockitoComponentManager componentManager, MockitoOldcore oldcore) throws Exception
458 {
459 ...
460 }
461 {{/code}}
462 )))
463
464 == Temporary Directory ==
465
466 From time to time, tests require a temporary directory. We must not use the system's temporary directory nor use JUnit5's ##@TempDir## annotation (it also uses the system's temporary directory) for the following reasons:
467 * Leftover data in a shared location once the test has finished
468 * Creates a state that can make other tests fail
469 * Generate errors in Jenkins since Jenkins monitors created files and doesn't allow to remove files outside of the Worskspace
470
471 Thus the best practice is to use the ##XWikiTempDir## annotation as in the following examples:
472
473 {{code language="java"}}
474 @ComponentTest // or @ExtendWith(XWikiTempDirExtension.class)
475 public class MyTest
476 {
477 ...
478 // New tmp dir once per test class
479 @XWikiTempDir
480 private static File TEST_DIR;
481
482 // New tmp dir once per test
483 @XWikiTempDir
484 private File tmpDir;
485
486 // New tmp dir for this specific test
487 @Test
488 public void testXXX(@XWikiTempDir File tmpDir)
489 {
490 ...
491 }
492 ...
493 {{/code}}
494
495 The ##XWikiTempDir## annotation will create a unique temporary directory inside Maven's ##target## directory.
496
497 = Testing a JUnit5 Extension =
498
499 If you need to test a JUnit5 extension, user the following example as a basis for your test. It shows how to start a JUInit5 engine from inside a JUnit5 test ;)
500
501 {{code language='java'}}
502 public class LogCaptureExtensionTest
503 {
504 // Test class being tested
505 public static class SampleTestCase
506 {
507 private static final Logger LOGGER = LoggerFactory.getLogger(SampleTestCase.class);
508
509 @RegisterExtension
510 LogCaptureExtension logCapture = new LogCaptureExtension(LogLevel.WARN);
511
512 @Test
513 @Order(1)
514 void testWhenNotCaptured()
515 {
516 LOGGER.warn("uncaptured warn");
517 }
518
519 @Test
520 @Order(2)
521 void testWhenCaptured()
522 {
523 LOGGER.error("captured error");
524 assertEquals("captured error", logCapture.getMessage(0));
525 }
526
527 @Test
528 @Order(3)
529 void testWhenLoggerLevelNotCaptured()
530 {
531 LOGGER.info(("info ignored and not captured"));
532 }
533 }
534
535 @Test
536 void captureLogsWhenNotStatic()
537 {
538 TestExecutionSummary summary = executeJUnit(SampleTestCase.class);
539
540 // Verify that uncaptured logs are caught and generate an exception
541 assertEquals(1, summary.getFailures().size());
542 assertEquals("Following messages must be asserted: [uncaptured warn]",
543 summary.getFailures().get(0).getException().getMessage());
544 assertEquals(2, summary.getTestsSucceededCount());
545 }
546
547 private TestExecutionSummary executeJUnit(Class<?> testClass)
548 {
549 LauncherDiscoveryRequest request = LauncherDiscoveryRequestBuilder.request()
550 .selectors(selectClass(testClass))
551 .build();
552 Launcher launcher = LauncherFactory.create();
553 SummaryGeneratingListener summaryListener = new SummaryGeneratingListener();
554 launcher.execute(request, summaryListener);
555
556 return summaryListener.getSummary();
557 }
558 ...
559 {{/code}}
560
561
562 = Examples =
563
564 * [[Examples of ##@ComponentTest## in xwiki-commons>>https://github.com/xwiki/xwiki-commons/search?utf8=%E2%9C%93&q=%22%40ComponentTest%22&type=]]
565 * [[Examples of ##@ComponentTest## in xwiki-platform>>https://github.com/xwiki/xwiki-platform/search?utf8=%E2%9C%93&q=%22%40ComponentTest%22&type=]]
566 * [[Examples of ##@OldcoreTest## in xwiki-platform>>https://github.com/xwiki/xwiki-platform/search?utf8=%E2%9C%93&q=%22%40OldcoreTest%22&type=]]
567
568 = Best practices =
569
570 * Name the Test class with the name of the class under test **suffixed with ##Test##**. For example the JUnit test class for ##XWikiMessageTool## should be named ##XWikiMessageToolTest##
571 * Have each test method test only 1 use case as much as possible.
572 * Name the test methods with the method to test followed by a qualifier describing the test. For example ##importWithHeterogeneousEncodings()##. In the few cases where it's not applicable, name after the use case being tested.
573 * Don't use the ##public## keyword since they're not required any more by JUnit5 (not in the Test Class nor in the test methods nor in the lifecycle methods such as ##BeforeEach##).
574 * When you're testing for an exception, use the following strategy, shown on an example:(((
575 {{code language="java"}}
576 Throwable exception = assertThrows(IllegalArgumentException.class, () -> {
577 // Content throwing exception here
578 this.cssIdentifierSerializer.serialize(input);
579 });
580 assertEquals("Invalid character: the input contains U+0000.", exception.getMessage());
581 {{/code}}
582 )))
583 * We should use Hamcrest ##assertThat()## method when it makes sense (when there's no good JUnit 5.x assertion alternative or without a good reporting when the assertion fails. For example for asserting that a list contains a given entry).
584
585 = Tips =
586
587 * When Mocking, to ignore all debug logging calls and to allow all calls to ##is*Enabled## you could write:(((
588 {{code language="java"}}
589 // Ignore all calls to debug() and enable all logs so that we can assert info(), warn() and error() calls.
590 ignoring(any(Logger.class)).method("debug");
591 allowing(any(Logger.class)).method("is.*Enabled"); will(returnValue(true));
592 {{/code}}
593 )))
594
595 = Using JUnit 4 =
596
597 This is now deprecated but kept for the moment since there are still a lot of JUnit4 tests.
598
599 See the [[guide for writing JUnit4 tests>>doc:Community.Testing.JavaUnitTestingJUnit4.WebHome]].

Get Connected