Deno by example: Content server — Part 3 Unit testing

Mayank Choubey
Tech Tonic
Published in
9 min readDec 2, 2021

--

Welcome to the medium course:

Learn Deno by example — Content server.

This is a medium course that takes the user from writing, testing, formatting, logging, documenting, and deploying an application in Deno. This rapid course consists of six sections.

Here are the course contents:

In section 1, we’ve gone through the content server application along with some manual tests. In section 2, we’ve used Deno’s built-in formatter and linter to improvise on the content server application. In this section, we’ll learn about writing unit tests to automatically test the smallest units in the application. We’re progressively enhancing the application. It’s advisable to read sections 1 & 2 before proceeding with this section. Let’s get started!

Unit testing

The purpose of unit testing is to perform automated testing on the smallest possible units of the application. This is very different from the manual testing that we’ve done in the previous two sections. The difference is:

  • In manual testing, the application’s HTTP server was started that processed requests and sent responses back
  • In unit testing, there is no HTTP server as we intentionally want to test module level functions (exports)

Therefore, for unit testing, we need to simulate the function inputs which could be standard inputs like the Request object or the Headers object, or some other data. Based on the type of function, we need to build and supply that kind of input. This is much more work than simply issuing a curl request, but this is way more useful than performing black box testing of an application.

Generally, unit testing is performed using some special libraries like chai, mocha, etc. With Deno, nothing additional is required. Like formatter and linter, Deno also comes with a built-in feature rich test runner. We’ll be writing all the unit tests using Deno’s test() API.

Basics

We’ve already covered unit testing in depth in articles here, here, here, & here. Furthermore, we’ll just go through a short summary.

There are two parts of a unit test:

  • Unit testing function: This is the function that’s given as input to the Deno.test() API. This function will carry out the unit test and convey the results back.
Deno.test('test1', () => {
//check the function under test
});
//or,Deno.test('test2', async () => {
//check the function under test
});
  • Asserts: The unit testing function contains a number of asserts that check the output of the function under test. If all the asserts pass, the overall test is considered as successful. If any of the asserts fail, it raises an exception (AssertionError) that marks the test as failed.
Deno.test('test1', () => {
//check the function under test
//use asserts to check the output
assert(someVal==someOtherVal);
assertEquals(o1, o2);
//and so on
});

That’s all about the basics. A detailed knowledge of Deno’s unit testing can be obtained from the links given above.

Functions to unit test

First, we need to decide the functions we want to unit test. The simple rule of thumb is to test all the exports. Following the thumb rule, the content server application has the following functions to test:

  • main.ts: Not in scope as this doesn’t export anything
  • router.ts: route() function
  • controller.ts: handleRequest() function
  • authService.ts: authorize() function
  • fileService.ts: getContent() function

That’s all to test. Let’s have a look at the unique inputs.

Inputs

Based on the functions under the scope of the unit testing, there are three types of inputs:

  • Request: The standard Request object (route, handleRequest)
  • Headers: The standard Headers object (authorize)
  • String: The relative path of the file (getContent)

Outputs

Just like inputs, the functions produce an output that needs to be analyzed for correctness of the working of the function. Here are the different type of outputs that needs to be analyzed:

  • Response: The standard Response object (route, handleRequest)
  • Boolean: True if authorized, false otherwise (authorize)
  • Length: The length of the file
  • Stream: The stream containing the file contents

Unit tests

We’ll write unit tests for functions in the following order (going from small unit to bigger unit):

  • authorize()
  • getContent()
  • handleRequest()
  • route()

All the unit tests will be present in a directory test/unit:

$ ls -R
authService.ts controller.ts main.ts test
consts.ts fileService.ts router.ts cfg.json
./test:
unit
./test/unit:
data/

AuthService UT

The authorize() function takes Headers object as input and returns true, if authorized, false otherwise. It’s a quite simple function to test. Let’s write granular unit test cases.

To test most of the code present in the authorize function, we’ll enable authorization through a file containing api keys (apiKeys.json). The apiKeys.json file is part of the unit testing, therefore a dummy file will be present in the same directory as the test cases

$ cat test/unit/data/apiKeys.json 
[
"cba633d4-59f3-42a5-af00-b7430c3a65d8"
]

There are at least 5 unit tests that can check most of the parts of the authorize function. The only output to check is true or false. Here are the unit tests:

To run the tests, deno test command need to be used along with required permissions. In unit test mode, there is no need for network permissions as we’re not going to start an HTTP server. The general test command is:

$ deno test <required permissions like read, etc.> <directory containing tests>

We’ll use the default config present in cfg.json file that sets API_KEYS_PATH that would be used by authService module to get the path to the dummy keys file:

$ cat cfg.json
{
"apiKeysPath": "./test/unit/data/apiKeys.json",
"servePath": "./test/unit/"
}

That is all. Let’s run all the tests present in test/unit directory (we’re assuming that CI/CD would execute tests wrt path of the application’s root directory):

$ deno test --allow-read=./ test/unit 
running 5 tests from file:///Users/mayankc/Work/source/deno-course-content-server/test/unit/authService_test.ts
test No headers ... ok (10ms)
test Empty authorization header ... ok (9ms)
test Authorization header without key ... ok (8ms)
test wrong api key = fc485dd4-6237-42c3-aad8-a4eeef058239 ... ok (8ms)
test Correct api key = cba633d4-59f3-42a5-af00-b7430c3a65d8 ... ok (7ms)
test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out (60ms)

All the tests have passed! Let’s move on to the next one, file service.

FileService UT

The file service module’s getContent() function takes relative path as input and returns the length of the file & the file stream. Just like authorize(), we need to create some dummy data files that would be read during unit testing. The dummy data files would be created in the same directory: test/unit.

For unit testing, we’ll create two sample data files:

$ ls -l test/unit/data/
-rw-r--r-- 1 mayankc staff 69273 Nov 26 12:19 pdfFile.pdf
-rw-r--r-- 1 mayankc staff 22 Nov 26 12:18 textFile.txt

In each unit test, we’ll be checking for two things:

  • len: len should match the size of the file that was read
  • stream: The stream should be read in a loop and the total bytes present in the stream should match the size of the file that was read

We’ll write a utility function called readStream() that would loop through the stream and returns the total number of bytes present in the stream.

There are 4 unit tests that can check the basic functionality of getContent() function. Of course, they can be extended to different file sizes, but we’ll keep it to 4 only. Here are the unit tests:

We’ll use the default config present in cfg.json file that sets SERVE_PATH that would be used by fileService module to get the base path:

$ cat cfg.json
{
"apiKeysPath": "./test/unit/data/apiKeys.json",
"servePath": "./test/unit/"
}

That is all. Let’s run all the tests present in test/unit directory (we’re assuming that CI/CD would execute tests wrt path of the application’s root directory):

$ deno test --allow-read=./ test/unit 
running 5 tests from file:///Users/mayankc/Work/source/deno-course-content-server/test/unit/authService_test.ts
test No headers ... ok (12ms)
test Empty authorization header ... ok (9ms)
test Authorization header without key ... ok (8ms)
test wrong api key = fc485dd4-6237-42c3-aad8-a4eeef058239 ... ok (7ms)
test Correct api key = cba633d4-59f3-42a5-af00-b7430c3a65d8 ... ok (7ms)
running 4 tests from file:///Users/mayankc/Work/source/deno-course-content-server/test/unit/fileService_test.ts
test Inexistent file ... ok (8ms)
test Directory ... ok (8ms)
test Text file textFile.txt ... ok (8ms)
test PDF file pdfFile.txt ... ok (8ms)
test result: ok. 9 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out (46ms)

All 9 tests have passed (also includes authService tests). Let’s move on to the next one, controller.

Controller UT

The controller’s handleRequest() function takes a Request object as input and produces a Response object. The dummy data files and apiKeys.json are already there, so we don’t have to prepare any data.

In each unit test, we’ll be checking the following:

  • HTTP Status code
  • Content length header
  • Content type header
  • Length of actual data

While calling the function under test, we need to prepare and supply the Request object. For this particular use case, the only input for building the request object is the URL, as the URL also contains the path of the requested file.

There are 7 unit tests that can check the basic functionality of handleRequest() function. Of course, they can be extended to different file types & sizes, but we’ll keep it to 7 only. Here are the unit tests:

We’ll use the default config present in cfg.json file that sets SERVE_PATH that would be used by fileService module to get the base path:

$ cat cfg.json
{
"apiKeysPath": "./test/unit/data/apiKeys.json",
"servePath": "./test/unit/"
}

That is all. Let’s run all the tests present in test/unit directory (we’re assuming that CI/CD would execute tests wrt path of the application’s root directory):

$ deno test --allow-read=./ test/unit
running 5 tests from file:///Users/mayankc/Work/source/deno-course-content-server/test/unit/authService_test.ts
test No headers ... ok (11ms)
test Empty authorization header ... ok (9ms)
test Authorization header without key ... ok (8ms)
test wrong api key = fc485dd4-6237-42c3-aad8-a4eeef058239 ... ok (7ms)
test Correct api key = cba633d4-59f3-42a5-af00-b7430c3a65d8 ... ok (7ms)
running 7 tests from file:///Users/mayankc/Work/source/deno-course-content-server/test/unit/controller_test.ts
test No Authorization header ... ok (8ms)
test Incomplete Authorization header ... ok (9ms)
test Invalid key in Authorization header ... ok (8ms)
test No file path ... ok (8ms)
test Directory path ... ok (9ms)
test Text file ... ok (14ms)
test PDF file ... ok (11ms)
running 4 tests from file:///Users/mayankc/Work/source/deno-course-content-server/test/unit/fileService_test.ts
test Inexistent file ... ok (10ms)
test Directory ... ok (8ms)
test Text file textFile.txt ... ok (8ms)
test PDF file pdfFile.txt ... ok (9ms)
test result: ok. 16 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out (198ms)

All 16 tests have passed (also includes authService & fileService). Let’s move on to the last one, router.

Router

The unit testing of route() function is very simple because, only for GET, the router forwards request to the controller. After that it’s the same handleRequest() that we’ve already tested. For completeness, we’ll still write one of the basic positive controller related test case to router test cases.

All the basic ideas are the same as controller, so we’ll jump to the 3 unit test cases for router:

That is all. Let’s run all the tests present in test/unit directory (we’re assuming that CI/CD would execute tests wrt path of the application’s root directory):

$ deno test --allow-read=./ test/unit
running 5 tests from file:///Users/mayankc/Work/source/deno-course-content-server/test/unit/authService_test.ts
test No headers ... ok (11ms)
test Empty authorization header ... ok (9ms)
test Authorization header without key ... ok (8ms)
test wrong api key = fc485dd4-6237-42c3-aad8-a4eeef058239 ... ok (7ms)
test Correct api key = cba633d4-59f3-42a5-af00-b7430c3a65d8 ... ok (7ms)
running 7 tests from file:///Users/mayankc/Work/source/deno-course-content-server/test/unit/controller_test.ts
test No Authorization header ... ok (10ms)
test Incomplete Authorization header ... ok (8ms)
test Invalid key in Authorization header ... ok (9ms)
test No file path ... ok (8ms)
test Directory path ... ok (9ms)
test Text file ... ok (15ms)
test PDF file ... ok (11ms)
running 4 tests from file:///Users/mayankc/Work/source/deno-course-content-server/test/unit/fileService_test.ts
test Inexistent file ... ok (8ms)
test Directory ... ok (8ms)
test Text file textFile.txt ... ok (8ms)
test PDF file pdfFile.txt ... ok (10ms)
running 3 tests from file:///Users/mayankc/Work/source/deno-course-content-server/test/unit/router_test.ts
test POST ... ok (9ms)
test DELETE ... ok (7ms)
test Text file ... ok (11ms)
test result: ok. 19 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out (243ms)

All 19 tests have passed (also includes authService, fileService, and controller).

That’s all about the unit tests. We’ve written basic unit tests for all the public APIs.

We’re done with the third section of the course.

Here is the map of this rapid medium course:

In the next section, we’ll write integration & performance tests to validate the application as a black box.

--

--

Mayank Choubey
Tech Tonic

I write about Deno, Bun, and Node.js. My new book: https://choubey.gitbook.io/learn-react-in-a-day/