diff --git a/README.md b/README.md index 71163cf..cbf88c2 100644 --- a/README.md +++ b/README.md @@ -118,10 +118,10 @@ To add a fixture all you have to do is call one of the `addFixture` methods in t serverRule.addFixture(200, "body.json"); ``` -This will add a response with status code 200 and the contents of the file `body.json` as the body. This file, by default, must be located in a folder with name `fixtures`. This folder works different for Local Tests and Instrumented Tests. +This will add a response with status code 200 and the contents of the file `body.json` as the body. This file, by default, must be located in a folder with name `fixtures`. This folder works differently for Local Tests and Instrumented Tests. - Local tests: these are run locally in the JVM (usually with Robolectric) and follow different conventions. Your source folder `test` may contain a folder `java` and a folder `resources`. When you compile your code it takes everything in the `resources` folder and puts in the root of your `.class` files. So, your fixtures folder must go inside `resources` folder. -- Instrumented tests: there are run in a device or emulator (usually with Espresso or Robotium). It follows the android folder layout and so you may have an assets folder inside your `androidTest` folder. Your `fixtures` folder must go there. +- Instrumented tests: these are run on a device or emulator (usually with Espresso or Robotium). It follows the android folder layout and so you may have an assets folder inside your `androidTest` folder. Your `fixtures` folder must go there. Because of these differences, there are two implementations of `RequestMatcherRule`: `LocalTestRequestMatcherRule` and `InstrumentedTestRequestMatcherRule`. You should use the generic type for your variable and instantiate it with the required type. Example: @@ -141,6 +141,34 @@ The difference is that when we run an InstrumentedTest, we must pass the instrum More on the difference between each kind of test [here](https://medium.com/concrete-solutions/android-local-or-instrumented-tests-9da545af7777#.mmowgemc4) +#### Fixture templating + +You can also provide templates for cases when you need dynamic values to be passed to the JSON. For +this to work you need to provide a JSON file with keys that will be replaced by their values in runtime. + +A valid JSON template is: + +``` json +{ + "current_date": "${current_date}" +} +``` + +And it can be used like this: + +```java + +String timestamp = String.valueOf(new Date(778338900L).getTime()); + +server.addTemplate("json_template.json") + .withValueForKey("current_date", timestamp) + .ifRequestMatches() + .pathIs("/get_current_date"); + +// interact with server and make response assertions + +``` + ## Configuring the `RequestMatcherRule` It is possible to pass some parameters to the server rule's constructor: diff --git a/requestmatcher/src/androidTest/assets/fixtures/json_error_template.json b/requestmatcher/src/androidTest/assets/fixtures/json_error_template.json new file mode 100644 index 0000000..32eac81 --- /dev/null +++ b/requestmatcher/src/androidTest/assets/fixtures/json_error_template.json @@ -0,0 +1,3 @@ +{ + "error_message": "${error}" +} diff --git a/requestmatcher/src/androidTest/assets/fixtures/json_template.json b/requestmatcher/src/androidTest/assets/fixtures/json_template.json new file mode 100644 index 0000000..62bc0f0 --- /dev/null +++ b/requestmatcher/src/androidTest/assets/fixtures/json_template.json @@ -0,0 +1,3 @@ +{ + "current_date": "${current_date}" +} diff --git a/requestmatcher/src/main/java/br/com/concretesolutions/requestmatcher/RequestMatcherRule.java b/requestmatcher/src/main/java/br/com/concretesolutions/requestmatcher/RequestMatcherRule.java index ef0be8a..368c4f2 100644 --- a/requestmatcher/src/main/java/br/com/concretesolutions/requestmatcher/RequestMatcherRule.java +++ b/requestmatcher/src/main/java/br/com/concretesolutions/requestmatcher/RequestMatcherRule.java @@ -126,7 +126,7 @@ public MockWebServer getMockWebServer() { public String readFixture(final String fixturePath) { try { return IOReader.read(open(fixturesRootFolder + "/" + fixturePath)) + "\n"; - } catch (IOException e) { + } catch (IOException | IllegalArgumentException e) { throw new RuntimeException("Failed to read asset with path " + fixturePath, e); } } @@ -187,25 +187,32 @@ public IfRequestMatches addFixture(String fixturePath) { */ public IfRequestMatches addFixture(int statusCode, String fixturePath) { - final MockResponse mockResponse = new MockResponse() - .setResponseCode(statusCode) - .setBody(readFixture(fixturePath)); - - if (guessMimeType) { - final String mimeType = IOReader.mimeTypeFromExtension(fixturePath); + String body = readFixture(fixturePath); + return addResponse(prepareMockResponse(statusCode, fixturePath, body)); + } - if (mimeType != null) { - mockResponse.addHeader("Content-Type", mimeType); - } - } + /** + * Adds a template to be used during the test case, later turning it into a fixture to be used. + * + * @param templatePath The path of the fixture inside the fixtures folder. + * @return A dsl instance {@link DynamicIfRequestMatches} for chaining + */ + public DynamicIfRequestMatches addTemplate(String templatePath) { + return addTemplate(200, templatePath); + } - if (!defaultHeaders.isEmpty()) { - for (String headerKey : defaultHeaders.keySet()) { - mockResponse.addHeader(headerKey, defaultHeaders.get(headerKey)); - } - } + /** + * Adds a template to be used during the test case, later turning it into a fixture to be used. + * + * @param templatePath The path of the fixture inside the fixtures folder. + * @param statusCode The status of the mocked response. + * @return A dsl instance {@link DynamicIfRequestMatches} for chaining + */ + public DynamicIfRequestMatches addTemplate(int statusCode, String templatePath) { - return addResponse(mockResponse); + String templateBody = readFixture(templatePath); + return new DynamicIfRequestMatches<>(new RequestMatchersGroup(), dispatcher, + templateBody, prepareMockResponse(statusCode, templatePath, "")); } /** @@ -258,6 +265,54 @@ public T ifRequestMatches() { } } + public static class DynamicIfRequestMatches extends IfRequestMatches { + + private final HashMap values = new HashMap<>(); + private final String templateBody; + private final MockResponse response; + private final MatcherDispatcher dispatcher; + + private DynamicIfRequestMatches(T group, MatcherDispatcher dispatcher, + String templateBody, MockResponse mockResponse) { + super(group); + this.templateBody = templateBody; + this.dispatcher = dispatcher; + this.response = mockResponse; + } + + @Override + public T ifRequestMatches() { + return dispatchDynamic(super.ifRequestMatches(), values); + } + + public DynamicIfRequestMatches withValueForKey(String key, String value) { + values.put(key, value); + return this; + } + + private T dispatchDynamic(T group, HashMap values) { + response.setBody(replaceValues(values)); + dispatcher.addFixture(response, group); + return group; + } + + private String replaceValues(HashMap values) { + String body = templateBody; + + for (String key : values.keySet()) { + String keyFinder = "${" + key + "}"; + + if (!body.contains(keyFinder)) + fail("Could not find any template key named " + key); + + body = body.replace(keyFinder, values.get(key)); + } + + return body; + } + + } + private void after(Exception exception, boolean success) throws Exception { if (dispatcher.getAssertionException() != null) { @@ -324,4 +379,27 @@ public void evaluate() throws Throwable { } }; } + + private MockResponse prepareMockResponse(int statusCode, String path, String body) { + + final MockResponse mockResponse = new MockResponse() + .setResponseCode(statusCode) + .setBody(body); + + if (guessMimeType) { + final String mimeType = IOReader.mimeTypeFromExtension(path); + + if (mimeType != null) { + mockResponse.addHeader("Content-Type", mimeType); + } + } + + if (!defaultHeaders.isEmpty()) { + for (String headerKey : defaultHeaders.keySet()) { + mockResponse.addHeader(headerKey, defaultHeaders.get(headerKey)); + } + } + + return mockResponse; + } } diff --git a/requestmatcher/src/test-common/java/br/com/concretesolutions/requestmatcher/test/RequestMatcherRuleTest.java b/requestmatcher/src/test-common/java/br/com/concretesolutions/requestmatcher/test/RequestMatcherRuleTest.java index e4882f1..a7b1548 100644 --- a/requestmatcher/src/test-common/java/br/com/concretesolutions/requestmatcher/test/RequestMatcherRuleTest.java +++ b/requestmatcher/src/test-common/java/br/com/concretesolutions/requestmatcher/test/RequestMatcherRuleTest.java @@ -8,6 +8,7 @@ import org.junit.rules.TestRule; import java.io.IOException; +import java.util.Date; import java.util.concurrent.TimeUnit; import br.com.concretesolutions.requestmatcher.RequestMatcherRule; @@ -799,4 +800,66 @@ public void informsAboutNotUsedFixture() throws IOException { client.newCall(request0).execute(); // will fail with message of non used matchers } + + @Test + public void canReplaceValuesInTemplate() throws IOException { + + String timestamp = String.valueOf(new Date(1519775413753L).getTime()); + + server.addTemplate("json_template.json") + .withValueForKey("current_date", timestamp) + .ifRequestMatches(); + + this.request = new Request.Builder() + .url(server.url("/get")) + .get() + .build(); + + final Response resp = client.newCall(request).execute(); + + assertThat(resp.isSuccessful(), is(true)); + assertThat(resp.headers().get("Content-Type"), is("application/json")); + assertThat(resp.peekBody(1_000_000).source().readUtf8(), + containsString("\"current_date\": \"1519775413753\"")); + } + + @Test + public void canReplaceValuesInTemplateWithNonSuccessfulResponse() throws IOException { + + server.addTemplate(404, "json_error_template.json") + .withValueForKey("error", "Resource not found") + .ifRequestMatches(); + + this.request = new Request.Builder() + .url(server.url("/get")) + .get() + .build(); + + final Response resp = client.newCall(request).execute(); + + assertThat(resp.code() == 404, is(true)); + assertThat(resp.headers().get("Content-Type"), is("application/json")); + assertThat(resp.peekBody(1_000_000).source().readUtf8(), + containsString("\"error_message\": \"Resource not found\"")); + } + + @Test + public void failsIfTemplateDoesNotHaveGivenKeys() throws IOException { + + exceptionRule.expect(AssertionError.class); + exceptionRule.expectMessage(containsString("Could not find any template key named something")); + + server.addTemplate("body.json") + .withValueForKey("something", "something") + .ifRequestMatches(); + + this.request = new Request.Builder() + .url(server.url("/get")) + .get() + .build(); + + client.newCall(request).execute(); + } + + } diff --git a/requestmatcher/src/test/resources/fixtures/json_error_template.json b/requestmatcher/src/test/resources/fixtures/json_error_template.json new file mode 100644 index 0000000..32eac81 --- /dev/null +++ b/requestmatcher/src/test/resources/fixtures/json_error_template.json @@ -0,0 +1,3 @@ +{ + "error_message": "${error}" +} diff --git a/requestmatcher/src/test/resources/fixtures/json_template.json b/requestmatcher/src/test/resources/fixtures/json_template.json new file mode 100644 index 0000000..62bc0f0 --- /dev/null +++ b/requestmatcher/src/test/resources/fixtures/json_template.json @@ -0,0 +1,3 @@ +{ + "current_date": "${current_date}" +}