Unit Testing
Learning Objectives
- You know of unit testing and you can create unit tests with Deno.
Unit testing involves testing individual functions and components to verify that they work as expected. This is typically done by verifying that specific inputs produce expected outputs. Unit tests are often created with the help of a testing framework that provides functionality for defining and running tests.
First unit test
Deno has built-in functionality for defining and running tests, so for now, we do not need to install any additional testing frameworks. To get started, create a new folder with the file app_test.js
, and add the following content to the file.
import { assertEquals } from "jsr:@std/assert@1.0.8";
const fun = () => {
return "hello world!";
};
Deno.test("Fun should return 'hello world'", () => {
assertEquals(fun(), "hello world");
});
Run the command deno test
in the folder where you placed the file. The output should be similar to the following.
running 1 test from ./app_test.js
Fun should return 'hello world' ... FAILED (2ms)
ERRORS
Fun should return 'hello world' => ./app_test.js:7:6
error: AssertionError: Values are not equal.
[Diff] Actual / Expected
- hello world!
+ hello world
throw new AssertionError(message);
^
at assertEquals (https://jsr.io/@std/assert/1.0.8/equals.ts:51:9)
at file:///path/app_test.js:8:3
FAILURES
Fun should return 'hello world' => ./app_test.js:7:6
FAILED | 0 passed | 1 failed (3ms)
error: Test failed
From the output, we can see that the test failed. The test asserts that the string returned from the function fun
equals (is the same than) the string hello world
. However, the function fun
returns the string hello world!
, and the values are not equal. The output of the test shows the difference between the actual and expected values.
error: AssertionError: Values are not equal.
[Diff] Actual / Expected
- hello world!
+ hello world
Fix the issue by changing the return value of the function fun
to hello world
.
import { assertEquals } from "jsr:@std/assert@1.0.8";
const fun = () => {
return "hello world";
};
Deno.test("Fun should return 'hello world'", () => {
assertEquals(fun(), "hello world");
});
Now, when the command deno test
is run again, no errors are reported.
running 1 test from ./app_test.js
Fun should return 'hello world' ... ok (0ms)
ok | 1 passed | 0 failed (1ms)
Writing unit tests
Individual tests are defined with Deno’s Deno.test function, which is given a test name and a function that contains the test code. In unit testing, the test code calls specific functions of the tested components and checks that the outputs are as expected.
Deno’s standard library contains assertions, which are used to check whether the outputs of the functions match the expected values, verifying the expected functionality of the components. As an example, the assertEquals function is used to check whether an output from a function or a component matches an expected value. Similarly, the assertNotEquals function would be used to check that the output does not match an expected value.
The assertions are imported from the standard library
jsr:@std/assert@1.0.8
.
In the following, we define two unit tests for a simple function that adds two numbers.
import { assertEquals, assertNotEquals } from "jsr:@std/assert@1.0.8";
const sum = (a, b) => {
return a + b;
};
Deno.test("3 + 5 = 8", () => {
assertEquals(sum(3, 5), 8);
});
Deno.test("3 + 5 != 9", () => {
assertNotEquals(sum(3, 5), 9);
});
Running the tests using deno test
, we see that both tests pass.
running 2 tests from ./app_test.js
3 + 5 = 8 ... ok (0ms)
3 + 5 != 9 ... ok (0ms)
ok | 2 passed | 0 failed (2ms)
When the deno test
command is run, Deno identifies test files based on file names. If the name of a file ends with _test.js
, Deno assumes that the file contains tests.
Following the convention, we do not have to provide information to Deno on which tests should be run. Instead, we can just run the deno test
command and Deno finds the test files for us.
Separating tests from application logic
The previous examples demonstrate the core idea of unit testing: we have a function that we want to test, and we write a test that verifies that the function works as expected.
Unit tests are typically written in separate files from the application logic. Let’s separate the above functionality into two parts, a file called app.js
and a file called app_test.js
.
The file app.js
defines the function sum
and exports it for others to use. This is done as follows.
const sum = (a, b) => {
return a + b;
};
export { sum };
Now, app_test.js
needs to import the function sum
from the file app.js
for testing. Like before, the app_test.js
imports assertions from Deno’s test library, and has the tests that are used to verify whether the function works as expected. After extracting the function sum
from the file, and importing it from app.js
, the file app_test.js
looks as follows.
import { assertEquals, assertNotEquals } from "jsr:@std/assert@1.0.8";
import { sum } from "./app.js";
Deno.test("3 + 5 = 8", () => {
assertEquals(sum(3, 5), 8);
});
Deno.test("3 + 5 != 9", () => {
assertNotEquals(sum(3, 5), 9);
});
Now, when we run the tests, we again see that they pass.
running 2 tests from ./app_test.js
3 + 5 = 8 ... ok (0ms)
3 + 5 != 9 ... ok (0ms)
ok | 2 passed | 0 failed (1ms)
Separate folders for tests
When the application grows, it is common to separate the tests into a separate folder. This separation prevents tests from being mixed with application logic, making the codebase easier to maintain. As an example, we could place the application source code into a folder called src
and the tests into a folder called tests
.
With this, the structure would be as follows.
tree --dirsfirst
.
├── src
│ └── app.js
└── tests
└── app_test.js
And, the tests would have to import the function sum
from the file in the src
folder. The following example demonstrates this.
import { assertEquals, assertNotEquals } from "jsr:@std/assert@1.0.8";
import { sum } from "../src/app.js";
Deno.test("3 + 5 = 8", () => {
assertEquals(sum(3, 5), 8);
});
Deno.test("3 + 5 != 9", () => {
assertNotEquals(sum(3, 5), 9);
});
Now, the tests would be run from the project root directory (the folder that contains the folders src
and tests
), and the tests would be found from the tests
folder.
deno test
running 2 tests from ./tests/app_test.js
3 + 5 = 8 ... ok (0ms)
3 + 5 != 9 ... ok (0ms)
ok | 2 passed | 0 failed (2ms)
Naming of test files
Let’s look at another example. The following code defines a service that could be used to store and retrieve a hello message.
let message = "Hello service world!";
const getHello = () => {
return message;
};
const setHello = (newMessage) => {
message = newMessage;
};
export { getHello, setHello };
Assume that the above code is stored within a file helloService.js
, within a services
folder under src
. Now, if we wish to write tests for the helloService.js
file, we would create a file called helloService_test.js
that would reside in a folder called services
under tests
. The file structure would be as follows.
tree --dirsfirst
.
├── src
| └── services
│ └── helloService.js
└── tests
└── services
└── helloService_test.js
In the above case, the file helloService_test.js
would import the functions from the helloService.js
using a relative path. The following example tests that the function getHello
returns the message Hello service world!
.
import { assertEquals } from "jsr:@std/assert@1.0.8";
import { getHello } from "../../src/services/helloService.js";
Deno.test("Function getHello returns 'Hello service world!'", () => {
assertEquals(getHello(), "Hello service world!");
});
When we run the test, we notice that the test passes.
deno test
running 1 test from ./tests/services/helloService_test.js
Function getHello returns 'Hello service world!' ... ok (0ms)
ok | 1 passed | 0 failed (1ms)
Let’s add another test for the helloService.js
file. In the following test, we test both the getHello
and the setHello
functions. We first set a hello message, which we then retrieve.
import { assertEquals } from "jsr:@std/assert@1.0.8";
import { getHello, setHello } from "../../src/services/helloService.js";
Deno.test("Function getHello returns 'Hello service world!'", () => {
assertEquals(getHello(), "Hello service world!");
});
Deno.test("Message set using setHello returned from getHello", () => {
setHello("hello world!");
assertEquals(getHello(), "hello world!");
});
Again, when we run the tests, we observe that everything works as expected.
running 2 tests from ./tests/services/helloService_test.js
Function getHello returns 'Hello service world!' ... ok (0ms)
Message set using setHello returned from getHello ... ok (0ms)
ok | 2 passed | 0 failed (1ms)
The above output shows that two tests were run and that both tests passed.
What to test
When writing unit tests, the focus is on testing individual functions and components. Tests should cover the default behavior of the function as well as edge cases.
Edge cases are scenarios where the function behaves differently from its default behavior. For example, if a function is expected to return a value when the input is a number, one possible edge case could be having the input as a string. Edge cases are often where the function is most likely to fail.
In this chapter, we focused on unit testing of functions and components in Deno. When working with Svelte, testing differs somewhat — for a starter, see the Svelte documentation on testing at https://svelte.dev/docs/svelte/testing.