Skip to content

Commit

Permalink
implemented match contains only which fixes #7
Browse files Browse the repository at this point in the history
  • Loading branch information
ptrthomas committed Feb 20, 2017
1 parent 113a5b8 commit 5a1f5b8
Show file tree
Hide file tree
Showing 8 changed files with 83 additions and 32 deletions.
24 changes: 19 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ And you don't need to create Java objects (or POJO-s) for any of the payloads th
# Features
* Scripts are plain-text files and require no compilation step or IDE
* Java knowledge is not required to write tests
* Syntax 'natively' supports JSON and XML - including [JsonPath](https://github.com/jayway/JsonPath) and XPath expressions
* Syntax 'natively' supports JSON and XML - including [JsonPath](https://github.com/jayway/JsonPath) and [XPath](https://www.w3.org/TR/xpath/) expressions
* Embedded JavaScript engine that enables you to build a library of re-usable functions that suit your specific environment
* Re-use of payload-data and user-defined functions across tests is so easy - that it becomes natural for the test-developer
* Built-in support for switching configuration across different environments (e.g. dev, QA, pre-prod)
Expand Down Expand Up @@ -147,7 +147,7 @@ When method get
Then status 200
# variant of the 'match' syntax to compare file contents
And match response * == read('test.pdf')
And match response == read('test.pdf')
```

# Getting Started
Expand Down Expand Up @@ -1139,7 +1139,7 @@ Given def cat =
]
}
"""
# normal 'equality' match. note the wildcard '*' in the JSONPath (returns an array)
# normal 'equality' match. note the wildcard '*' in the JsonPath (returns an array)
Then match cat.rivals[*].id == [23, 42]
# when inspecting a json array, 'contains' just checks if the expected items exist
Expand All @@ -1163,6 +1163,20 @@ When you use Karate, all your data assertions can be done in pure JSON and witho
forest of companion Java objects. And when you [`read`](#read) your JSON objects from (re-usable) files,
even complex response payload assertions can be accomplished in just a single line of Karate-script.

#### `match contains only`
For those cases where you need to assert that **all** array elements are present but in **any order**
you can do this:

```cucumber
* def data = { foo: [1, 2, 3] }
* match data.foo contains [1]
* match data.foo contains [3, 2]
* match data.foo contains only [3, 2, 1]
* match data.foo contains only [2, 3, 1]
# this will fail
# * match data.foo contains only [2, 3]
```

## Validate every element in a JSON array
### `match each`
Karate has syntax sugar that can iterate over all elements in a JSON array. Here's how it works:
Expand Down Expand Up @@ -1378,7 +1392,7 @@ special object in a variable named: `karate`. This provides the following metho
* `url`: URL of the HTTP call to be made
* `method`: HTTP method, can be lower-case
* `body`: JSON payload
* `karate.set(key, value)` - set the value of a variable immediately, which ensures that any active [`headers`](#headers) routine does the right thing for future HTTP calls (even those made by this function)
* `karate.set(key, value)` - set the value of a variable immediately, which ensures that any active [`headers`](#headers) routine does the right thing for future HTTP calls (even those made by this function being `call`-ed)
* `karate.get(key)` - get the value of a variable by name, if not found - this returns `null` which is easier to handle in JavaScript (than `undefined`)
* `karate.log(... args)` - log to the same logger being used by the parent process
* `karate.env` - gets the value (read-only) of the environment setting 'karate.env' used for bootstrapping [configuration](#configuration)
Expand Down Expand Up @@ -1434,7 +1448,7 @@ function(creds) {
}
```
And here's how it works in a test-script. Note that you need to do this only once within a `Scenario:`,
perhaps at the beginning.
perhaps at the beginning, or within the `Background:` section.
```cucumber
* header Authorization = call read('basic-auth.js') { username: 'john', password: 'secret' }
Expand Down
1 change: 1 addition & 0 deletions karate-core/src/main/java/com/intuit/karate/MatchType.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ public enum MatchType {

EQUALS,
CONTAINS,
CONTAINS_ONLY,
EACH_EQUALS,
EACH_CONTAINS

Expand Down
42 changes: 29 additions & 13 deletions karate-core/src/main/java/com/intuit/karate/Script.java
Original file line number Diff line number Diff line change
Expand Up @@ -422,12 +422,19 @@ public static AssertionResult matchStringOrPattern(MatchType matchType, ScriptVa
}
} else {
String actual = actValue.getAsString();
if (matchType == MatchType.CONTAINS) {
if (!actual.contains(expected)) {
return matchFailed(path, actual, expected + " (not a sub-string)");
}
} else if (!expected.equals(actual)) {
return matchFailed(path, actual, expected);
switch (matchType) {
case CONTAINS:
if (!actual.contains(expected)) {
return matchFailed(path, actual, expected + " (not a sub-string)");
}
break;
case EQUALS:
if (!expected.equals(actual)) {
return matchFailed(path, actual, expected);
}
break;
default:
throw new RuntimeException("unsupported match type for string: " + matchType);
}
}
return AssertionResult.PASS;
Expand Down Expand Up @@ -455,6 +462,14 @@ public static AssertionResult matchXmlPath(MatchType matchType, ScriptValue actu
}
return matchNestedObject('/', path, matchType, actObject, expObject, context);
}

private static MatchType getInnerMatchType(MatchType outerMatchType) {
switch (outerMatchType) {
case EACH_CONTAINS: return MatchType.CONTAINS;
case EACH_EQUALS: return MatchType.EQUALS;
default: throw new RuntimeException("unexpected outer match type: " + outerMatchType);
}
}

public static AssertionResult matchJsonPath(MatchType matchType, ScriptValue actual, String path, String expression, ScriptContext context) {
DocumentContext actualDoc;
Expand Down Expand Up @@ -492,14 +507,15 @@ public static AssertionResult matchJsonPath(MatchType matchType, ScriptValue act
expObject = expected.getValue();
}
switch (matchType) {
case CONTAINS_ONLY:
case CONTAINS:
case EQUALS:
return matchNestedObject('.', path, matchType, actObject, expObject, context);
case EACH_EQUALS:
case EACH_CONTAINS:
case EACH_EQUALS:
if (actObject instanceof List) {
List actList = (List) actObject;
MatchType listMatchType = matchType == MatchType.EACH_CONTAINS ? MatchType.CONTAINS : MatchType.EQUALS;
MatchType listMatchType = getInnerMatchType(matchType);
int actSize = actList.size();
for (int i = 0; i < actSize; i++) {
Object actListObject = actList.get(i);
Expand All @@ -511,7 +527,7 @@ public static AssertionResult matchJsonPath(MatchType matchType, ScriptValue act
}
return AssertionResult.PASS;
} else {
throw new RuntimeException("'match all' failed, not a json array: + " + actual + ", path: " + path);
throw new RuntimeException("'match each' failed, not a json array: + " + actual + ", path: " + path);
}
default:
// dead code
Expand Down Expand Up @@ -553,7 +569,7 @@ public static AssertionResult matchNestedObject(char delimiter, String path, Mat
if (matchType != MatchType.CONTAINS && actMap.size() > expMap.size()) { // > is because of the chance of #ignore
return matchFailed(path, actObject, expObject);
}
for (Map.Entry<String, Object> expEntry : expMap.entrySet()) {
for (Map.Entry<String, Object> expEntry : expMap.entrySet()) { // TDDO should we assert order, maybe XML needs this ?
String key = expEntry.getKey();
String childPath = path + delimiter + key;
AssertionResult ar = matchNestedObject(delimiter, childPath, MatchType.EQUALS, actMap.get(key), expEntry.getValue(), context);
Expand All @@ -570,7 +586,7 @@ public static AssertionResult matchNestedObject(char delimiter, String path, Mat
if (matchType != MatchType.CONTAINS && actCount != expCount) {
return matchFailed(path, actObject, expObject);
}
if (matchType == MatchType.CONTAINS) { // just checks for existence
if (matchType == MatchType.CONTAINS || matchType == MatchType.CONTAINS_ONLY) { // just checks for existence
for (Object expListObject : expList) { // for each expected item in the list
boolean found = false;
for (int i = 0; i < actCount; i++) {
Expand Down Expand Up @@ -641,14 +657,14 @@ public static void setValueByPath(String name, String path, String exp, ScriptCo
} else if (isXmlPath(path)) {
Document doc = context.vars.get(name, Document.class);
ScriptValue sv = preEval(exp, context);
switch(sv.getType()) {
switch (sv.getType()) {
case XML:
Node node = sv.getValue(Node.class);
XmlUtils.setByPath(doc, path, node);
break;
default:
XmlUtils.setByPath(doc, path, sv.getAsString());
}
}
} else {
throw new RuntimeException("unexpected path: " + path);
}
Expand Down
1 change: 0 additions & 1 deletion karate-core/src/main/java/com/intuit/karate/SslUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.security.cert.CertificateException;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down
33 changes: 21 additions & 12 deletions karate-core/src/main/java/com/intuit/karate/StepDefs.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.NewCookie;
import javax.ws.rs.core.Response;
import org.apache.commons.lang3.StringUtils;
import org.glassfish.jersey.media.multipart.BodyPart;
import org.glassfish.jersey.media.multipart.FormDataBodyPart;
import org.glassfish.jersey.media.multipart.MultiPart;
Expand Down Expand Up @@ -396,32 +395,42 @@ public void status(int status) {
assertEquals(status, response.getStatus());
}

private static MatchType toMatchType(String each, boolean contains) {
return each == null
? contains ? MatchType.CONTAINS : MatchType.EQUALS
: contains ? MatchType.EACH_CONTAINS : MatchType.EACH_EQUALS;
private static MatchType toMatchType(String each, String only, boolean contains) {
if (each == null) {
if (contains) {
return only == null ? MatchType.CONTAINS : MatchType.CONTAINS_ONLY;
} else {
return MatchType.EQUALS;
}
} else {
if (contains) {
return MatchType.EACH_CONTAINS;
} else {
return MatchType.EACH_EQUALS;
}
}
}

@Then("^match (each )?([^\\s]+)( .+)? ==$")
public void matchEqualsDocString(String each, String name, String path, String expected) {
matchEquals(each, name, path, expected);
}

@Then("^match (each )?([^\\s]+)( .+)? contains$")
public void matchContainsDocString(String each, String name, String path, String expected) {
matchContains(each, name, path, expected);
@Then("^match (each )?([^\\s]+)( .+)? contains( only)?$")
public void matchContainsDocString(String each, String name, String path, String only, String expected) {
matchContains(each, name, path, only, expected);
}


@Then("^match (each )?([^\\s]+)( .+)? == (.+)")
public void matchEquals(String each, String name, String path, String expected) {
MatchType mt = toMatchType(each, false);
MatchType mt = toMatchType(each, null, false);
matchNamed(mt, name, path, expected);
}

@Then("^match (each )?([^\\s]+)( .+)? contains (.+)")
public void matchContains(String each, String name, String path, String expected) {
MatchType mt = toMatchType(each, true);
@Then("^match (each )?([^\\s]+)( .+)? contains( only)?(.+)")
public void matchContains(String each, String name, String path, String only, String expected) {
MatchType mt = toMatchType(each, only, true);
matchNamed(mt, name, path, expected);
}

Expand Down
1 change: 0 additions & 1 deletion karate-core/src/main/java/com/intuit/karate/XmlUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.DocumentFragment;
import org.w3c.dom.Node;

/**
Expand Down
5 changes: 5 additions & 0 deletions karate-core/src/test/java/com/intuit/karate/ScriptTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,11 @@ public void testMatchAllJsonPath() {
ctx.vars.put("myJson", doc);
ScriptValue myJson = ctx.vars.get("myJson");
assertTrue(Script.matchJsonPath(MatchType.EQUALS, myJson, "$.foo", "[{bar: 1, baz: 'a'}, {bar: 2, baz: 'b'}, {bar:3, baz: 'c'}]", ctx).pass);
assertTrue(Script.matchJsonPath(MatchType.CONTAINS, myJson, "$.foo", "[{bar: 1, baz: 'a'}, {bar: 2, baz: 'b'}, {bar:3, baz: 'c'}]", ctx).pass);
assertTrue(Script.matchJsonPath(MatchType.CONTAINS_ONLY, myJson, "$.foo", "[{bar: 1, baz: 'a'}, {bar: 2, baz: 'b'}, {bar:3, baz: 'c'}]", ctx).pass);
// shuffle
assertTrue(Script.matchJsonPath(MatchType.CONTAINS_ONLY, myJson, "$.foo", "[{bar: 2, baz: 'b'}, {bar:3, baz: 'c'}, {bar: 1, baz: 'a'}]", ctx).pass);
assertFalse(Script.matchJsonPath(MatchType.CONTAINS_ONLY, myJson, "$.foo", "[{bar: 1, baz: 'a'}, {bar: 2, baz: 'b'}]", ctx).pass);
assertTrue(Script.matchJsonPath(MatchType.EACH_EQUALS, myJson, "$.foo", "{bar:'#number', baz:'#string'}", ctx).pass);
assertTrue(Script.matchJsonPath(MatchType.EACH_CONTAINS, myJson, "$.foo", "{bar:'#number'}", ctx).pass);
assertTrue(Script.matchJsonPath(MatchType.EACH_CONTAINS, myJson, "$.foo", "{baz:'#string'}", ctx).pass);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,14 @@ Then match pdf == read('test.pdf')
* match foo contains { bar:1, baz: 'hello' }
# * match foo == { bar:1, baz: 'hello' }

# match contains only
* def data = { foo: [1, 2, 3] }
* match data.foo contains [1]
* match data.foo contains [3, 2]
* match data.foo contains only [3, 2, 1]
* match data.foo contains only [2, 3, 1]
# * match data.foo contains only [2, 3]

# match each
* def data = { foo: [{ bar: 1, baz: 'a' }, { bar: 2, baz: 'b' }, { bar: 3, baz: 'c' }]}
* match each data.foo == { bar: '#number', baz: '#string' }
Expand Down

0 comments on commit 5a1f5b8

Please sign in to comment.