What SOAP is, and a first call
Envelopes, operations and one URL for everything, in SOAP 1.1 and 1.2, with curl and Java.
SOAP is a way for programs to call each other over a network by exchanging XML messages. A client sends a message that says "run this operation with these values", and the server sends back a message with the result or an error. It was standardized by the W3C in the early 2000s, became the backbone of enterprise integration, and still runs a great deal of banking, insurance, telecoms, government and travel software. Newer APIs mostly use REST and JSON, but the SOAP services already in place aren't going anywhere, and someone has to test them.
SOAP compared with REST
If you know REST, the biggest adjustment is that SOAP doesn't use HTTP the same way. REST spreads meaning across the URL, the method and the status code. SOAP puts nearly everything in the XML message and uses HTTP mainly to carry it:
| REST | SOAP | |
|---|---|---|
| URLs | One per resource: /orders/42 | Usually one per service: /api/soap/calculator |
| What to do | The HTTP method: GET, POST, PUT, DELETE | An operation named in the body: Add, GetCountryInfo |
| HTTP method | Varies with the action | POST for every call |
| Format | Usually JSON, sometimes XML | Always XML, in a fixed envelope |
| Errors | Status codes, plus a body in the API's own format | A standard Fault element (lesson 5) |
| Contract | Optional, often OpenAPI | A WSDL, which tools can turn into client code (lesson 4) |
Neither is better in general. SOAP's strict contract and standard error format suit systems where many organizations integrate and the rules must be exact. REST's simplicity suits the web. As a tester you test whichever you have, and the skills overlap more than the table suggests.
The envelope
Every SOAP message has the same structure:
<soap:Envelope xmlns:soap="..."> ← wraps the whole message
<soap:Header> ← optional: information about the message, such as
... a security token or a transaction ID
</soap:Header>
<soap:Body> ← required: the message itself
<Add xmlns="http://tempuri.org/"> ← the operation to run
<intA>5</intA> ← its parameters
<intB>3</intB>
</Add>
</soap:Body>
</soap:Envelope>Envelopeis the root element. Its namespace says which version of SOAP the message uses (lesson 2 explained namespaces).Headeris optional. It carries information aboutthe message rather than the request itself: security tokens, transaction IDs, routing details. The calls in this tutorial don't need one.Bodyholds the request or the response. In the common document/literal wrapped style, the Body contains one element named after the operation, and its children are the parameters. The response comes back asOperationResponsecontainingOperationResult, a naming habit the WSDL spells out (lesson 4).
A first call
This API's calculator service has four operations: Add, Subtract, Multiply and Divide, each taking two integers, intA and intB. With curl:
$ curl -i -X POST https://api.testingapis.com/api/soap/calculator \
-H "Content-Type: text/xml; charset=utf-8" \
-H 'SOAPAction: "http://tempuri.org/Add"' \
-d '<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Body><Add xmlns="http://tempuri.org/"><intA>5</intA><intB>3</intB></Add></soap:Body></soap:Envelope>'
HTTP/1.1 200 OK
content-type: text/xml; charset=utf-8
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<AddResponse xmlns="http://tempuri.org/">
<AddResult>8</AddResult>
</AddResponse>
</soap:Body>
</soap:Envelope>A POST to the service's single URL, with an XML body and two headers. Content-Type: text/xml says this is SOAP 1.1, and SOAPAction names the operation, which lets servers and proxies route a request without reading the body. The answer is in AddResponse/AddResult: 8. Send something other than XML and the server refuses before reading anything:
$ curl -i -X POST https://api.testingapis.com/api/soap/calculator -H "Content-Type: application/json" -d '{"intA":5}'
HTTP/1.1 415 Unsupported Media Type
{"status":415,"error":"Unsupported Media Type","message":"SOAP requests must use Content-Type text/xml (SOAP 1.1) or application/soap+xml (SOAP 1.2)."}SOAP 1.1 and 1.2
Two versions are in use, and this service accepts both. The same call in SOAP 1.2:
$ curl -i -X POST https://api.testingapis.com/api/soap/calculator \
-H "Content-Type: application/soap+xml; charset=utf-8" \
-d '<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope"><soap:Body><Add xmlns="http://tempuri.org/"><intA>5</intA><intB>3</intB></Add></soap:Body></soap:Envelope>'
HTTP/1.1 200 OK
content-type: application/soap+xml; charset=utf-8
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope">
<soap:Body>
<AddResponse xmlns="http://tempuri.org/">
<AddResult>8</AddResult>
</AddResponse>
</soap:Body>
</soap:Envelope>Same operation, same answer. Three things changed:
| SOAP 1.1 | SOAP 1.2 | |
|---|---|---|
| Envelope namespace | http://schemas.xmlsoap.org/soap/envelope/ | http://www.w3.org/2003/05/soap-envelope |
| Content-Type | text/xml | application/soap+xml |
| The action | A SOAPAction header | An action parameter on the Content-Type, optional |
| Faults | faultcode, faultstring; always HTTP 500 | Code, Reason; 400 or 500 depending on the cause (lesson 5) |
The envelope namespace and the Content-Type have to agree. A 1.2 envelope sent as text/xml is an error, which lesson 5 shows. The server answers in whichever version you used, so a test for each is a quick way to check a service supports both.
A service that returns a record
The second service on this API, at /api/soap/countriessign in to see, returns structured data. GetCountryInfo takes a two-letter country code:
$ curl -i -X POST https://api.testingapis.com/api/soap/countries \
-H "Content-Type: text/xml; charset=utf-8" \
-H 'SOAPAction: "http://example.com/countries/GetCountryInfo"' \
-d '<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Body><GetCountryInfo xmlns="http://example.com/countries"><countryCode>IN</countryCode></GetCountryInfo></soap:Body></soap:Envelope>'
HTTP/1.1 200 OK
content-type: text/xml; charset=utf-8
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<GetCountryInfoResponse xmlns="http://example.com/countries">
<Country>
<Code>IN</Code>
<Name>India</Name>
<Capital>New Delhi</Capital>
<Currency>INR</Currency>
<PhoneCode>+91</PhoneCode>
<Continent>Asia</Continent>
<Population>1428627663</Population>
</Country>
</GetCountryInfoResponse>
</soap:Body>
</soap:Envelope>Notice the new namespace, http://example.com/countries. Each service has its own, and requests must use it. ListCountries takes no parameters and returns all ten countries; lesson 6 queries it with XPath.
The same calls in Java
The project wraps the repetitive parts in a small helper. Soap.envelope builds the envelope for either version, and Soap.call sends it with the right headers:
package com.testingapis.soap;
import java.util.HashMap;
import java.util.Map;
import org.w3c.dom.Document;
/** Building SOAP envelopes and sending them, in either version of SOAP. */
public final class Soap {
public static final String SOAP11 = "http://schemas.xmlsoap.org/soap/envelope/";
public static final String SOAP12 = "http://www.w3.org/2003/05/soap-envelope";
/** The two versions differ in the envelope namespace, the Content-Type and where the action goes. */
public enum Version {
SOAP_1_1(SOAP11, "text/xml; charset=utf-8"),
SOAP_1_2(SOAP12, "application/soap+xml; charset=utf-8");
public final String namespace;
public final String contentType;
Version(String namespace, String contentType) {
this.namespace = namespace;
this.contentType = contentType;
}
}
/** A SOAP fault. In both versions the code is a qualified name such as "soap:Server". */
public record Fault(String code, String reason) {
/** The code without its prefix: "Server", "Sender", "VersionMismatch". The prefix can be anything. */
public String codeName() {
return code.substring(code.indexOf(':') + 1);
}
}
private Soap() {}
/** The fault in a response body, or null if it isn't one. Reads both versions' layouts. */
public static Fault fault(Document doc) {
XmlQuery q = new XmlQuery(Map.of("s11", SOAP11, "s12", SOAP12));
// SOAP 1.1: faultcode and faultstring are plain elements with no namespace.
if (q.bool(doc, "boolean(/s11:Envelope/s11:Body/s11:Fault)")) {
return new Fault(q.string(doc, "/s11:Envelope/s11:Body/s11:Fault/faultcode"),
q.string(doc, "/s11:Envelope/s11:Body/s11:Fault/faultstring"));
}
// SOAP 1.2: everything is in the envelope namespace, and the code sits inside Code/Value.
if (q.bool(doc, "boolean(/s12:Envelope/s12:Body/s12:Fault)")) {
return new Fault(q.string(doc, "/s12:Envelope/s12:Body/s12:Fault/s12:Code/s12:Value"),
q.string(doc, "/s12:Envelope/s12:Body/s12:Fault/s12:Reason/s12:Text"));
}
return null;
}
/** Wraps one operation element, such as {@code <Add>...</Add>}, in an envelope. */
public static String envelope(Version version, String operation) {
return "<soap:Envelope xmlns:soap=\"" + version.namespace + "\">"
+ "<soap:Body>" + operation + "</soap:Body>"
+ "</soap:Envelope>";
}
/**
* POSTs an envelope. SOAP 1.1 puts the action in a SOAPAction header; SOAP 1.2 puts it in an action
* parameter of the Content-Type. Pass null for no action.
*/
public static Api.Response call(String path, Version version, String action, String envelope) {
Map<String, String> headers = new HashMap<>();
if (version == Version.SOAP_1_1) {
headers.put("Content-Type", version.contentType);
if (action != null) headers.put("SOAPAction", "\"" + action + "\"");
} else {
headers.put("Content-Type", action == null ? version.contentType : version.contentType + "; action=\"" + action + "\"");
}
return Api.post(path, headers, envelope);
}
}Version holds the two things that change between versions: the namespace and the content type. call puts the action where each version expects it. fault is used from lesson 5 onward. With that, a test reads much like the curl command:
@Test
@DisplayName("Add 5 and 3 over SOAP 1.1")
void addOverSoap11() {
String request = Soap.envelope(SOAP_1_1, "<Add xmlns=\"" + CALC + "\"><intA>5</intA><intB>3</intB></Add>");
Api.Response response = Soap.call("/api/soap/calculator", SOAP_1_1, CALC + "Add", request);
assertEquals(200, response.status());
assertTrue(response.contentType().startsWith("text/xml"));
XmlQuery q = new XmlQuery(Map.of("soap", Soap.SOAP11, "t", CALC));
assertEquals("8", q.string(response.xml(), "/soap:Envelope/soap:Body/t:AddResponse/t:AddResult"));
}The XPath at the end follows the response down from the envelope to AddResult, with prefixes bound to the SOAP and calculator namespaces. The SOAP 1.2 test is the same with SOAP_1_2, and checks for application/soap+xml instead. And GetCountryInfo:
@Test
@DisplayName("a service that returns a record: GetCountryInfo")
void getCountryInfo() {
String request = Soap.envelope(SOAP_1_1, "<GetCountryInfo xmlns=\"" + COUNTRIES + "\"><countryCode>IN</countryCode></GetCountryInfo>");
Document doc = Soap.call("/api/soap/countries", SOAP_1_1, null, request).xml();
XmlQuery q = new XmlQuery(Map.of("c", COUNTRIES));
assertEquals("India", q.string(doc, "//c:Country/c:Name"));
assertEquals("New Delhi", q.string(doc, "//c:Country/c:Capital"));
assertEquals("INR", q.string(doc, "//c:Country/c:Currency"));
}If you prefer a graphical tool, any HTTP client that can send a raw body works: set the method to POST, add the two headers and paste the envelope. The SOAP API guide lists the endpoints and sample envelopes.
Check your understanding
1.Which HTTP method does a SOAP call use, and why doesn't it change between operations?Show answer
POST, always. The operation is named inside the XML body, so HTTP only carries the message; it doesn't say what to do.
2.What are the three parts of a SOAP envelope, and which is optional?Show answer
Envelope, Header and Body. The Header is optional.
3.How does the server know whether a request is SOAP 1.1 or 1.2?Show answer
From the Content-Type, text/xml or application/soap+xml, which must match the envelope's namespace.
4.In SOAP 1.1, where does the action go?Show answer
In a SOAPAction header, as a quoted URI such as "http://tempuri.org/Add".
5.An AddResponse element has no prefix but xmlns="http://tempuri.org/". Which namespace must your test use to find AddResult?Show answer
http://tempuri.org/, bound to a prefix of your choice in the XPath query.
Exercise
Send a Multiply request with curl, first in SOAP 1.1 with the right SOAPAction, then in SOAP 1.2. Then write a Java test for ListCountries that checks the response has ten Country elements, using getElementsByTagNameNS with the countries namespace. The usage page at GET /api/soap/countriessign in to see lists the valid codes and a sample envelope.