Skip to content

Commit

Permalink
Setup TestKit with random walker #433
Browse files Browse the repository at this point in the history
RandomWalkLinkChecker introduced that allows to randomly walk a API endpoint
by following links to verify that GET requests can successfully by
searched.
  • Loading branch information
remmeier committed Nov 4, 2019
1 parent a793f3c commit 2b2314e
Show file tree
Hide file tree
Showing 6 changed files with 229 additions and 0 deletions.
1 change: 1 addition & 0 deletions crnk-documentation/src/docs/asciidoc/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ include::resource.adoc[]
include::repositories.adoc[]
include::client.adoc[]
include::format.adoc[]
include::testkit.adoc[]
include::reactive.adoc[]
include::security.adoc[]
include::dataaccess.adoc[]
Expand Down
31 changes: 31 additions & 0 deletions crnk-documentation/src/docs/asciidoc/testkit.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
anchor:testkit[]

# TestKit

With `crnk-testkit` is in the early stages of providing utilities to facilitate testing.
`RandomWalkLinkChecker` is a class that performs a random walk on an API endpoint by following
all the links as specified by JSON:API. It will verify that each links provides a valid
`2xx` response code. The setup looks like:

[source,java]
----
CrnkClient client = ...
HttpAdapter httpAdapter = client.getHttpAdapter();
RandomWalkLinkChecker linkChecker = new RandomWalkLinkChecker(httpAdapter);
linkChecker.setWalkLength(100);
linkChecker.addStartUrl(...)
linkChecker.performCheck();
----

More possibilities for automation in the future are to add checks for:

- paging
- filtering
- sorting
- security
- field sets
- ...
11 changes: 11 additions & 0 deletions crnk-testkit/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
apply plugin: 'java'

dependencies {
compile project(':crnk-core')
compile project(':crnk-client')
testCompile project(':crnk-test')
testCompile project(':crnk-home')

compile 'org.assertj:assertj-core:3.9.1'
compile 'org.apache.httpcomponents:httpclient:4.5.2'
}
143 changes: 143 additions & 0 deletions crnk-testkit/src/main/java/io/crnk/testkit/RandomWalkLinkChecker.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package io.crnk.testkit;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.crnk.client.http.HttpAdapter;
import io.crnk.client.http.HttpAdapterRequest;
import io.crnk.client.http.HttpAdapterResponse;
import io.crnk.core.engine.http.HttpMethod;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Performs a random walk across an API endpoint by following JSON:API links.
*/
public class RandomWalkLinkChecker {

private static final Logger LOGGER = LoggerFactory.getLogger(RandomWalkLinkChecker.class);

private final HttpAdapter httpAdapter;

private int walkLength = 1000;

private Set<String> visited = new HashSet<>();

private List<String> upcoming = new ArrayList<>();

private ObjectMapper mapper = new ObjectMapper();

private String currentUrl;

public RandomWalkLinkChecker(HttpAdapter httpAdapter) {
this.httpAdapter = httpAdapter;
}

public void addStartUrl(String url) {
upcoming.add(url);
}

public void setWalkLength(int walkLength) {
this.walkLength = walkLength;
}

public int getWalkLength() {
return walkLength;
}

public Set<String> performCheck() {
Random random = new Random();

int index = 0;
while (index < walkLength && !upcoming.isEmpty()) {

int nextIndex = random.nextInt(upcoming.size());

currentUrl = upcoming.remove(nextIndex);
if (!visited.contains(currentUrl)) {
visited.add(currentUrl);
index++;
visit(currentUrl);
}
}
return visited;
}

private void visit(String url) {
try {
LOGGER.info("visiting {}", url);
HttpAdapterRequest request = httpAdapter.newRequest(url, HttpMethod.GET, null);
HttpAdapterResponse response = request.execute();
int code = response.code();
if (code >= 300) {
throw new IllegalStateException("expected endpoint to return success status code, got " + code + " from " + url);
}
String body = response.body();
if (body == null) {
throw new IllegalStateException("expected a body to be returned from " + url);
}

JsonNode jsonNode = mapper.reader().readTree(body);
findLinks(jsonNode);
} catch (IOException e) {
throw new IllegalStateException("failed to visit " + url, e);
}
}

private void findLinks(JsonNode jsonNode) {
if (jsonNode instanceof ArrayNode) {
ArrayNode arrayNode = (ArrayNode) jsonNode;
for (int i = 0; i < arrayNode.size(); i++) {
JsonNode childNode = arrayNode.get(i);
findLinks(childNode);
}
} else if (jsonNode instanceof ObjectNode) {
ObjectNode objectNode = (ObjectNode) jsonNode;
Iterator<Map.Entry<String, JsonNode>> fields = objectNode.fields();
while (fields.hasNext()) {
Map.Entry<String, JsonNode> entry = fields.next();
String name = entry.getKey();
if (name.equals("links")) {
JsonNode linksValue = entry.getValue();
if(!(linksValue instanceof ObjectNode)){
throw new IllegalStateException("illegal use of links field in " + currentUrl + ": " + linksValue);
}
collectLinks((ObjectNode) linksValue);
} else {
findLinks(entry.getValue());
}
}
}
}

private void collectLinks(ObjectNode linksNode) {
Iterator<Map.Entry<String, JsonNode>> iterator = linksNode.fields();
while (iterator.hasNext()) {
Map.Entry<String, JsonNode> entry = iterator.next();
JsonNode link = entry.getValue();
String url = link.asText();
if (url == null || !url.startsWith("http")) {
try {
String linkName = entry.getKey();
throw new IllegalStateException(
"expected link `" + linkName + "` from " + currentUrl + " to contain a valid link, got " + url + " from " + mapper.writer().writeValueAsString(linksNode));
} catch (JsonProcessingException e) {
throw new IllegalStateException(e);
}
} else if (!visited.contains(url)) {
upcoming.add(url);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package io.crnk.testkit;


import java.util.Set;

import io.crnk.client.http.inmemory.InMemoryHttpAdapter;
import io.crnk.core.boot.CrnkBoot;
import io.crnk.core.module.SimpleModule;
import io.crnk.core.repository.InMemoryResourceRepository;
import io.crnk.home.HomeModule;
import io.crnk.test.mock.models.BulkTask;
import io.crnk.test.mock.models.HistoricTask;
import org.junit.Assert;
import org.junit.Test;

public class RandomWalkLinkCheckerTest {

@Test
public void test() {
SimpleModule testModule = new SimpleModule("test");
testModule.addRepository(new InMemoryResourceRepository<>(BulkTask.class));
testModule.addRepository(new InMemoryResourceRepository<>(HistoricTask.class));

CrnkBoot boot = new CrnkBoot();
boot.addModule(HomeModule.create());
boot.addModule(testModule);
boot.boot();

String baseUrl = "http://localhost";
InMemoryHttpAdapter adapter = new InMemoryHttpAdapter(boot, baseUrl);

RandomWalkLinkChecker checker = new RandomWalkLinkChecker(adapter);
checker.addStartUrl(baseUrl + "/");
Set<String> visited = checker.performCheck();
Assert.assertTrue(visited.contains("http://localhost/tasks/history"));
Assert.assertTrue(visited.contains("http://localhost/"));
Assert.assertTrue(visited.contains("http://localhost/bulkTasks"));
Assert.assertTrue(visited.contains("http://localhost/tasks/"));
Assert.assertEquals(4, visited.size());
}

}
1 change: 1 addition & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ include ':crnk-core'
include ':crnk-reactive'
include ':crnk-home'
include ':crnk-test'
include ':crnk-testkit'
include ':crnk-client'
include ':crnk-meta'
include ':crnk-validation'
Expand Down

0 comments on commit 2b2314e

Please sign in to comment.