Tuesday, January 19, 2010

Cucumber, Webrat and XML

I've written before about using cucumber and soap4r to test SOAP web services. That still works, but it's annoying when you're trying to test a SOAP web service you're writing. In order to test a web service you're writing with soap4r, you have to pretend its remote and run it outside your standard test environment. So in order to run tests that way, I had to:
  • start my server
  • put the right data into the database my server is pointing at
  • reconcile the data in the server database with my test data
  • run the tests
  • re-reconcile the data in the server database to clean it up
I wound up spending a lot of time on maintenance and tracking down issues that really didn't have anything to do with the software. Plus, the setup requiremented certainly limited the incentive to run the tests. A typical run would look like this:
  • run tests. (FAIL)
  • Whoops. Forgot. Start the server.
  • run tests. (FAIL)
  • whoops. I didn't clean out from the other manual testing I was doing on that server so my database has unexpected data in it. Clean out database.
  • run tests (actually find something useful or interesting)
  • etc...
Step back a moment, and let's look at what we're testing. We're exercising the code, yes. We're also testing our system configuration (Is the web server properly configured for ssl? Does it run on the expected ports?). This is useful in some scenarios, but when it's just my development server, testing the system configuration seems less than ideal. I'm really more interested in testing the code at this point.

So why are we doing this? Long story short, I blame one key limitation in soap4r, and that is that soap4r expects to make API calls to a full domain name. You have to call "http://localhost/webservice/api/MyCall", and you cannot call "webservice/api/MyCall". This makes it really hard to run in the standard cucumber test environment. It's fine as long as you're consuming a truly remote web service (e.g., a Google API or something), but it's rough when you're trying to test your own web service.
But there is another way. You can simply skip the problem component (soap4r in this case). Instead, I simply use cucumber and webrat's existing infrastructure and feed it my self-formed SOAP requests. For example, this is how I send a call to our Activation web service:
When /^"([^\"]*)" activates a widget with description "([^\"]*)" with password "([^\"]*)"$/ do |email, description, password|
cert_key = "-----BEGIN CERTIFICATE-----
MIIDGDCCAwwCADADBgEAMAAwHhcNMTAwMTE4MDUxMDM0WhcNMTAwMTE4MDYxMDM0
WjCBujELMAkGA1UEBhMCVVMxFjAUBgNVBAgMDU1hc3NhY2h1c2V0dHMxDzANBgNV
BAcMBkJvc3RvbjETMBEGA1UECgwKQWJha2FzIExMQzEtMCsGA1UEAwwkNzhCQUJG
MzgtOEEzOC00MjAwLTlBN0ItNjIwQjQ3MTlGNDkzMT4wPAYJKoZIhvcNAQkBFi83
OEJBQkYzOC04QTM4LTQyMDAtOUE3Qi02MjBCNDcxOUY0OTNAYWJha2FzLmNvbTCC
AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALFCwTbWA95OwK1fKFLycsgG
PuawxjKtkYaiSbSSwMgpU8qqqN4pvv4aPzqLJiZweGMnGFZA7zOePUgu1AHBIKy2
cfrOyxpyrxNUy8mK8RnQKdLkJkmLPBIj5i+w2QoTNs/v+hRW2XLvrzntkVO6U7VJ
NrvKTTfck7rR9v1+9ctrDR8+h0qo/N6fy1k+2jsiEb6VcuC8KjNqVaqragiQe7LT
pbwqeaaYKkH6kNmzNX7tRItaPfyn5G3eE+LSjrpyyzOImWRD/PeA1ZGx8HwYI5rG
gz9Dvs5XGwwyjgwJLCYSEoDnZ0F3o/lhDUjrKjwrgJW0uEQwfwLzCL5APneNpPn5
5owXUCzHe+U6xhO4josYPG7Qx5eDHnuxLWCFuxqvIVSisTRNrW9IYHGifRoNeDEy
qSRlCZB35SMXMAW+b3iVd8uz9HO2UELGnirmPTXY1SV7+1TvAbJJG64ISXCrw/dq
v5mU0nLp1AxX7hv2jjq302t7mVHvA+b+D1I8O13HEANkL3ebVuMotCQ52AgAHTDK
8BfCS+lp3Cdm/778dDRq6UAk/ZLIKb+6/m33yYS9j1IS3Xri5qroIC5rMU6IGtLz
XIOy9DA0CRiOSev8c/g83VpOnhCrhkfmKUivvBchEMvlqOFZa1TQqbq/z5KxeCBl
sS1nbjn/2AyCrVcFQFqTAgMBAAEwAwYBAAMBAA==
-----END CERTIFICATE-----
"
body = <<-body
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
env:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
#{description}
#{cert_key}
body
post "/csh/api", body, {"HTTP_AUTHORIZATION" => basic_auth(email, password), "HTTP_SOAPACTION" => "soap/csh/api/Activation"}
end
There are a few key things to notice here:
  • The second argument to the post call contains the SOAP message itself (in XML form, not in any object form)
  • The third argument to the post call is where all the headers go. For a SOAP message, you have to set HTTP_SOAPACTION.
  • I'm using basic authentication here, so I had to set that in the headers as well. You could use cert-based auth by setting "SSL_CLIENT_CERT" in the headers instead.
  • Note that I've munged the example a bit so I didn't have to include all the code that constructs the XML, but I'll leave that as an exercise to the reader.
There is a place in this world for soap4r and the like, but for someone developing their own web service, this other way might be useful.

1 comment: