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://code.google.com/p/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 == Capturing and Asserting Logs ==
298
299 If the code under tests output logs, they need to be captured and possibly asserted. For example:
300
301 {{code language="java"}}
302 ...
303 /**
304 * Capture logs.
305 */
306 @RegisterExtension
307 LogCaptureExtension logCapture = new LogCaptureExtension(LogLevel.WARN);
308 ...
309 assertEquals("Error getting resource [bad resource] because of invalid path format. Reason: [invalid url]",
310 logCapture.getMessage(0));
311 ...
312 {{/code}}
313
314 {{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:
315
316 {{code}}
317 @RegisterExtension
318 static LogCaptureExtension logCapture = new LogCaptureExtension(LogLevel.INFO);
319 {{/code}}
320
321 And perform the verification in a ##@AfterAll## method, as in:
322
323 {{code language='java'}}
324 @AfterAll
325 static void verifyLog() throws Exception
326 {
327 // Assert log happening in the first initialize() call.
328 assertEquals(String.format("Using filesystem store directory [%s]",
329 new File(new File("."), "store/file").getCanonicalFile()), logCapture.getMessage(0));
330 }
331 {{/code}}
332
333 == @OldcoreTest ==
334
335 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.
336
337 === Accessing MockitoOldcore ===
338
339 You have several ways to access it:
340
341 * Either by annotating a field of the test class with ##@InjectMockitoOldcore##. For example:(((
342 {{code language="java"}}
343 @InjectMockitoOldcore
344 private MockitoOldcore oldCore;
345 {{/code}}
346 )))
347 * Either by adding a parameter of type ##MockitoOldcore## to the various JUnit5 test methods. For example:(((
348 {{code language="java"}}
349 @BeforeEach
350 public void before(MockitoOldcore oldcore)
351 {
352 ....
353 }
354
355 @AfterEach
356 public void after(MockitoOldcore oldcore)
357 {
358 ....
359 }
360
361 @Test
362 public void something(MockitoOldcore oldcore)
363 {
364 ...
365 }
366 {{/code}}
367
368 Note that this can be combined with the ##MockitoComponentManager## parameter type too, as in:
369
370 {{code language="java"}}
371 @Test
372 public void something(MockitoComponentManager componentManager, MockitoOldcore oldcore)
373 {
374 ...
375 }
376 {{/code}}
377 )))
378 * You can also pass a ##MockitoOldcore## parameter to methods annotated with ##@BeforeComponent## and ##@AfterComponent##. For example:(((
379 {{code language="java"}}
380 @BeforeComponent
381 public void configure(MockitoComponentManager componentManager, MockitoOldcore oldcore) throws Exception
382 {
383 ...
384 }
385 {{/code}}
386 )))
387
388 == Temporary Directory ==
389
390 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:
391 * Leftover data in a shared location once the test has finished
392 * Creates a state that can make other tests fail
393 * Generate errors in Jenkins since Jenkins monitors created files and doesn't allow to remove files outside of the Worskspace
394
395 Thus the best practice is to use the ##XWikiTempDir## annotation as in the following examples:
396
397 {{code language="java"}}
398 // New tmp dir once per test class
399 @XWikiTempDir
400 private static File TEST_DIR;
401
402 // New tmp dir once per test
403 @XWikiTempDir
404 private File tmpDir;
405
406 // New tmp dir for this specific test
407 @Test
408 public void testXXX(@XWikiTempDir File tmpDir)
409 {
410 ...
411 }
412 {{/code}}
413
414 The ##XWikiTempDir## annotation will create a unique temporary directory inside Maven's ##target## directory.
415
416 = Testing a JUnit5 Extension =
417
418 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 ;)
419
420 {{code language='java'}}
421 public class LogCaptureExtensionTest
422 {
423 // Test class being tested
424 public static class SampleTestCase
425 {
426 private static final Logger LOGGER = LoggerFactory.getLogger(SampleTestCase.class);
427
428 @RegisterExtension
429 LogCaptureExtension logCapture = new LogCaptureExtension(LogLevel.WARN);
430
431 @Test
432 @Order(1)
433 void testWhenNotCaptured()
434 {
435 LOGGER.warn("uncaptured warn");
436 }
437
438 @Test
439 @Order(2)
440 void testWhenCaptured()
441 {
442 LOGGER.error("captured error");
443 assertEquals("captured error", logCapture.getMessage(0));
444 }
445
446 @Test
447 @Order(3)
448 void testWhenLoggerLevelNotCaptured()
449 {
450 LOGGER.info(("info ignored and not captured"));
451 }
452 }
453
454 @Test
455 void captureLogsWhenNotStatic()
456 {
457 TestExecutionSummary summary = executeJUnit(SampleTestCase.class);
458
459 // Verify that uncaptured logs are caught and generate an exception
460 assertEquals(1, summary.getFailures().size());
461 assertEquals("Following messages must be asserted: [uncaptured warn]",
462 summary.getFailures().get(0).getException().getMessage());
463 assertEquals(2, summary.getTestsSucceededCount());
464 }
465
466 private TestExecutionSummary executeJUnit(Class<?> testClass)
467 {
468 LauncherDiscoveryRequest request = LauncherDiscoveryRequestBuilder.request()
469 .selectors(selectClass(testClass))
470 .build();
471 Launcher launcher = LauncherFactory.create();
472 SummaryGeneratingListener summaryListener = new SummaryGeneratingListener();
473 launcher.execute(request, summaryListener);
474
475 return summaryListener.getSummary();
476 }
477 ...
478 {{/code}}
479
480
481 = Examples =
482
483 * [[Examples of ##@ComponentTest## in xwiki-commons>>https://github.com/xwiki/xwiki-commons/search?utf8=%E2%9C%93&q=%22%40ComponentTest%22&type=]]
484 * [[Examples of ##@ComponentTest## in xwiki-platform>>https://github.com/xwiki/xwiki-platform/search?utf8=%E2%9C%93&q=%22%40ComponentTest%22&type=]]
485 * [[Examples of ##@OldcoreTest## in xwiki-platform>>https://github.com/xwiki/xwiki-platform/search?utf8=%E2%9C%93&q=%22%40OldcoreTest%22&type=]]
486
487 = Best practices =
488
489 * 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##
490 * Have each test method test only 1 use case as much as possible.
491 * 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.
492 * 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##).
493 * When you're testing for an exception, use the following strategy, shown on an example:(((
494 {{code language="java"}}
495 Throwable exception = assertThrows(IllegalArgumentException.class, () -> {
496 // Content throwing exception here
497 this.cssIdentifierSerializer.serialize(input);
498 });
499 assertEquals("Invalid character: the input contains U+0000.", exception.getMessage());
500 {{/code}}
501 )))
502
503 = Tips =
504
505 * When Mocking, to ignore all debug logging calls and to allow all calls to ##is*Enabled## you could write:(((
506 {{code language="java"}}
507 // Ignore all calls to debug() and enable all logs so that we can assert info(), warn() and error() calls.
508 ignoring(any(Logger.class)).method("debug");
509 allowing(any(Logger.class)).method("is.*Enabled"); will(returnValue(true));
510 {{/code}}
511 )))
512
513 = Using JUnit 4 =
514
515 This is now deprecated but kept for the moment since there are still a lot of JUnit4 tests.
516
517 See the [[guide for writing JUnit4 tests>>doc:Community.Testing.JavaUnitTestingJUnit4.WebHome]].

Get Connected