Skip to content
This repository has been archived by the owner on Jul 2, 2023. It is now read-only.

Commit

Permalink
Add a mock library to make testing HTTP calls simpler. (#26)
Browse files Browse the repository at this point in the history
* Add a mock library to make testing HTTP calls simpler.

* Add a bit of documentation around the http-requests-mock library.
  • Loading branch information
budjb authored May 21, 2020
1 parent 13495df commit 58d063f
Show file tree
Hide file tree
Showing 16 changed files with 1,224 additions and 2 deletions.
1 change: 1 addition & 0 deletions http-requests-bom/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ dependencies {
api project(':http-requests-core')
api project(':http-requests-groovy')
api project(':http-requests-httpcomponents-client')
api project(':http-requests-mock')
api project(':http-requests-jackson')
api project(':http-requests-jersey1')
api project(':http-requests-jersey2')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -720,7 +720,7 @@ private HttpResponse run(HttpMethod method, HttpRequest request, HttpEntity enti
try {
response = execute(context, entity, filterProcessor);
}
catch (IOException e) {
catch (IOException | HttpClientException e) {
throw e;
}
catch (Exception e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,17 @@

import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

public class EntityConverterManager {
/**
* Empty entity converter manager.
*/
public static final EntityConverterManager empty = new EntityConverterManager(Collections.emptyList());

/**
* Logger.
*/
Expand All @@ -39,6 +45,9 @@ public class EntityConverterManager {
*/
private final List<EntityConverter> converters;

/**
* Creates an entity converter manager containing the provided list of converters.
*/
public EntityConverterManager(List<EntityConverter> entityConverters) {
Comparator<EntityConverter> comparator = (o1, o2) -> {
int l = (o1 instanceof Ordered) ? ((Ordered) o1).getOrder() : 0;
Expand Down
2 changes: 2 additions & 0 deletions http-requests-documentation/src/docs/changelog.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ The changelog will be updated with release notes for each release of the library
* Ensure that a Content-Type specified in the request is preferred over the one provided
in the HTTP entity. This ensures that any Content-Type provided by a converter will not take
precedence over a user-specified one.
* Add http-requests-mock library to enable easier testing of code using
the http-requests libraries.
2.0.6::

Expand Down
2 changes: 2 additions & 0 deletions http-requests-documentation/src/docs/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,6 @@ include::spring.adoc[]

include::examples.adoc[]

include::testing.adoc[]

include::changelog.adoc[]
1 change: 1 addition & 0 deletions http-requests-documentation/src/docs/installing.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ for the HTTP Requests library and what their purposes are.
| `http-requests-spring` | Enables automatic configuration of the HTTP requests components as Spring
beans.
| `http-requests-groovy` | Adds Groovy DSLs to the library and Groovy-specific entity converters.
| `http-requests-mock` | Adds support for mocking HTTP requests in unit tests.
|===

NOTE: All modules are deployed under the group `com.budjb`.
Expand Down
132 changes: 132 additions & 0 deletions http-requests-documentation/src/docs/testing.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
== Testing

Since it is not ideal to make actual HTTP requests while performing unit tests,
a mock library exists that allows tests to inject responses for expected HTTP requests
made by the code being tested. Using the built-in request mocking functionality avoids
the need to use dynamically generated mocking and stubbing libraries such as Mockito
or Spock.

=== Request Mocks

A request mock contains the details about a potential HTTP request that should have a
mocked response. The `RequestMock` object contains most of the same properties that
a typical `HttpRequest` does, such as request URI, headers, and query parameters,
as well as some additional properties, such as the HTTP method that should match.

It also includes properties about the response, such as response status code, headers,
and the response entity.

The minimal requirement for a mock to match a request is that the URI must match the
request. If a mock has other details set, such as request headers, HTTP method, or
query parameters, those also must match. If they are not specified in the mock, they
are not considered as part of the match criteria. This allows tests to be as specific
or not as they need to be.

Mocks that match requests will have their counters incremented each time a match occurs.
The `RequestMock` object exposes these details via the `called` and `getCalledCount`
methods.

NOTE: If a no matching mock is found for an HTTP request, and `UnmatchedRequestMockException`
will be thrown.

=== MockHttpClientFactory

The `MockHttpClientFactory` class may be used in tests and injected into those objects
being tested. This factory implementation exposes the same interface as the other provider
libraries, and adds some additional methods specific to testing. Request mocks are
created via the `createMock` method, which returns and tracks an empty mock which may
be configured.

The factory exposes a method `allMocksCalled` that will return whether all created mocks
have been called at least once.

=== Example

Below is a simple Java object that makes an API request and returns the payload,
which is expected to be a simple `String`.

[source,java]
----
/**
* A test object that makes use of the HTTP requests library to fetch
* a string result from the API located at http://localhost/api.
*/
class TestObject {
private final HttpClientFactory httpClientFactory;
public TestObject(HttpClientFactory httpClientFactory) {
this.httpClientFactory = httpClientFactory;
}
public String makeApiCall() {
return httpClientFactory.createHttpClient.get("http://localhost/api").getEntity(String.class);
}
}
----

This class may be tested using a framework, such as Junit or Spock. A Spock
example follows:

[source,groovy]
----
class FunctionalSpec extends Specification {
/**
* Mock HTTP client factory.
*/
MockHttpClientFactory httpClientFactory
/**
* Entity converter manager.
*/
EntityConverterManager entityConverterManager
/**
* Configure the environment before each test.
*/
def setup() {
// Create an entity converter manager with the Groovy JSON converters
entityConverterManager = new EntityConverterManager([
new JsonEntityWriter(),
new JsonEntityReader()
])
// Create the mock HTTP client factory with the entity converter manager above
httpClientFactory = new MockHttpClientFactory(entityConverterManager)
}
def 'A simple mocked GET request returns the expected response entity'() {
setup:
// Create a mock for a specific URL with a response payload
RequestMock mock = httpClientFactory
.createMock()
.setRequestUri('http://localhost/foo/bar')
.setResponseEntity([
foo: 'bar',
baz: 'boz'
])
TestObject object = new TestObject(httpClientFactory)
when:
// Make the API call through the test object
def response = object.makeApiCall()
then:
// The mock should have been called at least once
mock.called()
// Since the above mock is the only one that was created, all mocks
// should have been called from the client factory.
httpClientFactory.allMocksCalled()
// The response should have been returned and converted properly
response == [
foo: 'bar',
baz: 'boz'
]
}
}
----

27 changes: 27 additions & 0 deletions http-requests-mock/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright 2016-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

apply from: "${rootDir}/gradle/library-publish.gradle"

apply plugin: "groovy"

dependencies {
api project(':http-requests-core')

testImplementation "org.codehaus.groovy:groovy:${groovyVersion}"
testImplementation "org.spockframework:spock-core:${spockVersion}"
testImplementation project(':http-requests-groovy')
}
16 changes: 16 additions & 0 deletions http-requests-mock/gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#
# Copyright 2016-2018 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
projectDescription=Mock requests with HTTP Requests.
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* Copyright 2016-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.budjb.httprequests.test;

import com.budjb.httprequests.*;
import com.budjb.httprequests.converter.EntityConverterManager;
import com.budjb.httprequests.filter.HttpClientFilterProcessor;

import java.io.IOException;

/**
* An implementation of {@link HttpClient} that uses the Jersey Client 1.x library.
*/
public class MockHttpClient extends AbstractHttpClient {
/**
* Mock HTTP client factory.
*/
private final MockHttpClientFactory httpClientFactory;

/**
* Constructor.
*
* @param converterManager Entity converter manager.
*/
MockHttpClient(MockHttpClientFactory httpClientFactory, EntityConverterManager converterManager) {
super(converterManager);
this.httpClientFactory = httpClientFactory;
}

/**
* {@inheritDoc}
*/
@Override
protected HttpResponse execute(HttpContext context, HttpEntity entity, HttpClientFilterProcessor filterProcessor) throws IOException {
HttpRequest request = context.getRequest();
HttpMethod method = context.getMethod();

RequestMock mock = httpClientFactory.findMatchingMock(context.getRequest(), context.getMethod());

if (mock == null) {
throw new UnmatchedRequestMockException(request, method);
}

mock.incrementCalled();

return new MockHttpResponse(request, getConverterManager(), mock);
}
}
Loading

0 comments on commit 58d063f

Please sign in to comment.