Typescript, Unit tests and AWS Lambdas – Part 1

I admit it. I like it.

I’ve taken a keen liking to developing with Typescript. Especially combining it with AWS Lambda functions. I don’t tend to jump on the newest development fad bandwagon, but my job took me down this path. And I’m glad for it. I still won’t give up Java™ as my favorite language to work with. But I’ll stop talking about that before I get stoned. Or before you get bored and stop reading.

I have run into the same frustrations I’ve heard from many other developers starting with AWS: the documentation Amazon offers is atrocious. It’s hard to find what you’re looking for. And when you think you’ve found it…you find it lacking good examples. Or it takes you down a rabbit-hole of link clicking. Heck, even Stack Overflow is lacking helpful answers!

So, that is why I’m writing this—to provide the community with real-world examples which I hope are seriously helpful. Without further ado, today’s topic: Portable HTTP(S) request mocking in unit tests.

HTTP(S) Request Mocking (except kinda for real)

Alright, so I lied a little. This first article has nothing to do with AWS lambdas. There will be some in the future though. I promise. But this does touch on a real issue with unit testing HTTP endpoints, their responses, and their failures. Unit tests can be made ridiculously simple with a built-in Node.js module: “http.” Or “https” if you care about that. I’ll focus on “http” for this post.

Here are some wonderful things about the method I’m about to teach you:

  • It keeps the testing local. No need to setup physical or virtual endpoints.
  • It is portable. As long as your test environment runs node, it will work (if it didn’t run node, your tests wouldn’t run anyhow).
  • It is really, really simple.
  • It is a real HTTP(S) server which responds however you need it to.
  • As long as you set it to listen only locally, it’s sheltered from the outside world.

Assumptions

I’m going to assume you know how to setup a Typescript (or even just Node.js) project. There is plenty of documentation on that. I’ll also assume you know how to use npm to install packages.
And one more bigger assumption: you are familiar with using Chai for unit testing.

The Setup

You only need 4 libraries to get this example up and running.

The Strategy

A simple HTTP server will be created with strategic routes to simulate different responses and errors. This server will be available during the lifetime of the test suite (spoiler alert: it goes in the “before” method). I like solid examples. So here it is:

import 'mocha';
import {expect} from 'chai';
import * as http from 'http';
import {Client} from "node-rest-client";

describe('Some HTTP Tests', () => {
    // A suite-level variable for the http server.
    let server;
    // Port the server will listen on
    const PORT = 32153;

    before('Environment setup', () => {

        // Set up the server and routes.
        server = http.createServer((request, response) => {
            // A simple case statement handles the routing. You can get as fancy
            // as you like. For this example, we don't care what kind of request
            // it is.
            switch (request.url) {
                // A nice 200 response.
                case "/MyHappyPath":
                    response.writeHead(200,
                        {"Content-Type": "application/json"});
                    response.write('{"result":"happy"}');
                    response.end();
                    break;
                // Build in a 2-second response delay using setTimeout. This is
                // useful for testing timeouts.
                case "/2SecondDelay":
                    setTimeout(() => {
                        response.writeHead(200,
                            {"Content-Type": "application/json"});
                        response.write('{"result":"2 seconds later."}');
                        response.end();
                    }, 2000);
                    break;
                // Well, you can't win 'em all. Here's a 500 response for you.
                case "/InconsiderateServer":
                    setTimeout(() => {
                        response.writeHead(500,
                            {"Content-Type": "application/json"});
                        response.write('{"result":"The server doesn\'t care."}');
                        response.end();
                    }, 5000);
                    break;
                // Gotta have the 404 for the invalid paths.
                default:
                    response.writeHead(404,
                        {"Content-Type": "application/json"});
                    response.write('{"result":"failed!"}');
                    response.end();
            }

        }).listen(PORT, '127.0.0.1');
    });

    after(() => {
        // This cleanup is VERY important. Your test will never end if you don't
        // close the server.
        server.close(() => {
            console.log("Server closed.");
        });
    });    
});

Some Tests

That setup is all well and good, but it’s useless without some tests. Obviously you will need to apply the knowledge to your own architecture. But these should, at least, give you you a firm foundation to build upon. I’m not going to post the entire script; just the tests. If your curious where they go, they go within the “describe” block in the script above.

Yes, they are trivial examples. I’ll add some more real-world type stuff at some point in a later tutorial. Gotta build on knowledge, right? Besides, I have to leave some kind of cliffhanger so you come back for more!

Vanilla: The 200 response.

This is a simple “get” test on the happy path to 200.

it('Checks for a 200 response', (done) => {
  let client = new Client();
   
  // A simple get request
  client.get(`http://127.0.0.1:${PORT}/HappyPath`, (data, response) => {          
      expect(response.statusCode).to.equal(200);
      done();
  });
});

Timeout Test

it('Tests a request timeout', (done) => {
    let client = new Client();

    // A small bit of configuration for the request
    let args = {
        data: '{"some": "data"}',
        headers: {"header1": "the header"},
        requestConfig: {
            timeout: 100 // Set a request timeout of 100 ms
        },
        responseConfig: {
            timeout: 100 // Set a response timeout of 100 ms
        }
    };

    client.get(`http://127.0.0.1:${PORT}/2SecondDelay`,
        (data, response) => {
            // Test failure!
            done(new Error("Uh-oh, it didn't time out."));
        })
        .on('requestTimeout', (req) => {
            req.abort(); // An important bit here. STOP THE REQUEST!
            done(); // This is the happy end of the test.
        })
        .on('responseTimeout', (res) => {
            done();
        })
        .on('error', (err) => {
            console.log(err);
            done();
        });
}).timeout(3000);

Okay, the end.

Well, the end of Part 1. Hopefully you find this useful. I don’t know what Part 2 is going to be yet. But it will build on this, so stay tuned.