Skip to content

Instantly share code, notes, and snippets.

@vicly
Last active November 22, 2017 02:37
Show Gist options
  • Save vicly/eb248adbefd1fb5d37e7b5380c8c68ed to your computer and use it in GitHub Desktop.
Save vicly/eb248adbefd1fb5d37e7b5380c8c68ed to your computer and use it in GitHub Desktop.
[Test API Client with Mock Server] #REST #Test
// testCompile "junit:junit:4.12"
// testCompile "org.assertj:assertj-core:3.5.2"
// testCompile "com.github.tomakehurst:wiremock-standalone:2.7.1"
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
import static com.github.tomakehurst.wiremock.client.WireMock.delete;
import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
import static com.github.tomakehurst.wiremock.client.WireMock.post;
import static com.github.tomakehurst.wiremock.client.WireMock.put;
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
import java.util.List;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.WebTarget;
import org.apache.commons.lang3.RandomUtils;
import org.glassfish.jersey.client.ClientConfig;
import org.glassfish.jersey.logging.LoggingFeature;
import org.junit.After;
import org.junit.Before;
import org.junit.ClassRule;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.tomakehurst.wiremock.client.ResponseDefinitionBuilder;
import com.github.tomakehurst.wiremock.junit.WireMockRule;
import com.github.tomakehurst.wiremock.stubbing.StubMapping;
import com.google.common.primitives.Ints;
/**
* Abstract test used to test API client.
*
* 1. initialises a mock server stub
* 2. provides helper methods to mock service.
*
* @author Vic Liu
*/
public abstract class AbstractClientTest
{
/**
* Singleton, shared cross multiple test classes, DO NOT call {@code WIREMOCK} in java {@code static} block, but only
* in your instance level test methods.
*/
@ClassRule
public static final WireMockRule WIREMOCK = new WireMockRule(
wireMockConfig().dynamicPort());
private static final ObjectMapper MAPPER = new ObjectMapper();
private static final Object CLIENT_LOCK = new Object();
protected Client client;
/**
* the mock server stub
*/
protected WebTarget baseTarget;
/**
* Do not override this method.
*
* @see #beforeEachTest
*/
@Before
public void setUp()
{
if (client == null)
{
synchronized (CLIENT_LOCK)
{
int port = WIREMOCK.port();
this.client = ClientBuilder.newClient(new ClientConfig().register(LoggingFeature.class));
this.baseTarget = this.client.target("http://localhost:" + port);
afterWebTargetInit(this.baseTarget);
}
}
beforeEachTest();
}
protected void afterWebTargetInit(WebTarget baseTarget)
{
// do nothing by default
}
protected void beforeEachTest()
{
// do nothing by default
}
/**
* Do not override this method.
*
* @see #afterEachTest
*/
@After
public void tearDown()
{
afterEachTest();
}
protected void afterEachTest()
{
// do nothing by default
}
protected StubMapping simplePostStub(String path, int responseStatus)
{
return simplePostStub(path, responseStatus, null);
}
/**
* helper to stub POST with give response.
*
* @param path relative path
* @param responseStatus http response status code
* @param responseBody http response entity object, will be convert to JSON string
* @return StubMapping instance
*/
protected StubMapping simplePostStub(String path, int responseStatus, Object responseBody)
{
ResponseDefinitionBuilder responseBuilder = aResponse().withStatus(responseStatus);
if (responseBody != null)
{
responseBuilder
.withHeader("Content-Type", "application/json")
.withBody(toJson(responseBody));
}
return stubFor(post(urlEqualTo(path))
.withHeader("Accept", equalTo("application/json"))
.willReturn(responseBuilder));
}
/**
* helper to stub PUT with give response.
*
* @param path relative path
* @param responseStatus http response status code
* @param responseBody http response entity object, will be convert to JSON string
* @return StubMapping instance
*/
protected StubMapping simplePutStub(String path, int responseStatus, Object responseBody)
{
ResponseDefinitionBuilder responseBuilder = aResponse().withStatus(responseStatus);
if (responseBody != null)
{
responseBuilder
.withHeader("Content-Type", "application/json")
.withBody(toJson(responseBody));
}
return stubFor(put(urlEqualTo(path))
.withHeader("Accept", equalTo("application/json"))
.willReturn(responseBuilder));
}
/**
* helper to stub DELETE with give response.
*
* @param path relative path
* @param responseStatus http response status code
* @param responseBody http response entity object, will be convert to JSON string
* @return StubMapping instance
*/
protected StubMapping simpleDeleteStub(String path, int responseStatus, Object responseBody)
{
ResponseDefinitionBuilder responseBuilder = aResponse().withStatus(responseStatus);
if (responseBody != null)
{
responseBuilder
.withHeader("Content-Type", "application/json")
.withBody(toJson(responseBody));
}
return stubFor(delete(urlEqualTo(path))
.withHeader("Accept", equalTo("application/json"))
.willReturn(responseBuilder));
}
/**
* convert object to JSON string.
*
* @param val object
* @return JSON string
*/
protected static String toJson(Object val)
{
try
{
return MAPPER.writeValueAsString(val);
}
catch (JsonProcessingException e)
{
throw new RuntimeException("Failed to construct test json", e);
}
}
/**
* generate random http status not in the specified list.
*
* @param excludes what to be excluded.
* @return a random number between 100 and 599
*/
protected static int randomHttpStatusNotIn(int... excludes)
{
List<Integer> list = Ints.asList(excludes);
while (true)
{
int status = RandomUtils.nextInt(100, 599);
if (!list.contains(status))
{
return status;
}
}
}
}
public class SampleTest
extends AbstractClientTest
{
@Rule
public ExpectedException thrown = ExpectedException.none();
private XyzApiClient xyzApiClient;
@Override
protected void afterWebTargetInit(WebTarget baseTarget)
{
// init client object
super.afterWebTargetInit(baseTarget);
this.xyzApiClient = new JaxRsXyzClient(this.baseTarget);
}
@Test
public void test200()
{
// api path
String path = "/xyz";
// test data
String xyz = "a string";
// mock service
simplePostStub(path, 200, createResponse(xyz));
// run
Xyz xyz = callApi();
// verify
assertThat(xyz).isNotNull();
assertThat(xyz.getValue()).isEqualTo(xyz);
verify(postRequestedFor(urlPathEqualTo(path)));
}
@Test
public void test400()
{
simplePostStub("/", 400);
this.thrown.expect(XyzException.class);
this.thrown.expectMessage(startsWith("Error posting xyz"));
callApi();
}
private Xyz callApi()
{
// invoke api
XyzRequest fakeRequest = ...;
return this.xyzApiClient.post(fakeRequest);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment