Simple Example - Cat Facts

In this example we'll show how Chaotics can be used to form part of your testing approach. We'll start with a trivial CatFactsService and improve it by adding more tests to uncover problems with the functionality.

Lets get started!

public class CatFactsService {

  private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

  public CatFact getCatFact() {
    CatFact catFact = null;
    try {
      Response response = Request.Get("https://cat-fact.herokuapp.com/facts/random")
        .execute();
      catFact = OBJECT_MAPPER.readValue(response.returnResponse().getEntity().getContent(), CatFact.class);
    } catch (IOException e) {
      e.printStackTrace();
    }

    return catFact;
  }
}

This class is using the Apache HTTP Client to make a GET request to https://cat-fact.herokuapp.com/facts/random the response is converted to a Java object using the Jackson Databind library. It has very basic error handling, but that's it.

The first thing to notice is that it's hard to introduce Chaotics to this class. The URL is hardcoded, this is the first change we'll make. We'll follow the principles of 12 Factor Apps and make the configuration changeable depending on the environment it's running in. In this case we'll use a constructor argument to set a class variable.

public class CatFactsService {

  ...  

  private final String catFactUrl;

  public CatFactsService(String catFactUrl) {
    this.catFactUrl = catFactUrl;
  }

  public CatFact getCatFact() {
    CatFact catFact = null;
    try {
      Response response = Request.Get(catFactUrl).execute();
    ...
  }
}

Now we can write our first test case. In this scenario we'll use Chaotics to return a successful response.

Let's look at what a JUnit test might look like for this. I've kept every line in to show exactly how it works. In the future I'll cut away some of the boiler plate code for brevity.

public class CatFactsService1Test {

    private static final ChaoticsClient CHAOTICS_CLIENT = new ChaoticsClient();
    private static final List<ApiSecretPair> APIS_TO_DELETE = new ArrayList<>();
    private static final String CHAOTICS_URL = "https://api.chaotics.io/endpoint?apiId=";

    @AfterClass
    public static void afterTests() {
        APIS_TO_DELETE.forEach(apiSecretPair -> {
            try {
                CHAOTICS_CLIENT.deleteEndpoint(apiSecretPair);
            } catch (IOException e) {
                e.printStackTrace();
            }
        });
    }

    @Test
    public void testSuccessfulResponse() throws IOException {
        ApiSecretPair apiSecretPair = CHAOTICS_CLIENT.createEndpoint(successfulResponse());
        APIS_TO_DELETE.add(apiSecretPair);
        ReliableCatFactService catFactService = new ReliableCatFactService(CHAOTICS_URL + apiSecretPair.getApiId());

        CatFact catFact = catFactService.getCatFact();

        Assert.assertEquals("Thank to an extremely efficient pair of kidneys, cats can hydrate themselves by drinking salt water.", catFact.getText());
    }

    private static CreateApiRequest successfulResponse() {
        String responseBody = "{\"status\":{\"verified\":true,\"sentCount\":1},\"type\":\"cat\",\"deleted\":false,\"_id\":\"5b1b3fd8841d9700146158ce\",\"updatedAt\":\"2020-08-23T20:20:01.611Z\",\"createdAt\":\"2018-07-17T20:20:02.104Z\",\"user\":\"5a9ac18c7478810ea6c06381\",\"text\":\"Thank to an extremely efficient pair of kidneys, cats can hydrate themselves by drinking salt water.\",\"source\":\"user\",\"__v\":0,\"used\":false}";

        return CreateApiRequest.builder()
                .withApiResponses(Collections.singletonList(ApiResponse.builder()
                        .withResponseBody(responseBody)
                        .withResponseContentType(ContentType.APPLICATION_JSON.getMimeType())
                        .withResponseStatusCode(200)
                        .withCharset("UTF-8")
                        .build()))
                .build();
    }
}

Let's try testing behavior that isn't normally exhibited in the normal operation of the Cat Facts API.

Now we have our first successful test we can test trying out edge cases. We'll write our tests first, get a failure, then write the fix.

First things first, what will happen if Cat Facts returns a 503 Service Unavailble HTTP status code. Now we don't know when Cat Facts will have a problem, so lets use Chaotics to create an endpoint which mimics that behavior.

@Test
public void testDefaultFactIsGivenWhenServiceUnavailable() throws IOException {
    ApiSecretPair apiSecretPair = CHAOTICS_CLIENT.createEndpoint(unavailableResponse());
    APIS_TO_DELETE.add(apiSecretPair);
    CatFactService catFactService = new CatFactService(CHAOTICS_URL + apiSecretPair.getApiId());

    CatFact catFact = catFactService.getCatFact();

    Assert.assertEquals("Cats sleep 70% of their lives.", catFact.getText());
}

private CreateApiRequest unavailableResponse() {
    return CreateApiRequest.builder()
            .withApiResponses(Collections.singletonList(ApiResponse.builder()
                    .withResponseBody("service unavailable")
                    .withResponseContentType(ContentType.TEXT_PLAIN.getMimeType())
                    .withResponseStatusCode(503)
                    .withCharset("UTF-8")
                    .build()))
            .build();
}

Here we create an API endpoint which responses with a 503 status code, a body which reads service unavailable and a valid content type header.

Now I want my CatFact to return a hard coded Cat Fact if the real dependency is unavailable.

Currently, with this implementation the Apache HTTP Client will see that the status code is > 300 and throw an error. This will mean that our class will return null.

Let's fix the problem.

private static final CatFact ERROR_CAT_FACT = new CatFact("Cats sleep 70% of their lives.");

public CatFact getCatFact() {
    CatFact catFact = null;
    try {
        Response response = Request.Get(catFactUrl).execute();
        HttpResponse httpResponse = response.returnResponse();
        if (httpResponse.getStatusLine().getStatusCode() > 200) {
            catFact = ERROR_CAT_FACT;
        } else {
            catFact = OBJECT_MAPPER.readValue(httpResponse.getEntity().getContent(), CatFact.class);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }

    return catFact;
}

Great! We're now handling this error case. Our class is slightly better than it was before.

Let's keep going, how will this code handle a long response? Let's see. Here is the JUnit test case.

@Test
public void testDefaultFactIsGivenWhenServiceTimesout() throws IOException {
    ApiSecretPair apiSecretPair = CHAOTICS_CLIENT.createEndpoint(timeoutResponse());
    APIS_TO_DELETE.add(apiSecretPair);
    CatFactService catFactService = new CatFactService(CHAOTICS_URL + apiSecretPair.getApiId());

    CatFact catFact = catFactService.getCatFact();

    Assert.assertEquals("Cats sleep 70% of their lives.", catFact.getText());
}

private CreateApiRequest timeoutResponse() {
    String responseBody = "...";

    return CreateApiRequest.builder()
        .withApiResponses(Collections.singletonList(ApiResponse.builder()
            .withResponseBody(responseBody)
            .withResponseContentType(ContentType.APPLICATION_JSON.getMimeType())
            .withResponseStatusCode(200)
            .withCharset("UTF-8")
            .withLatency(5000)
            .build()))
        .build();
  }

Now Chaotics will delay our response for at least 5000ms (5 seconds). Our code doesn't handle this well, and just waits for the API to response, I could set this even longer, but to save wasting time 5 seconds will do.

Let's make another change to our CatFactsService

public CatFact getCatFact() {
    CatFact catFact;

    try {
        Response response = Request.Get(catFactUrl)
                .socketTimeout(4 * 1000)
                .connectTimeout(4 * 1000)
                .execute();
        HttpResponse httpResponse = response.returnResponse();
        if (httpResponse.getStatusLine().getStatusCode() > 200) {
            catFact = ERROR_CAT_FACT;
        } else {
            catFact = OBJECT_MAPPER.readValue(httpResponse.getEntity().getContent(), CatFact.class);
        }
    } catch (IOException e) {
        catFact = ERROR_CAT_FACT;
    }

    return catFact;
}

Here is complete method, I've added some timeouts. Now if the response takes longer than the timeout we'll get an Exception. We'll catch that exception and return a hardcoded value. So even if we have a problem with our dependant API we still provide a service to our customers, even if it is a diminished one.