View on GitHub

cdi-test

JUnit extension for easy and efficient testing of CDI components

cdi-test

Build CDI Test CodeQL Coverage (Sonar) Status (Sonar) Maintainability (Sonar) Maven Central

Main features:

cdi-test is targeted at running unit, component and integration tests at scale. It accomplishes this with:

Tutorial

A quick tutorial is also available. Or just read on.

Introduction

When testing projects that are based on cdi there are different approaches available. I'll discuss this topic briefly to give you an idea what this library is all about.

Run the tests in a production container

Nowadays as we package everything as a docker container it's possible to setup integration testing solutions using docker and Testcontainers without too much fuss.

However this approach is not well suited for unit and component tests.

Run the tests in a production-like container

This approach is taken by the Arquillian framework. The advantages are:

However this approach also hase some disadvantages:

Run unit and module tests in a test container

This is what this library does and it is all about running light-weight tests with easy to define boundaries. I want my unit and module tests to start quick (startup/creation time) and to run fast even if there are hundreds or thousands of tests in my project. Indeed this is what this library was developed for in the first place.

Why is there no testing support readily available with cdi as there is with spring? Well, hm ... Actually because spring is a product (one implementation) and this product has junit test support. cdi is just a standard in the first place and as it is an api it doesn't define any testing hooks. Maybe it should require them.

Apache DeltaSpike has a similar test runner as cdi-test but it's - at least imho - not quite the same and not as easy to use and extend as cdi-test.

Conclusion

cdi-test shouldn't do everything and it's not the right tool if you want to create integration tests. In this case take a look at Testcontainers or maybe Arquillian.

If you want to test the components of your cdi application read on! I'll refer to the tests contained in the cdi-test-core project. Maybe it's best to clone cdi-test to play around with the tests.

Project setup

Additional to the junit 5 library you need the following dependencies:

    <dependency>
        <groupId>de.hilling.junit.cdi</groupId>
        <artifactId>cdi-test-core</artifactId>
        <version>3.4.0</version>
        <scope>test</scope>
    </dependency>

https://github.com/guhilling/cdi-test/blob/82e6e4c8df5a952798c9f4e91558baec473ecbc9/integration-tests/pom.xml#L24-L45

Some internals and first test.

First the (not so) obvious: Don't forget to include a beans.xml for your tests or cdi won't find any of your testing components. However the test class itself is not a cdi bean but is created by junit. This is different from the version 1.x and 2.x of cdi-test where the test case was created using cdi.

The junit engine is extended with CdiTestJunitExtension. If you need to reference cdi components from your test case you must use field injection. Only this is supported by cdi-test: It will use weld to resolve a second instance of the test class and then copy the @Injected fields to the test instance. So producer methods and qualifiers will be supported.

The creation of a second instance will eventually be removed in the future (see #215 ).

In the example below we let the extension resolve and inject the SampleService which is under test, into the test.

@ExtendWith(CdiTestJunitExtension.class)
public class SampleServiceTest {

    @Inject
    private SampleService sampleService;

    @Test
    public void createPerson() {
        Person person = new Person();
        sampleService.storePerson(person);
    }
}

The Service is resolved by the cdi implementation as usual. In the above test no cdi-test magic is done.

Well ... there is one thing: All standard scopes are created and destroyed just before any single test that is run. This way it is possible to run the tests with decent performance and have them isolated from each other anyway.

The only beans that survive the test are the special @TestSuiteScoped beans. These are used in cdi-test internally but you are certainly free to use them in your test support classes. This often makes sense for components that should be replaced globally an might be expensive to create.

Mocking beans

Taking a lot at the first example we might be tempted to look at SampleService closer:

public class SampleService {

    @Inject
    private BackendService backendService;

    public void storePerson(Person person) {
        backendService.storePerson(person);
    }
}

Maybe we need to mock the BackendService for our unit test. This is easily done by just creating the required mock object in the test class. It is necessary to add the MockitoExtension after the CdiTestJunitExtension because we add a listener for mock creation first:

@ExtendWith(CdiTestJunitExtension.class)
@ExtendWith(MockitoExtension.class)
public class MockProxyTest {

    @Mock
    private BackendService backendService;

    @Inject
    private SampleService sampleService;

    @Test
    public void createPerson() {
        Person person = new Person();
        sampleService.storePerson(person);
        verify(backendService).storePerson(person);
    }

    @Test
    public void doNothing() {
        verifyZeroInteractions(backendService);
    }
}

By just defining the BackendService as a mock using the standard mockito annotation @Mock it is automatically used when calling the bean of type BackendService.

Because we are reusing the MockitoExtension here we can also use all its features like mocking per test case as described in the Mockito documentation

How is this done?

Actually quite simple: During testing every bean call is executed with an additional interceptor that dispatches the calls. This is configured by the CdiTestJunitExtension that analyzes the test class for mock definitions.

So there is one fixed "method routing" defined per test class. In another module test you are free to use the actual BackendService together with SampleService.

Test implementations

Mocks are not always the easiest way to create a certain test behaviour. Maybe you want to create a special implementation of BackendService and use this in your unit test.

This is actually easy to accomplish with cdi-test. First you annotate your test implementation as @ActivatableTestImplementation:

@ActivatableTestImplementation
public class BackendServiceTestImplementation extends BackendService {
// your implementation
}

This implementation is by default not used when "routing" the method calls. You can however enable it in your tests by just injecting the test implementation in your test. This should be quite natural as you probably have to set up and verify the test implementation:

@ExtendWith(CdiTestJunitExtension.class)
@ExtendWith(MockitoExtension.class)
public class ActivateAlternativeForRegularBeanTest {
    @Inject
    private SampleService sampleService;
    @Inject
    private BackendServiceTestImplementation testBackendService;

    @Test
    public void callTestActivatedService() {
        sampleService.storePerson(new Person());
        assertEquals(1, testBackendService.getInvocations());
    }

}

Technically it works quite the same way as using mocks.

Test implementations part II

If there is an @ActivatableTestImplementation there should also be something that isn't "activatable"?

Yes! If you want to override a certain service in all of your tests you can use @GlobalTestImplementation. This will almost always be used, except if you override it with a mock.

Extending cdi-test

For extensions based on cdi-test you mainly need the following classes:

cdi-test-microprofile should be a nice example for a small but hopefully useful extension.

Feedback and future development

Feedback is always welcome. Feel free to ask for extensions and support for building your own!

LICENSE

Copyright 2019 Gunnar Hilling

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.