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;
@BeforeClass
public 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:
name value (example) db.host localhost db.port 5432 db.database okapi_modules db.username
folio_admin db.password folio_admin
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.Rule
public 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;
}
@Override
public R answer(InvocationOnMock invocation) {
Handler<H> handler = invocation.getArgument(argumentIndex);
handler.handle(handlerResult);
return returnResult;
}
}
Example of test:
@RunWith(VertxUnitRunner.class)
public class MockDummyService {
@Mock
public DummyService dummyServiceMock;
@Before
public void setUp(TestContext testContext) throws Exception {
MockitoAnnotations.initMocks(this);
}
@Test
public 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:
- WireMock getting started guide
- WireMock request matchers
- WireMock: response templating
- Rest Assured usage guide
- Testing RESTful Services in Java: Best Practices (a lot of useful examples with rest assured)