REST API and Unit testing for back-end
Examples of tests written using following approach can be found here
Testing REST API:
Most of okapi back-end modules are covered with API tests using Vertx unit
Test setup:
Add runner:
@RunWith(VertxUnitRunner.class)
Run application (deploy verticle):
During test setup a verticle should be deployed to vertx instance (custom verticle class or RestVerticle class in case of using raml module builder)
private static Vertx vertx;private static int port;
@BeforeClasspublic static void setUpClass(final TestContext context) throws Exception { Async async = context.async();vertx = Vertx.vertx();port = NetworkUtils.nextFreePort();
//database setup goes here if necessary
TenantClient tenantClient = new TenantClient("localhost", port, "diku", "dummy-token");
DeploymentOptions restVerticleDeploymentOptions = new DeploymentOptions().setConfig(new JsonObject().put("http.port", port));
vertx.deployVerticle(RestVerticle.class.getName(), restVerticleDeploymentOptions, res -> {try {tenantClient.post(null, res2 -> {async.complete(); }); } catch (Exception e) { e.printStackTrace(); } });}
Run database:
If test covers database interaction, database client should be configured.
Following code provides a possibility to use existing database for tests or run database in embedded mode (slower):
useExternalDatabase = System.getProperty("org.folio.password.validator.test.database","embedded");switch (useExternalDatabase) {case "environment": System.out.println("Using environment settings");break;case "external": String postgresConfigPath = System.getProperty("org.folio.password.validator.test.config","/postgres-conf-local.json"); PostgresClient.setConfigFilePath(postgresConfigPath);break;case "embedded": PostgresClient.setIsEmbedded(true); PostgresClient.getInstance(vertx).startEmbeddedPostgres();break;default: String message = "No understood database choice made." +"Please set org.folio.password.validator.test.database" +"to 'external', 'environment' or 'embedded'";throw new Exception(message);}
To configure database connection via environment variables:
set folio.password.validator.test.database property to 'environment';
set following environment variables:
To configure database connection via file:
create file with configuration:
{"host" : "localhost","port" : 5432,"database" : "okapi_modules","username" : "folio_admin","password" : "folio_admin"}set folio.password.validator.test.database property to 'external';
put path to config file in property org.folio.password.validator.test.config;
Setting system property via cli parameters:
-Dorg.folio.password.validator.test.database=external
-Dorg.folio.password.validator.test.config=/postgres-conf.json
Running tests via maven:
Example (file with name "postgres-conf.json" is located in resources root):
mvn clean install -Dorg.folio.password.validator.test.database=external -Dorg.folio.password.validator.test.config=/postgres-conf.json
Running tests in IDE (intelliji idea example):
Mock HTTP calls:
/* Define wiremock test dependency */
<dependency> <groupId>com.github.tomakehurst</groupId> <artifactId>wiremock</artifactId> <version>2.19.0</version> <scope>test</scope></dependency>
WireMock framework allows to create and setup mock server:
Following code runs new mock server for every test on dynamic port.
To get port value use userMockServer#port
Notifier logs all incoming calls to mock server.
@org.junit.Rulepublic WireMockRule userMockServer = new WireMockRule( WireMockConfiguration.wireMockConfig() .dynamicPort() .notifier(new ConsoleNotifier(true)));
To mock response to specific request use template:
WireMock.stubFor(<request matching>.willReturn(<response definition>));
Example:
WireMock.stubFor(WireMock.get("/users?query=id==" + ADMIN_ID) .willReturn(WireMock.okJson(someJsonResponse) ));
Examples for <request matching>:
WireMock has advanced features for request matching (withHeader, withCookie, withQueryParam, withRequestBody) :
WireMock.post(WireMock.urlMatching("regexp here")) .withRequestBody(WireMock.equalToJson("json here"))
WireMock.any(WireMock.urlMatching("regexp here")) .withHeader("header", WireMock.equalTo("value")) .withHeader("header2", WireMock.absent()) .withHeader("header3", WireMock.matching("regexp")) .withQueryParam("param", WireMock.containing("substring))
All <request matching> variants can be found in documentation Request Matching
Examples for <response definition> :
WireMock.aResponse() .withStatus(200) .withHeader("response-header", "value") .withBody(someByteArray);
WireMock.ok("bodyHere")
WireMock.okJson(someJsonResponse)
WireMock.notFound()
All <response definition> variants can be found in documentation Response Templating
Perform REST API testing:
Required dependencies:
/* Exactly that rest-assured library */
<dependency> <groupId>io.rest-assured</groupId> <artifactId>rest-assured</artifactId> <version>${rest-assured.version}</version> <scope>test</scope></dependency>
/* By default RestAssured uses jackson databinding under the hoods, if such functionality doesn't exist - you can use fasterxml.jackson.core */
<dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.7.3</version></dependency>
Rest-Assured provides a fluent API to create readable tests for REST resources:
RestAssured.given() .param("limit", 20) .when() .get("blogs") .then() .statusCode(200);
One of the ways to use RestAssured is following:
RestAssured.given()
<building request>
.when()
<specifying method and path>
.then()
<response verification>
RestAssured use cases are described is this guide: Testing RESTful Services in Java: Best Practices
Unit testing:
This section does not describe unit testing approach in general but only some confusing cases about testing asynchronous calls.
Waiting and verifying asynchronous results:
Testing methods wtih Handler<AsyncResult> parameter:
@RunWith(VertxUnitRunner.class)
...
@Test
public void test(TestContext testContext) {
//some test setup
//expect
Handler<AsyncResult<JsonObject>> checkingHandler = testContext.asyncAssertSuccess(response -> {
//asserting goes here
Assert.assertThat(actual, Matchers.is(expected));
Mockito.verify(...);
});
someService.someAction(params, checkingHandler);
}
TestContext parameter should be added to the test method.
To expect failed asynchronous result use testContext#asyncAssertFailure
Mocking asynchronous method calls:
For example, in test it is necessary to mock some method that returns a result asynchronously. It takes Handler<AsyncResult> parameter to provide callback.
public interface DummyService {boolean dummyAsyncCall(String someParameter, Handler<AsyncResult<String>> handler);}
Suggested solution: use Mockito answers. Following class' constructor takes mock result that should be passed to handler as well as handler argument index.
public class GenericHandlerAnswer<H, R> implements Answer<R> {private H handlerResult;private int argumentIndex;private R returnResult;public GenericHandlerAnswer(H handlerResult, int handlerArgumentIndex) {this.handlerResult = handlerResult;this.argumentIndex = handlerArgumentIndex; }public GenericHandlerAnswer(H handlerResult, int handlerArgumentIndex, R returnResult) {this(handlerResult, handlerArgumentIndex);this.returnResult = returnResult; }@Overridepublic R answer(InvocationOnMock invocation) { Handler<H> handler = invocation.getArgument(argumentIndex); handler.handle(handlerResult);return returnResult; }}
Example of test:
@RunWith(VertxUnitRunner.class)public class MockDummyService {@Mockpublic DummyService dummyServiceMock;@Beforepublic void setUp(TestContext testContext) throws Exception { MockitoAnnotations.initMocks(this); }@Testpublic void test(TestContext testContext) { AsyncResult<String> dummyCallResult = Future.succeededFuture("Result for handler");int handlerParameterIndex = 1;boolean methodReturnedValue = true; GenericHandlerAnswer<AsyncResult<String>, Boolean> answer =new GenericHandlerAnswer<>(dummyCallResult, handlerParameterIndex, methodReturnedValue); Mockito.doAnswer(answer) .when(dummyServiceMock) .dummyAsyncCall(ArgumentMatchers.anyString(), ArgumentMatchers.any()); Handler<AsyncResult<String>> checkingHandler = testContext.asyncAssertSuccess(response -> { Assert.assertThat(response, Matchers.is(dummyCallResult.result())); });boolean result = dummyServiceMock.dummyAsyncCall("some string", checkingHandler); Assert.assertTrue(result); }}
Useful links:
Testing RESTful Services in Java: Best Practices (a lot of useful examples with rest assured)