Skip to content
This repository has been archived by the owner on Sep 12, 2022. It is now read-only.

Fixture templating: adds the ability to create fixtures from templates files using dynamic values #16

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 29 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand All @@ -141,6 +141,33 @@ 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("/get_current_date");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The method ifRequestMatches() does not take arguments right?


// interact with server and make response assertions

```

## Configuring the `RequestMatcherRule`

It is possible to pass some parameters to the server rule's constructor:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"error_message": "${error}"
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please add a new line at the end of the file?

Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"current_date": "${current_date}"
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here.

Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What triggers dynamically IllegalArgument?

Copy link
Author

@mcassiano mcassiano Mar 20, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IOReader's read throws IllegalArgumentException when it receives a null arg, so if someone tries to open a file that does not exist, IllegalArgumentException is thrown without much detail. If you catch it, the exception is rethrown as a RuntimeException and with a helpful message (stating the resolved path for the file).

Copy link
Author

@mcassiano mcassiano Mar 20, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another way to go around this is to throw a FileNotFoundException when getResourceAsStream returns null inside LocalTestRequestMatcherRule's open. Let me know what you think is best!

throw new RuntimeException("Failed to read asset with path " + fixturePath, e);
}
}
Expand Down Expand Up @@ -187,25 +187,32 @@ public IfRequestMatches<RequestMatchersGroup> addFixture(String fixturePath) {
*/
public IfRequestMatches<RequestMatchersGroup> 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<RequestMatchersGroup> 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<RequestMatchersGroup> addTemplate(int statusCode, String templatePath) {

return addResponse(mockResponse);
String templateBody = readFixture(templatePath);
return new DynamicIfRequestMatches<>(new RequestMatchersGroup(), dispatcher,
templateBody, prepareMockResponse(statusCode, templatePath, ""));
}

/**
Expand Down Expand Up @@ -258,6 +265,54 @@ public T ifRequestMatches() {
}
}

public static class DynamicIfRequestMatches<T extends RequestMatchersGroup> extends IfRequestMatches<T> {

private final HashMap<String, String> 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<T> withValueForKey(String key, String value) {
values.put(key, value);
return this;
}

private T dispatchDynamic(T group, HashMap<String, String> values) {
response.setBody(replaceValues(values));
dispatcher.addFixture(response, group);
return group;
}

private String replaceValues(HashMap<String, String> 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) {
Expand Down Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"error_message": "${error}"
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New line here too please.

3 changes: 3 additions & 0 deletions requestmatcher/src/test/resources/fixtures/json_template.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"current_date": "${current_date}"
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another new line :)