Mocking SOAP web services with Sandbox

A while ago I started this product called BomRastreio with my good friend Porcelani. The promise was simple: to track objects sent through the Brazilian postal service and try predicting delays. In order to do this, periodically, we used to consume a SOAP web service from Correios and apply business logic based on the statuses - which are a lot - returned to all the related objects. Although Correios provides a relatively decent service, testing the application would require it to be present in the pipeline and we couldn't rely on it.

Now, the situation described it's a perfect scenario to place a Test Double. The interesting aspect here is being able to replace a SOAP web service so we can test our application reliably.

WSDL/SOAP

Although the WSDL from the Correios service isn't big, I'll omit it for brevity. Within the WSDL we can find two operations the buscaEventos and buscaEventosLista, which allow us to query information for objects sent through the postal service using the S10 (UPU standard)). Here is an example of a request message we can use to search for one object using the operation buscaEventos:

<soapenv:Envelope
    xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" 
    xmlns:res="http://resource.webservice.correios.com.br/">
  <soapenv:Header/>
  <soapenv:Body>
    <res:buscaEventos>
      <usuario>ECT</usuario>
      <senha>SRO</senha>
      <tipo>L</tipo>
      <resultado>T</resultado>
      <lingua>101</lingua>
      <objetos>AA598971235BR</objetos>
    </res:buscaEventos>
  </soapenv:Body>
</soapenv:Envelope>

Once we ran the query, here what we get as a result if the object is present:

<soapenv:Envelope
    xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
  <soapenv:Body>
    <ns2:buscaEventosResponse
        xmlns:ns2="http://resource.webservice.correios.com.br/">
      <return>
        <versao>2.0</versao>
        <qtd>1</qtd>
        <objeto>
          <numero>AA598971235BR</numero>
          <sigla>JF</sigla>
          <nome>NOME DO OBJETO</nome>
          <categoria>CATEGORIA DO OBJETO</categoria>
          <evento>
            <tipo>BDE</tipo>
            <status>23</status>
            <data>13/09/2018</data>
            <hora>10:58</hora>
            <descricao>Objeto devolvido ao remetente</descricao>
            <detalhe/>
            <local>CTCE MACEIO</local>
            <codigo>57060971</codigo>
            <cidade>MACEIO</cidade>
            <uf>AL</uf>
          </evento>
        </objeto>
      </return>
    </ns2:buscaEventosResponse>
  </soapenv:Body>
</soapenv:Envelope>

Otherwise, we'll get the following response if the object is absent:

<soapenv:Envelope 
    xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
  <soapenv:Header/>
  <soapenv:Body>
    <ns2:buscaEventosResponse 
        xmlns:ns2="http://resource.webservice.correios.com.br/">
      <return>
        <versao>2.0</versao>
        <qtd>1</qtd>
        <objeto>
          <numero>AA000000000BR</numero>
          <erro>Objeto não encontrado na base de dados dos Correios.</erro>
        </objeto>
      </return>
    </ns2:buscaEventosResponse>
  </soapenv:Body>
</soapenv:Envelope>

As you can see, the request and response messages are simple to replicate. In this case, we'll create a fake object for two different scenarios: one when the object is present; another when the object is absent.

Sandbox

Sandbox is a very interesting tool that we can have at our disposal. It allows us to create fake objects and provide the response we need to test our application. Now, we can run Sandbox in the cloud or locally. For our test case will run it locally. In order to get started with it, first, it's necessary to have Java 8 update 72 or later installed in our system. Second, we need to download the Sandbox binary here and unzipped it. Once we have done it, we can run Sandbox as follows:

Unix and Linux

./sandbox run

Windows

java -jar sandbox run

Now, to make these example a bit clearer, I have created a folder where I put the Sandbox binary and other files required to make this work. Here is the folder structure of this example:

Sandbox
├── requests
│   ├── absent-object.xml
│   └── present-object.xml
├── templates
│   ├── absent-object.liquid
│   ├── present-object.liquid
├── main.js
└── sandbox

As you can see in the structure above, what is required by Sandbox is that we have a main.js file where we'll put our mocking script. Also, a templates folder must be created when using Shopify liquid templates to render responses. For this test case, we'll check for two scenarios: 1) when the object AA000000000BR doesn't exist and; 2) when any other object id exists. Both scenarios are using SOAP messages and for this particular matter, Sandbox provides us with the ability to handle SOAP actions independently, as follows:

main.js
Sandbox.soap("/service/rastro", "buscaEventos", function(request, response) {
  var objectId = request.xmlDoc.get("//objetos").text();

  if (objectId === 'AA000000000BR') {
    response.render('absent-object', {
      objectId: objectId
    });
  } else {
    response.render('present-object', {
      objectId: objectId,
      events: [
        {
          type: 'BDE',
          status: 23,
          date: '13/09/2018',
          time: '10:58',
          description: 'Objeto devolvido ao remetente', 
          locality: 'CTCE MACEIO',
          code: '57060971',
          city: 'MACEIO',
          state: 'AL'
        }
      ]
    });
  }
});

As we can notice in the script above is that Sandbox provides us the XML parsed so we can apply xpath expressions to it. In this example, we are only looking for the object ID so we can hydrate the templates accordingly. We do hydrate the templates by calling response.render('absent-object', {}) with the name of the template file we can found in the folder templates together with the JSON object that will be used within it. Here is how the templates look like:

absent-object.liquid
<soapenv:Envelope 
    xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
  <soapenv:Header/>
  <soapenv:Body>
    <ns2:buscaEventosResponse 
        xmlns:ns2="http://resource.webservice.correios.com.br/">
      <return>
        <versao>2.0</versao>
        <qtd>1</qtd>
        <objeto>
          <numero>{{data.objectId}}</numero>
          <erro>Objeto não encontrado na base de dados dos Correios.</erro>
        </objeto>
      </return>
    </ns2:buscaEventosResponse>
  </soapenv:Body>
</soapenv:Envelope>
present-object.liquid
<soapenv:Envelope 
    xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
  <soapenv:Body>
    <ns2:buscaEventosResponse 
        xmlns:ns2="http://resource.webservice.correios.com.br/">
      <return>
        <versao>2.0</versao>
        <qtd>1</qtd>
        <objeto>
          <numero>{{data.objectId}}</numero>
          <sigla>JF</sigla>
          <nome>NOME DO OBJETO</nome>
          <categoria>CATEGORIA DO OBJETO</categoria>
          {% for item in data.events %}
          <evento>
            <tipo>{{item.type}}</tipo>
            <status>{{item.status}}</status>
            <data>{{item.date}}</data>
            <hora>{{item.time}}</hora>
            <descricao>{{item.description}}</descricao>
            <detalhe/>
            <local>{{item.locality}}</local>
            <codigo>{{item.code}}</codigo>
            <cidade>{{item.city}}</cidade>
            <uf>{{item.state}}</uf>
          </evento>
          {% endfor %}
        </objeto>
      </return>
    </ns2:buscaEventosResponse>
  </soapenv:Body>
</soapenv:Envelope>

Now, when Sandbox receives the request, it will check the proper scenario, hydrate the template and render the response. In order to test it, as you can see in the folder structure, there are two request files with the same content except the content of the tag objects:

<soapenv:Envelope 
    xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" 
    xmlns:res="http://resource.webservice.correios.com.br/">
  <soapenv:Header/>
  <soapenv:Body>
    <res:buscaEventos>
      <usuario>ECT</usuario>
      <senha>SRO</senha>
      <tipo>L</tipo>
      <resultado>T</resultado>
      <lingua>101</lingua>
      <objetos>AA000000000BR</objetos>
    </res:buscaEventos>
  </soapenv:Body>
</soapenv:Envelope>

With the request message in hands, we can call our service as we would do with the Correios service. The only difference is that now we have control over the scenarios that we can test. You can test by using the following command:

curl -X POST \
  -d @requests/absent-object.xml \
  -H "SOAPAction:buscaEventos" \
  -H "Content-Type:text/xml" \
  http://localhost:8080/service/rastro

Alright, we just put our Test Double to work and he provided us with a good quality fake object so we can test our application without relying on a service that may or may not be available or doesn't provide the speed required for our test.

See you!