Skip to content

Commit 113a5b8

Browse files
committed
implemented set for xml chunks, release v0.1.5
1 parent a3d6bfc commit 113a5b8

File tree

9 files changed

+89
-43
lines changed

9 files changed

+89
-43
lines changed

README.md

Lines changed: 43 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,12 @@ And you don't need to create Java objects (or POJO-s) for any of the payloads th
4747
| [`status`](#status) | [`multipart post`](#multipart-post) | [`soap action`](#soap-action) | [`ssl enabled`](#ssl-enabled)
4848
**Secondary HTTP Keywords** | [`param`](#param) | [`header`](#header) | [`cookie`](#cookie)
4949
| [`form field`](#form-field) | [`multipart field`](#multipart-field) | [`multipart entity`](#multipart-entity)
50-
**Set, Match, Assert** | [`set`](#set) | [`match`](#match) | [`contains`](#match-contains) | [Ignore / Vallidate](#ignore-or-validate)
50+
**Set, Match, Assert** | [`set`](#set) | [`match`](#match) | [`match contains`](#match-contains) | [`match each`](#match-each)
5151
**Special Variables** | [`headers`](#headers) | [`response`](#response) | [`cookies`](#cookies) | [`read`](#read)
5252
| [`responseHeaders`](#responseheaders) | [`responseStatus`](#responsestatus) | [`responseTime`](#responsetime)
5353
**Reusable Functions** | [`call`](#call) | [`karate` object](#the-karate-object)
5454
**Tips and Tricks** | [Embedded Expressions](#embedded-expressions) | [GraphQL RegEx Example](#graphql--regex-replacement-example) | [Multi-line Comments](#multi-line-comments) | [Cucumber Tags](#cucumber-tags)
55-
| [Data Driven Tests](#data-driven-tests) | [Auth](#sign-in-example) and [Headers](#http-basic-authentication-example) | [Dynamic Port Numbers](#dynamic-port-numbers)
55+
| [Data Driven Tests](#data-driven-tests) | [Auth](#sign-in-example) and [Headers](#http-basic-authentication-example) | [Dynamic Port Numbers](#dynamic-port-numbers) | [Ignore / Vallidate](#ignore-or-validate)
5656

5757
# Features
5858
* Scripts are plain-text files and require no compilation step or IDE
@@ -162,7 +162,7 @@ This is all that you need within your `<dependencies>`:
162162
<dependency>
163163
<groupId>com.intuit.karate</groupId>
164164
<artifactId>karate-core</artifactId>
165-
<version>0.1.4</version>
165+
<version>0.1.5</version>
166166
<scope>test</scope>
167167
</dependency>
168168
```
@@ -178,7 +178,7 @@ You can replace the values of 'com.mycompany' and 'myproject' as per your needs.
178178
mvn archetype:generate \
179179
-DarchetypeGroupId=com.intuit.karate \
180180
-DarchetypeArtifactId=karate-archetype \
181-
-DarchetypeVersion=0.1.4 \
181+
-DarchetypeVersion=0.1.5 \
182182
-DgroupId=com.mycompany \
183183
-DartifactId=myproject
184184
```
@@ -516,10 +516,10 @@ no need to compile Java code any more.
516516

517517
- | Cucumber | Karate
518518
------ | -------- | ------
519-
**Must Add Step Definitions** | Yes. You need to keep implementing them as your functionality grows. [This can get very tedious](https://angiejones.tech/rest-assured-with-cucumber-using-bdd-for-web-services-automation#comment-40). | No.
520-
**Layers of Code to Maintain** | **2** : Gherkin (custom grammar), and corresponding Java step-definitions | **1** : Karate DSL
521-
**Natural Language** | Yes. Cucumber will read like natural language if you implement the step-definitions right. | No. Although Karate is a [true DSL](https://ayende.com/blog/2984/dsl-vs-fluent-interface-compare-contrast), it is ultimately a mini-programming language, although a very simple one. But for testing web-services at the level of HTTP requests and responses, it is ideal. Keep in mind that web-services are not 'human-facing' by design.
522-
**BDD Syntax** | Yes | Yes
519+
**More Step Definitions Needed** | **Yes**. You need to keep implementing them as your functionality grows. [This can get very tedious](https://angiejones.tech/rest-assured-with-cucumber-using-bdd-for-web-services-automation#comment-40). | **No**.
520+
**Layers of Code to Maintain** | **2** Layers. One is the Gherkin custom grammar for your business domain, and you will also have the corresponding Java step-definitions. | **1** Layer. Just Karate-script, and no Java code needs to be implemented.
521+
**Natural Language** | **Yes**. Cucumber will read like natural language if you implement the step-definitions right. | **No**. Although Karate is simple, and a [true DSL](https://ayende.com/blog/2984/dsl-vs-fluent-interface-compare-contrast), it is ultimately a mini-programming language. But for testing web-services at the level of HTTP requests and responses - it is ideal.
522+
**BDD Syntax** | **Yes** | **Yes**
523523

524524
One nice thing about the design of the underlying Cucumber framework is that
525525
script-steps are treated the same no matter whether they start with the keyword
@@ -622,7 +622,7 @@ the elegance of JSON to express complex nested data - while at the same time bei
622622
able to dynamically plug values (that could be also JSON trees) into a JSON 'template'.
623623

624624
The [GraphQL / RegEx Replacement example](#graphql--regex-replacement-example) also demonstrates the usage
625-
of 'embedded expressions', e.g. `'#(query)'`.
625+
of 'embedded expressions', look for: `'#(query)'`.
626626

627627
### Multi-Line Expressions
628628
The keywords [`def`](#def), [`set`](#set), [`match`](#match) and [`request`](#request) take multi-line input as
@@ -700,11 +700,11 @@ function(s) {
700700
If you want to do advanced stuff such as make HTTP requests within a function -
701701
that is what the [`call`](#call) keyword is for.
702702

703-
[More examples](#calling-java) of calling Java appear later in this document.
703+
[More examples](#calling-java) of calling Java appear later on in this document.
704704

705705
## Reading Files
706706
This actually is a good example of how you could extend Karate with custom functions.
707-
`read()` is a JavaScript function that is automatically available when Karate starts.
707+
The variable `read` is a JavaScript function that is automatically available when Karate starts.
708708
It takes the name of a file as the only argument.
709709

710710
By default, the file is expected to be in the same folder (package) as the *.feature file.
@@ -884,7 +884,7 @@ When multipart post
884884
Then status 201
885885
```
886886

887-
# Multipart and SOAP
887+
# Multipart, SOAP and SSL
888888
## `multipart post`
889889
Since a multipart request needs special handling, this is a rare case where the
890890
[`method`](#method) step is not used to actually fire the request to the server. The only other
@@ -911,6 +911,7 @@ And match response /Envelope/Body/QueryUsageBalanceResponse == read('expected-re
911911
## `ssl enabled`
912912
This switches on Karate's support for making HTTPS calls without needing to configure a trusted certificate
913913
or key-store. It is recommended that you do this at the start of your script or in the `Background:` section.
914+
This built-in 'relaxed' mode is not enabled by default.
914915
```cucumber
915916
* ssl enabled
916917
```
@@ -994,15 +995,17 @@ Setting values on JSON documents is simple using the `set` keyword and JSON-Path
994995
# you can ignore fields marked with '#ignore'
995996
* match myJson == { cat: '#ignore', hey: 'ho', foo: 'world', zee: [5] }
996997
```
997-
XML and XPath is similar.
998-
> TODO: XML `set` support is limited to text-content and xml-attributes as of now
999-
(not XML chunks).
1000998

999+
XML and XPath works just like you'd expect.
10011000
```cucumber
1002-
# xml set
10031001
* def cat = <cat><name>Billie</name></cat>
10041002
* set cat /cat/name = 'Jean'
10051003
* match cat / == <cat><name>Jean</name></cat>
1004+
1005+
# you can even set whole fragments of xml
1006+
* def xml = <foo><bar>baz</bar></foo>
1007+
* set xml/foo/bar = <hello>world</hello>
1008+
* match xml == <foo><hello>world</hello></foo>
10061009
```
10071010

10081011
## Ignore or Validate
@@ -1160,7 +1163,7 @@ When you use Karate, all your data assertions can be done in pure JSON and witho
11601163
forest of companion Java objects. And when you [`read`](#read) your JSON objects from (re-usable) files,
11611164
even complex response payload assertions can be accomplished in just a single line of Karate-script.
11621165

1163-
## Matching All Array Elements
1166+
## Validate every element in a JSON array
11641167
### `match each`
11651168
Karate has syntax sugar that can iterate over all elements in a JSON array. Here's how it works:
11661169
```cucumber
@@ -1375,7 +1378,7 @@ special object in a variable named: `karate`. This provides the following metho
13751378
* `url`: URL of the HTTP call to be made
13761379
* `method`: HTTP method, can be lower-case
13771380
* `body`: JSON payload
1378-
* `karate.set(key, value)` - set the value of a variable immediately, which ensures that the [`headers`](#headers) routine for any HTTP calls made before this JavaScript function exits - works as expected
1381+
* `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)
13791382
* `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`)
13801383
* `karate.log(... args)` - log to the same logger being used by the parent process
13811384
* `karate.env` - gets the value (read-only) of the environment setting 'karate.env' used for bootstrapping [configuration](#configuration)
@@ -1404,7 +1407,8 @@ Either - it can be assigned to a variable like so.
14041407
Or - if a `call` is made without an assignment, and if the function returns a map-like
14051408
object, it will add each key-value pair returned as a new variable into the execution context.
14061409
```cucumber
1407-
# while this looks innocent, behind the scenes it could be creating (or over-writing) lots of variables !
1410+
# while this looks innocent ...
1411+
# ... behind the scenes, it could be creating (or over-writing) a bunch of variables !
14081412
* call someFunction
14091413
```
14101414
While this sounds dangerous and should be used with care (and limits readability), the reason
@@ -1418,7 +1422,7 @@ You can invoke a function in a [re-usable file](#reading-files) using this short
14181422
### HTTP Basic Authentication Example
14191423
This should make it clear why Karate does not provide 'out of the box' support for any particular HTTP authentication scheme.
14201424
Things are designed so that you can plug-in what you need, without needing to compile Java code. You get to choose how to
1421-
manage the environment-specific configuration values such as user-names and passwords.
1425+
manage your environment-specific configuration values such as user-names and passwords.
14221426

14231427
First the JavaScript file, `basic-auth.js`:
14241428
```javascript
@@ -1490,12 +1494,13 @@ function() {
14901494

14911495
## GraphQL / RegEx replacement example
14921496
As a demonstration of Karate's power and flexibility, here is an example that reads a
1493-
GraphQL string (which could be from a file) and modifies it to build custom queries
1497+
GraphQL string (which could be from a file) and manipulates it to build custom dynamic queries
14941498
and filter criteria.
14951499

14961500
Once the function is declared, observe how calling it and performing the replacement
14971501
is an elegant one-liner.
14981502
```cucumber
1503+
# this function would normally reside in a file
14991504
* def replacer =
15001505
"""
15011506
function(args) {
@@ -1509,14 +1514,23 @@ function(args) {
15091514
return query;
15101515
}
15111516
"""
1512-
# in real life this line would likely read from a file
1513-
* def query = 'query q { company { taxAgencies { } } }'
1514-
# the next line is where the magic happens
1517+
1518+
# this 'base GraphQL query' would also likely be read from a file in real-life
1519+
* def query = 'query q { company { taxAgencies { edges { node { id, name } } } } }'
1520+
1521+
# the next line is where the criteria is injected using the regex function
15151522
* def query = call replacer { query: '#(query)', field: 'taxAgencies', criteria: 'first: 5' }
1516-
* assert query == 'query q { company { taxAgencies(first: 5) { } } }'
1523+
1524+
# here is the result
1525+
* assert query == 'query q { company { taxAgencies(first: 5) { edges { node { id, name } } } } }'
1526+
15171527
Given request { query: '#(query)' }
1528+
And header Accept = 'application/json'
15181529
When method post
15191530
Then status 200
1531+
1532+
* def agencies = $.data.company.taxAgencies.edges
1533+
* match agencies[0].node == { id: '#uuid', name: 'John Smith' }
15201534
```
15211535
## Multi-line Comments
15221536
### How do I 'block-comment' multiple lines ?
@@ -1537,15 +1551,17 @@ interesting options when running tests in bulk. The most common use-case would
15371551
partition your tests into 'smoke', 'regression' and the like - which enables being
15381552
able to selectively execute a sub-set of tests.
15391553

1540-
Read more at the [Cucumber wiki](https://github.com/cucumber/cucumber/wiki/Tags).
1554+
The documentation on how to run tests via the [command line](#command-line) has an example of how to use tags
1555+
to decide which tests to *not* run (or ignore). The [Cucumber wiki](https://github.com/cucumber/cucumber/wiki/Tags)
1556+
has more information on tags.
15411557

15421558
## Dynamic Port Numbers
15431559
In situations where you start an (embedded) application server as part of the test set-up phase, a typical
15441560
challenge is that the HTTP port may be determined at run-time. So how can you get this value injected
15451561
into the Karate configuration ?
15461562

15471563
It so happens that the [`karate`](#the-karate-object) object has a field called `properties`
1548-
which can read a Java system-property by name: `properties[key]`. Since the `karate` object is injected
1564+
which can read a Java system-property by name like this: `properties['myName']`. Since the `karate` object is injected
15491565
within [`karate-config.js`](#configuration) on start-up, it is a simple and effective way for other
15501566
processes within the same JVM to pass configuration values into Karate at run-time.
15511567

karate-archetype/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<parent>
66
<groupId>com.intuit.karate</groupId>
77
<artifactId>karate-parent</artifactId>
8-
<version>0.1.4</version>
8+
<version>0.1.5</version>
99
</parent>
1010
<artifactId>karate-archetype</artifactId>
1111
<packaging>jar</packaging>

karate-archetype/src/main/resources/archetype-resources/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
<dependency>
1818
<groupId>com.intuit.karate</groupId>
1919
<artifactId>karate-core</artifactId>
20-
<version>0.1.4</version>
20+
<version>0.1.5</version>
2121
<scope>test</scope>
2222
</dependency>
2323
</dependencies>

karate-core/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<parent>
66
<groupId>com.intuit.karate</groupId>
77
<artifactId>karate-parent</artifactId>
8-
<version>0.1.4</version>
8+
<version>0.1.5</version>
99
</parent>
1010
<artifactId>karate-core</artifactId>
1111
<packaging>jar</packaging>

karate-core/src/main/java/com/intuit/karate/Script.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -641,10 +641,14 @@ public static void setValueByPath(String name, String path, String exp, ScriptCo
641641
} else if (isXmlPath(path)) {
642642
Document doc = context.vars.get(name, Document.class);
643643
ScriptValue sv = preEval(exp, context);
644-
if (sv.getType() != STRING) {
645-
throw new RuntimeException("TODO set non-string XML values");
646-
}
647-
XmlUtils.setByPath(doc, path, sv.getAsString());
644+
switch(sv.getType()) {
645+
case XML:
646+
Node node = sv.getValue(Node.class);
647+
XmlUtils.setByPath(doc, path, node);
648+
break;
649+
default:
650+
XmlUtils.setByPath(doc, path, sv.getAsString());
651+
}
648652
} else {
649653
throw new RuntimeException("unexpected path: " + path);
650654
}

karate-core/src/main/java/com/intuit/karate/XmlUtils.java

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import java.util.Map;
1010
import javax.xml.parsers.DocumentBuilder;
1111
import javax.xml.parsers.DocumentBuilderFactory;
12+
import javax.xml.transform.OutputKeys;
1213
import javax.xml.transform.Transformer;
1314
import javax.xml.transform.TransformerFactory;
1415
import javax.xml.transform.dom.DOMSource;
@@ -22,14 +23,15 @@
2223
import org.slf4j.Logger;
2324
import org.slf4j.LoggerFactory;
2425
import org.w3c.dom.Document;
26+
import org.w3c.dom.DocumentFragment;
2527
import org.w3c.dom.Node;
2628

2729
/**
2830
*
2931
* @author pthomas3
3032
*/
3133
public class XmlUtils {
32-
34+
3335
private static final Logger logger = LoggerFactory.getLogger(XmlUtils.class);
3436

3537
private XmlUtils() {
@@ -43,6 +45,7 @@ public static String toString(Node node) {
4345
TransformerFactory tf = TransformerFactory.newInstance();
4446
try {
4547
Transformer transformer = tf.newTransformer();
48+
transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
4649
transformer.transform(domSource, result);
4750
return writer.toString();
4851
} catch (Exception e) {
@@ -87,8 +90,8 @@ public static String getValueByPath(Node node, String path) {
8790
} catch (Exception e) {
8891
throw new RuntimeException(e);
8992
}
90-
}
91-
93+
}
94+
9295
public static void setByPath(Node doc, String path, String value) {
9396
Node node = getNodeByPath(doc, path);
9497
if (node.hasChildNodes() && node.getFirstChild().getNodeType() == Node.TEXT_NODE) {
@@ -98,12 +101,21 @@ public static void setByPath(Node doc, String path, String value) {
98101
}
99102
}
100103

104+
public static void setByPath(Document doc, String path, Node in) {
105+
Node node = getNodeByPath(doc, path);
106+
if (in.getNodeType() == Node.DOCUMENT_NODE) {
107+
in = in.getFirstChild();
108+
}
109+
Node newNode = doc.importNode(in, true);
110+
node.getParentNode().replaceChild(newNode, node);
111+
}
112+
101113
public static String toJsonString(Node node) {
102114
String xml = toString(node);
103-
JSONObject json = XML.toJSONObject(xml);
115+
JSONObject json = XML.toJSONObject(xml);
104116
return json.toString();
105-
}
106-
117+
}
118+
107119
public static Map<String, Object> toMap(Node node) {
108120
return toJsonDoc(node).read("$");
109121
}
@@ -112,6 +124,5 @@ public static DocumentContext toJsonDoc(Node node) {
112124
String json = toJsonString(node);
113125
return JsonPath.parse(json);
114126
}
115-
116127

117128
}

karate-core/src/test/java/com/intuit/karate/XmlUtilsTest.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,17 @@ public void testSetStringValueByPath() {
9494
Document doc = XmlUtils.toXmlDoc(xml);
9595
XmlUtils.setByPath(doc, "/foo/bar", "hello");
9696
String result = XmlUtils.toString(doc);
97-
assertTrue(result.contains("<foo><bar>hello</bar></foo>"));
97+
assertEquals(result, "<foo><bar>hello</bar></foo>");
9898
}
99+
100+
@Test
101+
public void testSetDomNodeByPath() {
102+
String xml = "<foo><bar>baz</bar></foo>";
103+
Document doc = XmlUtils.toXmlDoc(xml);
104+
Node temp = XmlUtils.toXmlDoc("<hello>world</hello>");
105+
XmlUtils.setByPath(doc, "/foo/bar", temp);
106+
String result = XmlUtils.toString(doc);
107+
assertEquals(result, "<foo><hello>world</hello></foo>");
108+
}
99109

100110
}

karate-core/src/test/java/com/intuit/karate/syntax/syntax.feature

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,11 @@ Then match cat / == <cat><name>Jean</name></cat>
7272
* set cat/cat/name = 'King'
7373
* match cat / == <cat><name>King</name></cat>
7474

75+
# set xml chunks
76+
* def xml = <foo><bar>baz</bar></foo>
77+
* set xml/foo/bar = <hello>world</hello>
78+
* match xml == <foo><hello>world</hello></foo>
79+
7580
# assign xpath expressions to variables
7681
# also note the multi-line option / syntax
7782
* def myXml =

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
<groupId>com.intuit.karate</groupId>
66
<artifactId>karate-parent</artifactId>
7-
<version>0.1.4</version>
7+
<version>0.1.5</version>
88
<packaging>pom</packaging>
99

1010
<name>${project.artifactId}</name>

0 commit comments

Comments
 (0)