How To Write Spec Tests In Deno

craigmorten

Craig Morten

Posted on June 23, 2020

How To Write Spec Tests In Deno

Deno has it's own built-in testing capability which makes it super simple to get started with testing - you don't need to import anything, you can just create a test file and get started with TDD:

// add.ts
export const add = (a: number, b: number) => {
  return a + b;
}

// add.test.ts
import { add } from "./add.ts";
import { assertEquals } from "https://deno.land/x/std@0.65.0/testing/asserts.ts";

Deno.test("it should add two numbers", () => {
  assertEquals(add(1, 2), 3);
});
Enter fullscreen mode Exit fullscreen mode

For simple tests this is perfect - it's lightweight and we can test our application easily using the deno test command, and get the following output:

$ deno test

running 1 tests
test it should add two numbers ... ok (13ms)

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out (6ms)
Enter fullscreen mode Exit fullscreen mode

What about non "hello world" projects?

As our application grows in complexity, our tests have to become more and more complex. With just Deno's built-in testing APIs we might start having to repeat code to setup and teardown things before and after every test.

This kind of requirement one of the reasons the likes of Mocha and Jest are so popular in Node - they allow us to write suites of tests with a set of useful hooks that allow us to define the setup and teardown in a single place which keeps our test code DRY and clean.

I've already covered how you can use Mocha for testing your Deno apps in a previous article, but here I am excited to share with you that the Drashland team have released a testing framework that is purpose built for Deno!

Introducing Rhum

Rhum is one of the latest projects being released by the Drashland team.

Rhum allows you to write descriptive tests, is simple and lightweight to use, adds all the standard hooks (e.g. beforeEach) that we are used to, and best of all, makes use of the Deno.test() API under-the-hood so it is fully compatible with the deno test CLI command.

All you need to get going is to import the Rhum module and start writing your tests!

Let's see how we would write our previous simple example using Rhum:

// add.ts
export const add = (a: number, b: number) => {
  return a + b;
}

// add.test.ts
import { add } from "./add.ts";
import { Rhum } from "https://deno.land/x/rhum@v1.1.2/mod.ts";

// Create a test plan for our `add.ts` file.
Rhum.testPlan("add.ts", () => {

  // Add a test suite for our `add()` function
  Rhum.testSuite("add()", () => {

    // Add a test case, this will be used in `Deno.test()` under-the-hood.
    Rhum.testCase("it should add two numbers", () => {
      const result = add(1, 2);

      Rhum.asserts.assertEquals(result, 3);
    });
  });
});

Rhum.run();
Enter fullscreen mode Exit fullscreen mode

And if we run the deno test command again we see our test being run:

$ deno test

running 1 tests

add.ts
    add()
        it should add two numbers ... ok (9ms)

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out (9ms)
Enter fullscreen mode Exit fullscreen mode

Notice how now we get all the useful context information from the testPlan name and the testSuite description.

Using hooks

Let's have a look at how we can make use of Rhum's test hooks to allow us to write several tests without having to repeat the test setup.

First let's create a simple Drash server that we can test:

// server.ts
import { Drash } from "https://deno.land/x/drash@v1.2.3/mod.ts";

/**
 * Setup our HomeResource for responding to requests
 * to the root path.
 */
class HomeResource extends Drash.Http.Resource {
  static paths = ["/"];
  public GET() {
    this.response.body = "Hello Deno!";

    return this.response;
  }
}

/**
 * Create our Drash HTTP Server.
 */
const server = new Drash.Http.Server({
  response_output: "text/html",
  resources: [HomeResource],
});

/**
 * Runs our Drash HTTP Server on http://localhost:3000
 */
export const runServer = async () => {
  await server.run({
    hostname: "localhost",
    port: 3000,
  });

  console.log("server listening at http://localhost:3000");
};

/**
 * Closes our Drash Server
 */
export const closeServer = () => {
  try {
    server.close();
  } catch (_) {}
};

// If we run this file directly then run the server.
if (import.meta.main) {
  runServer();
}
Enter fullscreen mode Exit fullscreen mode

Here we setup everything we need to run a simple Drash server on http://localhost:3000 that will respond to GET requests to the root path / with a body of "Hello Deno!".

Let's write some tests for our new server using a combination of Rhum and SuperDeno (a library for make testing HTTP servers easier!):

// server.test.ts
import { runServer, closeServer } from "./server.ts";
import { Rhum } from "https://deno.land/x/rhum@v1.1.2/mod.ts";
import {
  superdeno,
  Test,
} from "https://x.nest.land/superdeno@2.1.1/mod.ts";

Rhum.testPlan("server.ts", () => {
  Rhum.beforeAll(async () => {
    await runServer();
  });

  Rhum.afterAll(() => {
    closeServer();
  });

  Rhum.testSuite("when making a GET request to the root '/' path", () => {
    let result: Test;

    Rhum.beforeAll(() => {
      result = superdeno("http://localhost:3000").get("/");
    });

    Rhum.testCase(
      "it should respond with a HTTP 200 (OK) status code",
      async () => {
        await result.expect(200);
      },
    );

    Rhum.testCase(
      "it should respond with a 'html' like Content-Type",
      async () => {
        await result.expect("Content-Type", /html/);
      },
    );

    Rhum.testCase(
      "it should respond with 'Hello Deno!' as the body",
      async () => {
        await result.expect("Hello Deno!");
      },
    );
  });

  Rhum.testSuite(
    "when making a GET request to a path that hasn't been configured",
    () => {
      let result: Test;

      Rhum.beforeAll(() => {
        result = superdeno("http://localhost:3000").get("/does-not-exist");
      });

      Rhum.testCase(
        "it should respond with a HTTP 404 (Not Found) status code",
        async () => {
          await result.expect(404);
        },
      );

      Rhum.testCase(
        "it should respond with a 'html' like Content-Type",
        async () => {
          await result.expect("Content-Type", /html/);
        },
      );

      Rhum.testCase(
        "it should respond with 'Not Found' as the body",
        async () => {
          await result.expect("Not Found");
        },
      );
    },
  );
});

Rhum.run();
Enter fullscreen mode Exit fullscreen mode

So as before we set up our test plan for our server.ts file. We then make use of the beforeAll and afterAll test hooks to run and close our server before and after our tests.

We then define two test suites, one for testing our root / path and another to test the 404 behaviour of non-configured routes.

Again, for each suite we can make use of test hooks to perform our superdeno setup for making the requests which means we can keep our assertions in the test cases super short and focused.

Let's run deno test and see what happens!

$ deno test --allow-net

running 6 tests

server.ts
    when making a GET request to the root '/' path
        it should respond with a HTTP 200 (OK) status code ... ok (22ms)
        it should respond with a 'html' like Content-Type ... ok (8ms)
        it should respond with 'Hello Deno!' as the body ... ok (6ms)
    when making a GET request to a path that hasn't been configured                                                                   
        it should respond with a HTTP 404 (Not Found) status code ... ok (19ms)
        it should respond with a 'html' like Content-Type ... ok (10ms)
        it should respond with 'Not Found' as the body ... ok (14ms)

test result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out (79ms)
Enter fullscreen mode Exit fullscreen mode

That's awesome - we've been able to fully test our Drash server using the built-in Deno testing command, and our code is super simple and clean! 🎉


How are you getting on with testing your Deno applications? Doing anything different, or found another library that's made it simple for you? Let me know by dropping a comment in the comments below!

Catch you soon gang!

💖 💪 🙅 🚩
craigmorten
Craig Morten

Posted on June 23, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related