Testing
- Unit testing is done using JUnit 5 and Mockito is used for mocking calls to Domino. The versions currently in use can be found in the main pom.xml.
- Postman and Newman are used for integration testing.
- PMD is used to check the quality of generated code. Results are put in PMD Results report.
- Spot Bugs is used to generate bug reports. The result is in the Spot Bugs Report.
- CheckStyle with the Google ruleset is used to generate style reports and the Google ruleset. Results are put in CheckStyle report.
- Copy Paste Detection (CPD), part of PMD, is used to detect duplicate code. Results are put in CPD Result reports.
JUnit 5
JUnit 5 uses the org.junit.jupiter.api
packages. Tests are annotated (as usual) with @Test
. @DisplayName
can be used to give a nice display name for test results. However, in Eclipse, this means double-clicking throws an error because it tries to find a method with that name (which obviously doesn’t exist). After the error it just loads the full class. Considering that there are not a large number of tests in each test class, this isn’t a problem and ensures a cleaner description in results pages.
Bootstrapping tests
To setup a test, define your class level variables on top without initializing them. External classes, not subject to a test can be annotated using @Mock
and @Rule
.
Use the @BeforeEach
and @AfterEach
to initialize your variables. There is also the option of using static @BeforeAll
and @AfterAll
. However, we try to avoid these.
Pick your method names, so the @BeforeEach
method is sorted first.
Test annotations
We use Annotations to define the intended test use. Our annotations include the Vertx extension (see Vertx Tests below). We use:
@UnitTest
- a test that runs during themvn test
cycle, including in the CI/CD environment.@IntegrationTest
- a test that runs during themvn validate
cycle (might need a Domino backend).@PerformanceTest
- a test that needs manual launch and a Domino backend.
Make sure you check out the example with important details.
Assertions should use the org.junit.jupiter.api.Assertions
static methods, e.g. org.junit.jupiter.api.Assertions.assertEquals
. If imports of org.junit.Assert
static methods are accidentally selected, the code will still run.
Vert.x tests
For access to the Vertx runtime from tests, JUnit 5 is extensible and there is a Vertx extension. Annotate the class with @ExtendWith(VertxExtension.class)
and add the parameter Vertx vertx
to your test classes, e.g.
@ExtendWith(VertxExtension.class)
public class VertxTests {
@Test
@DisplayName("A Vertx Test")
void testSuccess() throws Exception {
JsonObject doc = KeepUtils.getJsonFromResource("/testdata/A_Json_Object.json");
when(mockDoc.getItemValueString(anyString())).then(invocation -> doc.getString(invocation.getArgument(0)));
final JsonObject requestParams = new JsonObject();
MyVertxHandler request = new MyVertxHandler(requestParams, vertx);
this.request.process(this.getJsonParams());
// do something
}
Testing Vertx routes
Accessing the Vertx instance is just one scenario. But if you’re setting up a verticle, the tests need to wait for the verticle to run. This is done by passing VertxTestContext
parameter into the bootstrapping and the tests. This allows checking for completions, successes and failures of handlers.
You can then use VertxTestContext.failNow(String)
and VertxTestContext.completeNow()
to tear down the test context for failure or success of a test.
VertxTestContext.completing()
creates a handler that failNow()
and completeNow()
can use and this can be passed as an additional parameter into an HttpServer’s listen
method like this:
vertx.createHttpServer()
.requestHandler(router::handle)
.listen(thePort, testContext.completing());
Testing full Vertx responses
A recent addition to Vertx’s JUnit 5 testing is the ability to create an HTTP request against a Vertx HttpServer and validate the response. This can be useful for testing bad responses, invalid methods or simple results. It doesn’t really allow creating more specific tests of the response content though, so unless you know exactly what you’re getting back, it doesn’t really work. It also doesn’t allow time for messages being passed around the EventBus though. It will automatically handle completing or failing the testContext. Here is a simple bad request test:
@Test
void testBadRequest() throws KeepExceptionMissingParameters {
/*{
JsonObject body = new JsonObject();
testRequest(client, HttpMethod.POST, "/auth")
.with(
requestHeader("ContentType", "application/json")
)
.expect(
statusCode(400)
)
.sendJson(body, testContext);*/
this.expectFailure(KeepExceptionMissingParameters.class, this.request, this.getJsonParams());
}
Reactive testing
All Domino side handlers in KEEP are reactive and need testing with request.setSubscriber()
. To ease testing there is a generic TestSubscriber
and a ready implementation of TestSubscriberJson
.
It can be configured to handle success and failure cases. Check its JavaDoc for details. It includes a callback method for even more detailed testing.
Test suites with JUnit 5
Test suites are done with JUnitPlatform, with the following format:
import org.junit.platform.runner.JUnitPlatform;
import org.junit.platform.suite.api.SelectPackages;
import org.junit.platform.suite.api.SuiteDisplayName;
import org.junit.runner.RunWith;
@RunWith(JUnitPlatform.class)
@SuiteDisplayName("Keep Test Suite")
@SelectPackages("com.hcl.domino.keep")
public class AllTests {
}
Mockito
Mockito allows you to mock classes and intercept methods. This is done by creating an instance of a class via the static org.mockito.Mockito.mock()
method. You can then intercept methods of that class by chaining when()
and thenReturn()
. Obviously, some mocks will need to return other mocks, e.g. DocumentCollection.getFirstDocument()
will need to return a mock of a Document
. In this case, you’ll need to mock the Document
prior to mocking the DocumentCollection
.
You can also mock non-database objects, as we do with mocking KeepJnxSession
, which needs passing into handlers.
As a sample of mocking, here is a chunk of code from IdentityFetchjwtRequestTest
:
@Mock
KeepJnxSession s;
@Mock
NotesIDTable fakeIds;
@Mock
NotesDbQueryResult result;
@Mock
Document fakeNote;
@Mock
Database db;
@Mock
DominoCollection mockNc;
@Rule
MockitoRule mockitoRule = MockitoJUnit.rule();
@BeforeEach
void beforeEach(final Vertx vertx) {
super.beforeEach(vertx);
// Initialize the Mock objects
MockitoAnnotations.initMocks(this);
MockKeepFactory keepFactory = new MockKeepFactory(vertx);
KeepFactorySource.INSTANCE.overwriteFactory(keepFactory);
final Integer id = 100;
final String hash = "HashValue";
// Defining Mock object behavior
when(fakeIds.isEmpty()).thenReturn(false);
when(fakeIds.getFirstId()).thenReturn(id);
when(result.getIDTable()).thenReturn(fakeIds);
when(fakeNote.getItemValueString("Hash")).thenReturn(hash);
when(fakeNote.getItemValueString("FullName")).thenReturn("John Doe");
when(db.query(anyString(), any(), anyInt(), anyInt(), anyInt())).thenReturn(result);
when(db.openNoteById(anyInt())).thenReturn(fakeNote);
}
As you see, you can create a mock of a method with hard-coded parameter names. These mocks only match the specified parameters. Alternatively, as you see with db.query()
, you can pass the relevant org.mockito.ArgumentMatchers
static method to simulate a String, int, object etc. This then matches whatever parameter is passed in.
Sometimes, however, you need to get more creative with mocking methods. Say, for instance, you have a JsonObject that simulates a Document and you want to get the relevant property from the JsonObject to match the requested Item on the Document. Instead of thenReturn()
, you can use then()
and specify a method to run. You can then get the relevant argument passed into the method.
// Variable declaration
@Mock
Document mockDo;
JsonObject doc = KeepUtils.getJsonFromResource("/testdata/A_Json_Object.json");
when(mockDoc.getItemValueString(anyString())).then(invocation -> doc.getString(invocation.getArgument(0)));
You can also iterate over a collection by creating a JsonArray of JsonObjects and using a java.util.concurrent.atomic.AtomicInteger
to get the relevant position. At the end of the loop, you just need to set null. Here’s an example:
AtomicInteger ordinal = new AtomicInteger(0);
final DocumentCollection dc = mock(DocumentCollection.class);
when(dc.getFirstDocument()).thenReturn(mockDoc); when(dc.getNextDocument(any())).then(invocation -> {
ordinal.getAndIncrement();
if (ordinal.intValue() == docs.size()) {
return null;
} else {
return mockDoc;
}
});
Then, when you’re mocking getItemValueString()
from mockDoc, you can access ordinal.intValue()
to get the atomic value. You just need to bear in mind what the current value is. If you’ve called getNextDocument()
at the start of the loop, ordinal
will already have been incremented on from the document you’re processing in getItemValueString()
. So you need to use ordinal.intValue() - 1
.
However, you cannot use Mockito to mock static methods in classes. This can cause problems if the code within a given handler makes a call to a static method. In KEEP, that is done for Domino sessions. All methods in DominoSessionManager
class are static. Powermock is designed for this, but doesn’t support JUnit 5 currently. As a result, any code that calls that will require a different approach.