Run assertions against echoed HTTP requests with echo.js

Intro

Say you are writing an HTTP client and you are provided with documentation detailing the endpoints supported by a REST API hosted somewhere. One way to test this client is to hit the server serving the API. For whatever reason, you might not want to do this. You might just want to test how your client’s HTTP requests look like. This post will guide you through doing just that with the help of echo.js.

testautonation-echo.js

echo.js

Echo.js is an HTTP server which simply takes any request it receives and sends it back as text/plain with a 200 status code.

You can install it as a dev dependency via npm with

npm i -D echo.js

The HTTP client

Our HTTP client code uses superagent and exports the getPizza and addPizza functions we want to test:

The _init function defines the common setup we will use for all calls against the pizza resource. We want all requests to have sensible defaults, but we also want everything to be overwritable. The tests we will be writing soon should help demonstrate this. They will spell out most of the details in the above code. After all, this kind of code lends itself well to being documented via unit tests.

The unit tests

We can set up our tests as follows using mocha (of course, mocha is not a requirement. You should be able to translate the following tests to the framework of your choice).

We are simply starting the echo.js server before any of our tests run, waiting for the server to be ready for connections (by passing done as the callback function), and shutting down the server when all our tests are over.

So… how will our request look like when we call getPizzas()? We can simply log out the response we get from our echo server. The property we are interested in is text:

In mocha, it.only is simply a way of only running the test in question. This gives us a handy way of inspecting the response we are caching in str without seeing the output from any other test we might have already written. You can run the tests with npm t in the repo for this post (after installing dependencies with npm i). Doing so should output the following:

GET /api/v1/pizzas HTTP/1.1
Host: localhost:8061
Accept-Encoding: gzip, deflate
User-Agent: node-superagent/3.8.2
Accept: application/json
Content-Type: application/json
Connection: close

… which finally gives us something to assert:

Rinse and repeat:

Which gives us the following handy overview of our client and it has supported functionality when we run our tests:

Our HTTP client supports:
    getPizzas()
      ✓ Should have GET /api/v1/pizzas
      ✓ Should have an Accept header of application/json
    getPizzas({ basePath: '/api/v2' })
      ✓ Should have GET /api/v2/pizzas
      ✓ Should have an Accept header of application/json
    getPizzas({ headers: { 'Accept': 'application/xml' } })
      ✓ Should have GET /api/v1/pizzas
      ✓ Should have an Accept header of application/xml
    addPizza()
      ✓ Throws Error when missing name
    addPizza({ name: 'margherita' })
      ✓ Should have POST /api/v1/pizzas
      ✓ Should have an X-API-Key header of foobar
      ✓ Should have a Content-Type header of application/json
      ✓ Should have a body of {"name": "margherita"}
    addPizza({ name: 'margherita', headers: { 'Content-Type': 'application/x-www-form-urlencoded' } })
      ✓ Should have POST /api/v1/pizzas
      ✓ Should have an X-API-Key header of foobar
      ✓ Should have a Content-Type header of application/x-www-form-urlencoded
      ✓ Should have a body of name=margherita
    addPizza({ name: 'margherita' }).send({ name: 'capricciosa' })
      ✓ Should have POST /api/v1/pizzas
      ✓ Should have an X-API-Key header of foobar
      ✓ Should have a Content-Type header of application/json
      ✓ Should have a body of {"name": "capricciosa"}


  19 passing (65ms)

In the following call: addPizza({ name: ‘margherita’ }).send({ name: ‘capricciosa’ }), note how we’re able to modify the request after calling one of our client’s API functions. This is because a superagent request isn’t triggered until .end() or (in our case) .then().

Although we have not (as testing usually goes) covered all possible input/output scenarios in our tests, this sort of setup is not half bad. It gives us tests we can run when we change our client code as well as a convenient way of exploration. As we did before, we can simply use t.only and log out what we are interested in.

You can find my GitHub repo here.

P.S. This was my first blog post as a guest author for TestAutonation. If you are interested in doing the same, contact the team here with your great ideas.

Justin Calleja

Front-end Developer

Leave a Comment