This post describes a simple way to create a test version of a service that reads JSON or XML from a REST service or similar. The purpose is to easily create a fake service that reads from files instead and that can be used for testing other code that use the service.
I believe in using production code as much as possible when running tests. Every time you use special test code, i.e., code that is only used for testing, you run the risk of the test code not behaving exactly the same as the production code. You also get a maintenance problem, where the test code must be kept up to date with respect to the production code.
Some types of code are inconvenient to use for testing, however. For example, database calls require setup and may be slow, and code calling REST services require the service to be available and again, may be slow. In a previous post, we saw a simple way to replace repositories calling a database with an in-memory version. In this post, we will see how to replace code calling a REST service with a version reading from file.
When creating a fake version of a piece of code, there are two things to keep in mind:
- The less of the production code you replace with test code, the easier it is to keep the two in sync.
- The test version should be tested using the same test suite as the production code, to verify that the two behave identically.
Getting Started
A sample project, rld-rest-sample, accompanying this post can be found on GitHub.
Use the following commands to download, build and run the sample project:
$ mkdir reallifedeveloper $ cd reallifedeveloper $ git clone https://github.com/reallifedeveloper/rld-rest-sample.git $ cd rld-rest-sample $ mvn -DcheckAll clean install # Should end with BUILD SUCCESS $ java -Dserver.port=8081 -jar target/rld-rest-sample-1.0.jar
You can now try the following URLs to see that everything is working:
Example Code
Assume that we want to create a REST service that can list the countries of the world, and also the states of a particular country. The main reason that this was chosen as example is that there are free online services that we can use for testing.
First of all, we define the CountryService
interface:
package com.reallifedeveloper.sample.domain; import java.io.IOException; import java.util.List; public interface CountryService { List<Country> allCountries() throws IOException; List<State> statesOfCountry(String alpha3Code) throws IOException; }
We now want to create an implementation of the CountryService
interface that uses the free services mentioned above, from a site called GroupKT. We call this implementation GroupKTCountryService
. To get started, we create integration tests that connect to the REST services and define the behavior we expect:
package com.reallifedeveloper.sample.infrastructure; // imports... public class GroupKTCountryServiceIT { @Rule public ExpectedException expectedException = ExpectedException.none(); private GroupKTCountryService service = new GroupKTCountryService("http://services.groupkt.com"); @Test public void allCountries() throws Exception { List<Country> allCountries = service().allCountries(); assertThat(allCountries, notNullValue()); assertThat(allCountries.size(), is(249)); } @Test public void indiaShouldHave36States() throws Exception { List<State> statesOfIndia = service().statesOfCountry("IND"); assertThat(statesOfIndia, notNullValue()); assertThat(statesOfIndia.size(), is(36)); } @Test public void unknownCountryShouldGiveEmptyList() throws Exception { List<State> statesOfUnknownCountry = service().statesOfCountry("foo"); assertThat(statesOfUnknownCountry, notNullValue()); assertThat(statesOfUnknownCountry.isEmpty(), is(true)); } @Test public void nullCountryShouldThrowException() throws Exception { expectedException.expect(IllegalArgumentException.class); expectedException.expectMessage("alpha3Code must not be null"); service().statesOfCountry(null); } @Test public void constructorNullBaseUrlShouldThrowException() { expectedException.expect(IllegalArgumentException.class); expectedException.expectMessage("baseUrl must not be null"); new GroupKTCountryService(null); } protected CountryService service() { return service; } }
Note the protected service
method that will be used later when we test the file version of the service.
The GroupKTCountryService
that is created together with the integration test is as follows:
package com.reallifedeveloper.sample.infrastructure; // imports... public class GroupKTCountryService implements CountryService { private final String baseUrl; public GroupKTCountryService(String baseUrl) { if (baseUrl == null) { throw new IllegalArgumentException("baseUrl must not be null"); } this.baseUrl = baseUrl; } @Override public List<Country> allCountries() throws IOException { String jsonCountries = jsonAllCountries(); ObjectMapper objectMapper = new ObjectMapper(); RestResponseWrapper<Country> countriesResponse = objectMapper.readValue(jsonCountries, new TypeReference<RestResponseWrapper<Country>>() {}); return countriesResponse.restResponse.result; } @Override public List<State> statesOfCountry(String alpha3Code) throws IOException { if (alpha3Code == null) { throw new IllegalArgumentException("alpha3Code must not be null"); } String jsonStates = jsonStatesOfCountry(alpha3Code); ObjectMapper objectMapper = new ObjectMapper(); RestResponseWrapper<State> statesResponse = objectMapper.readValue(jsonStates, new TypeReference<RestResponseWrapper<State>>() {}); return statesResponse.restResponse.result; } protected String jsonAllCountries() throws IOException { RestTemplate restTemplate = new RestTemplate(); return restTemplate.getForObject(baseUrl() + "/country/get/all", String.class); } protected String jsonStatesOfCountry(String alpha3Code) throws IOException { RestTemplate restTemplate2 = new RestTemplate(); String stateUrl = baseUrl() + "/state/get/" + alpha3Code + "/all"; return restTemplate2.getForObject(stateUrl, String.class); } protected String baseUrl() { return baseUrl; } private static final class RestResponseWrapper<T> { private final RestResponse<T> restResponse; @JsonCreator RestResponseWrapper(@JsonProperty("RestResponse") RestResponse<T> restResponse) { this.restResponse = restResponse; } private static final class RestResponse<T> { private final List<String> messages; private final List<T> result; @JsonCreator RestResponse(@JsonProperty("messages") List<String> messages, @JsonProperty("result") List<T> result) { this.messages = messages; this.result = result; } } } }
Note the protected jsonAllCountries
and jsonStatesOfCountry
methods that return a JSON string representing the different types of information. These methods are overridden in the FileCountryService
that reads JSON from files instead of over HTTP:
package com.reallifedeveloper.sample.infrastructure; import java.io.IOException; import com.reallifedeveloper.tools.test.TestUtil; public class FileCountryService extends GroupKTCountryService { public FileCountryService(String baseUrl) { super(baseUrl); } @Override protected String jsonAllCountries() throws IOException { return TestUtil.readResource(baseUrl() + "/all_countries.json"); } @Override protected String jsonStatesOfCountry(String alpha3Code) throws IOException { return TestUtil.readResource(baseUrl() + "/states_" + alpha3Code + ".json"); } }
The TestUtil.readResource
method comes from rld-build-tools
that is available from the central Maven repository and from GitHub. The method simply reads a file from classpath and returns its contents as a string.
We also need to add a few JSON files under src/test/resources/json
:
all_countries.json
that contains a list of all countriesstates_IND.json
that contains the states of Indiastates_foo.json
that contains an empty response
To test the FileCountryService
, we use the same test cases as for the GroupKTCountryService
, so we create a FileCountryServiceTest
that inherits from GroupKTCountryServiceIT
but plugs in a FileCountryService
to test instead of a GroupKTCountryService
:
package com.reallifedeveloper.sample.infrastructure; import com.reallifedeveloper.sample.domain.CountryService; public class FileCountryServiceTest extends GroupKTCountryServiceIT { private FileCountryService service = new FileCountryService("json"); @Override protected CountryService service() { return service; } }
We can now use the FileCountryService
when testing other code, for example application services or REST resources that use the service. We can be sure that it behaves like the real service since we run the same test suite on the two.
Packaging the Code
The test versions of your services, and the JSON or XML response files that you provide, should normally be under src/test
and will therefore not be available in the jar file created. If you need to use the test versions of services in other projects, you can easily configure Maven to create a jar file containing your test code:
<!-- Always generate a *-tests.jar with all test code --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>3.0.2</version> <executions> <execution> <goals> <goal>test-jar</goal> </goals> </execution> </executions> </plugin>
This will create a file called something like rld-rest-sample-1.0-tests.jar
containing the test code.
In other projects where you want to use the test versions of the services, add a dependency of type test-jar
:
<dependency> <groupId>com.reallifedeveloper</groupId> <artifactId>rld-rest-sample</artifactId> <version>1.0</version> <type>test-jar</type> <scope>test</scope> </dependency>
You can now use the test services and the packaged JSON or XML files when testing your other projects.
Summary
When you create a service that reads JSON or XML, isolate the methods that read over the network. Create a test version of the service that substitutes the methods with methods that read from local files instead. Also provide a few JSON or XML files that contain the responses you currently need during testing. It is easy to add more response files later if you need to add new test cases. Make sure that you run the same test cases on the file version of the service that you run on the real version.
Following these simple recommendations gives you a test version of the service that runs quickly and reliably, and that is guaranteed to be kept up to date with respect to the real version.
- Receiving Urgent Market Message Push Notifications from Nord Pool - November 1, 2018
- Zen and the Art of Computer Programming - September 2, 2017
- Reading JSON Files to Create Test Versions of REST Clients - April 8, 2017