How To Write Spec Tests In Deno
Tags: deno, typescript, testing, tutorial
Published: 2020-06-23 14:56:32

[Deno](https://deno.land) has it's own [built-in testing capability](https://deno.land/manual/testing) 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:

```ts
// 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);
});
```

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:

```console
$ 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)
```

# 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](https://mochajs.org/) and [Jest](https://jestjs.io/) 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](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself) and clean.

I've already covered [how you can use Mocha for testing your Deno apps in a previous article](https://dev.to/craigmorten/testing-your-deno-apps-with-mocha-4f35), 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](https://github.com/drashland/rhum/) is one of the latest projects being released by the [Drashland team](https://github.com/drashland).

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`:

```ts
// 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();
```

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

```console
$ 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)
```

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](https://github.com/drashland/deno-drash) that we can test:

```ts
// 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();
}
```

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](https://github.com/asos-craigmorten/superdeno) (a library for make testing HTTP servers easier!):

```ts
// 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();
```

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!

```console
$ 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)
```

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! :tada:

---

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!