Java Unit Testing

Version 4.1 by Vincent Massol on 2018/04/29 19:51

Characteristics

  • 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.
  • These are tests performed in isolation using Mock Objects. More specifically we're using JUnit 5.x and Mockito
  • These tests must not interact with the environment (Database, Container, File System, etc) and thus do not need any environmental setup to execute
  • These tests must not output anything to stdout or stderr (or it'll fail the build). 
  • 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.

Writing Unit Tests

Mockito Mocks

You can mock java classes and interfaces using Mockito in several ways:

  • using mock(...). Example:
    @Test
    public void something()
    {
        VelocityEngine engine = mock(VelocityEngine.class);
       ...
    }
  • using the @Mock Mockito annotation. Example:
    @Mock
    private VelocityEngine engine;

    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).

  • You can also use the Mockito @InjectMocks annotation which will inject mocks fields automatically. For example; here's how you could combine both @InjectMocks and @InjectMockComponents:
    @InjectMocks
    @InjectMockComponents
    private Component4Impl component4;

    Where Component4Impl is defined as:

    @Component
    @Singleton
    public class Component4Impl implements Component4Role
    {
       private List<String> list;

       @Inject
       private Component1Role component1;
    ...

    In this example, the Component4Impl#list field will be injected with a mock.

@ComponentTest

If you're testing XWiki Components, annotate your test class with @ComponentTest.

This will allow you to have the following features.

Component Manager

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:

  • Annotate your test class with @AllComponents. This will automatically register all components found in the class path. For example:
    @ComponentTest
    @AllComponents
    public class DefaultConverterManagerTest
    {
    ...
    }
  • Annotate your test class with @ComponentList(...) to explicitly list the component implementation to register in the Component Manager. For example:
    @ComponentTest
    @ComponentList({
        ListFilter.class,
        ListItemFilter.class,
        FontFilter.class,
        BodyFilter.class,
        AttributeFilter.class,
        UniqueIdFilter.class,
        DefaultHTMLCleaner.class,
        LinkFilter.class
    })
    public class DefaultHTMLCleanerTest
    {
    ...
    }
  • Programmatically register Components or mock components. For example to register a mock component:
    Environment environment = mockitoComponentManager.registerMockComponent(Environment.class);
  • Using the @MockComponent annotation, see below

@MockComponent

Any test class field annotated with MockComponent will have a mock component created and registered in the ComponentManager.

For example:

@ComponentTest
public class DefaultVelocityContextFactoryTest
{
   @MockComponent
   private VelocityConfiguration configuration;
...
}

@InjectMockComponents

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.

For example:

@ComponentTest
public class DefaultVelocityContextFactoryTest
{
   @InjectMockComponents
   private DefaultVelocityContextFactory factory;

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. 

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.

@ComponentTest
public class DefaultVelocityContextFactoryTest
{
   @MockComponent
   private VelocityConfiguration configuration;

   @MockComponent
   private ComponentManager componentManager;

   @InjectMockComponents
   private DefaultVelocityContextFactory factory;
...

If your component implements several roles, you'll need to disambiguate that and specify which role to use. For example:

@InjectMockComponents(role = Component2Role.class)
private Component5Impl component5Role1;

Accessing the Component Manager

You have 2 ways to access it:

  • Either by annotating a field of the test class with @InjectComponentManager. For example:
    @InjectComponentManager
    private MockitoComponentManager componentManager;
  • Either by adding a parameter of type ComponentManagfer or MockitoComponentManager to the various JUnit5 test methods. For example:
    @BeforeEach
    public void before(MockitoComponentManager componentManager)
    {
       ....
    }

    @AfterEach
    public void after(MockitoComponentManager componentManager)
    {
       ....
    }

    @Test
    public void something(MockitoComponentManager componentManager)
    {
       ...
    }

@BeforeComponent / @AfterComponent

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.

For example:

@ComponentTest
public class WikiUIExtensionComponentBuilderTest
{
...
   @InjectMockComponents
   private WikiUIExtensionComponentBuilder builder;

   @BeforeComponent
   public void configure() throws Exception
   {
       ...
   }
...
}

If you need access to the Component Manager, you have 2 solutions:

  • Use a field annotated with @InjectComponentManager as described above
  • Or add a parameter of type ComponentManager or MockitoComponentManager to the @BeforeComponent-annotated method. For example:
    @ComponentTest
    public class WikiUIExtensionComponentBuilderTest
    {
    ...
       @InjectMockComponents
       private WikiUIExtensionComponentBuilder builder;

       @BeforeComponent
       public void configure(MockitoComponentManager componentManager) throws Exception
       {
           ...
       }
    ...
    }

You can use @AfterComponent to perform some setup after the mock components have been setup.

Capturing and Asserting Logs

If the code under tests output logs, they need to be captured and possibly asserted. For example:

...
/**
 * Capture logs.
 */

@RegisterExtension
static LogCaptureExtension logCapture = new LogCaptureExtension(LogLevel.WARN);
...
assertEquals("Error getting resource [bad resource] because of invalid path format. Reason: [invalid url]",
    logCapture.getMessage(0));
...

Note that the JUnit5 extension must be used with @RegisterExtension and using a static variable or if you want to remove the static keyword, you'll need to annotate the test class with @TestInstance(TestInstance.Lifecycle.PER_CLASS).

@OldcoreTest

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.

Accessing MockitoOldcore

You have several ways to access it:

  • Either by annotating a field of the test class with @InjectMockitoOldcore. For example:
    @InjectMockitoOldcore
    private MockitoOldcore oldCore;
  • Either by adding a parameter of type MockitoOldcore to the various JUnit5 test methods. For example:
    @BeforeEach
    public void before(MockitoOldcore oldcore)
    {
       ....
    }

    @AfterEach
    public void after(MockitoOldcore oldcore)
    {
       ....
    }

    @Test
    public void something(MockitoOldcore oldcore)
    {
       ...
    }

    Note that this can be combined with the MockitoComponentManager parameter type too, as in:

    @Test
    public void something(MockitoComponentManager componentManager, MockitoOldcore oldcore)
    {
       ...
    }
  • You can also pass a MockitoOldcore parameter to methods annotated with @BeforeComponent and @AfterComponent. For example:
    @BeforeComponent
    public void configure(MockitoComponentManager componentManager, MockitoOldcore oldcore) throws Exception
    {
       ...
    }

Examples

Best practices

  • 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
  • Name the test methods with the method to test followed by a qualifier describing the test. For example importWithHeterogeneousEncodings().
  • When you're testing for an exception, use the following strategy, shown on an example:
    Throwable exception = assertThrows(IllegalArgumentException.class, () -> {
       // Content throwing exception here
       this.cssIdentifierSerializer.serialize(input);
    });
    assertEquals("Invalid character: the input contains U+0000.", exception.getMessage());

Tips

  • When Mocking, to ignore all debug logging calls and to allow all calls to is*Enabled you could write:
     // Ignore all calls to debug() and enable all logs so that we can assert info(), warn() and error() calls.
    ignoring(any(Logger.class)).method("debug");
    allowing(any(Logger.class)).method("is.*Enabled"); will(returnValue(true));
Tags:
   

Get Connected