SOAP Service Testing with Python¶
Testing SOAP/XML web services using requests (raw XML) and zeep (WSDL-aware client). SOAP is still common in enterprise, banking, and government integrations.
SOAP Basics for Testers¶
SOAP = XML envelope with Header + Body. WSDL = service contract describing endpoints, operations, and message types.
<!-- SOAP Request -->
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:usr="http://example.com/users">
<soapenv:Header/>
<soapenv:Body>
<usr:GetUser>
<usr:userId>123</usr:userId>
</usr:GetUser>
</soapenv:Body>
</soapenv:Envelope>
Testing with requests (Raw XML)¶
import requests
from lxml import etree
def test_get_user_soap():
url = "http://example.com/soap/UserService"
headers = {
"Content-Type": "text/xml; charset=utf-8",
"SOAPAction": "GetUser",
}
body = """<?xml version="1.0" encoding="utf-8"?>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:usr="http://example.com/users">
<soapenv:Body>
<usr:GetUser>
<usr:userId>123</usr:userId>
</usr:GetUser>
</soapenv:Body>
</soapenv:Envelope>"""
response = requests.post(url, data=body, headers=headers)
assert response.status_code == 200
root = etree.fromstring(response.content)
ns = {"usr": "http://example.com/users"}
name = root.find(".//usr:name", ns)
assert name is not None
assert name.text == "John Doe"
Testing with zeep (WSDL Client)¶
from zeep import Client
@pytest.fixture(scope="session")
def soap_client():
return Client("http://example.com/soap/UserService?wsdl")
def test_get_user_zeep(soap_client):
result = soap_client.service.GetUser(userId="123")
assert result.name == "John Doe"
assert result.email is not None
Zeep auto-generates Python objects from WSDL - no manual XML construction.
Parsing XML Responses¶
from lxml import etree
def parse_soap_response(response_bytes, namespace_map):
root = etree.fromstring(response_bytes)
body = root.find(".//{http://schemas.xmlsoap.org/soap/envelope/}Body")
return body
def extract_values(body, xpath, namespaces):
elements = body.findall(xpath, namespaces)
return [el.text for el in elements]
SOAP Fault Handling¶
def test_invalid_user_returns_fault():
response = send_soap_request(user_id="nonexistent")
root = etree.fromstring(response.content)
fault = root.find(".//{http://schemas.xmlsoap.org/soap/envelope/}Fault")
assert fault is not None
faultcode = fault.find("faultcode").text
faultstring = fault.find("faultstring").text
assert "Client" in faultcode
assert "not found" in faultstring.lower()
SOAP with Authentication¶
from requests import Session
from zeep.transports import Transport
session = Session()
session.auth = ("username", "password") # Basic auth
# or
session.headers.update({"Authorization": "Bearer token123"})
transport = Transport(session=session)
client = Client("http://example.com/service?wsdl", transport=transport)
For WS-Security (WSSE):
from zeep.wsse.username import UsernameToken
wsse = UsernameToken("user", "pass")
client = Client(wsdl_url, wsse=wsse)
Gotchas¶
-
Issue:
SOAPActionheader missing or wrong value causes 500 errors with unhelpful messages. Fix: Check WSDL<soap:operation soapAction="...">for exact action string. Some services require emptySOAPAction: "". -
Issue: Namespace mismatch - XPath returns None even though element exists in response. Fix: Always inspect raw response XML first (
print(response.text)). Register correct namespace prefixes in your XPath queries. -
Issue: Zeep caches WSDL at import time - if service changes, tests use stale contract. Fix: Disable cache for CI:
Client(url, transport=Transport(cache=None)).