Unit Testing
Learning objectives
- You know what unit tests are and you know how to write unit tests.
Unit testing refers to testing individual functions and components to verify that they work as expected, i.e. with given inputs, they produce the expected outputs.
Unit tests with Deno
Deno provides functionality for defining and running tests. Individual tests can be defined the Deno.test
function, which is given a test name and a function that contains the test code. In unit testing, the test code invokes 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 the functionality of a program. As an example, the assertEquals
function is used to check whether an output from a function or a component matches an expected value.
The program below outlines the basics of a test. The program has a function fun
that is being tested, and a test that checks that the output of the function is as expected. The function assertEquals
is used for concretely verifying that the output is as expected.
import { assertEquals } from "https://deno.land/std@0.222.1/testing/asserts.ts";
const fun = () => {
return "hello world!";
};
Deno.test("Fun should return 'hello world'", () => {
assertEquals(fun(), "hello world");
});
Save the above code to a file called app_test.js
and run the command deno test
in the folder where you placed the code. The output is as follows (on the first line, we list the files in the folder, while on the third line, we run the tests).
ls
app_test.js
deno test
running 1 test from ./app_test.js
Fun should return 'hello world' ... FAILED (3ms)
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://deno.land/std@0.222.1/assert/assert_equals.ts:53:9)
at file:///path-to-folder/app_test.js:8:3
FAILURES
Fun should return 'hello world' => ./app_test.js:7:6
FAILED | 0 passed | 1 failed (29ms)
error: Test failed
When we run the test, the test output shows that the test failed. This, in effect, means that the assertion -- assertEquals
-- failed, indicating that the functionality did not work as expected. Let's look at a part of the output in more detail.
Fun should return 'hello world' => ./app_test.js:7:6
error: AssertionError: Values are not equal.
[Diff] Actual / Expected
- hello world!
+ hello world
The above output indicates that the test called Fun should return 'hello world'
failed. There is an assertion error, showing that the compared values were not equal. The output provided by the tested functionality (in this case, the function fun
) was hello world!
, while the test expected that the output should be hello world
.
Let's fix the issue by changing the return value of the function fun
to hello world
.
import { assertEquals } from "https://deno.land/std@0.222.1/testing/asserts.ts";
const fun = () => {
return "hello world";
}
Deno.test("Fun should return 'hello world'", () => {
assertEquals(fun(), "hello world");
});
Now, when we run the tests again, we do not see any errors.
deno test
running 1 test from ./app_test.js
Fun should return 'hello world' ... ok (1ms)
ok | 1 passed | 0 failed (105ms)
How to identify files with tests?
By default, Deno identifies test files based on the file names. If the name of a file ends with test.js
, Deno assumes that the file contains tests. If we use the naming convention, we do not have to the file for which the tests should be run. Instead, we can just run the test
command and Deno finds the test files for us.
It is, however, possible to also explicitly pass test files to the command -- try it out by running the command deno test app_test.js
in the folder that contains the file app_test.js
.
Separating tests and application logic
The previous example demonstrates the core idea of unit testing: we define a function that we want to test, and we write a test that verifies that the function works as expected.
In practice, unit tests are not created to the same files that contain the application logic, but into separate files. 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 fun
and exports it for others to use. This is done as follows.
const fun = () => {
return "hello world!";
};
export { fun };
Now, the file app_test.js
should import the function fun
from the file app.js
for testing. Like before, the app_test.js
also imports the used assertions from Deno's test library, and contains the actual tests that are used to verify whether the application works as expected. After extracting the function fun
from the file, and importing it from app.js
, the file app_test.js
looks as follows.
import { assertEquals } from "https://deno.land/std@0.222.1/testing/asserts.ts";
import { fun } from "./app.js";
Deno.test("Fun should return 'hello world'", () => {
assertEquals(fun(), "hello world!");
});
Now, when we run the tests, we run the tests on the file app_test.js
. As shown below, the tests pass.
deno test
running 1 test from ./app_test.js
Fun should return 'hello world' ... ok (1ms)
ok | 1 passed | 0 failed (27ms)
Multiple tests
Let's look at another example. The following piece of code contains 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 };
Let's assume that the above code is stored within a file called helloService.js
, which resides in a folder called services
. If we write tests for a particular file, we use the same filename as the original, but add a suffix _test
to the name. For example, the tests for the file helloService.js
would be written to a file called helloService_test.js
. Further, if we would have a larger application, the tests would typically reside in a folder called tests
, matching the folder structure of the application.
That is, if the file helloService.js
would be within a folder called services
, then the corresponding helloService_test.js
would be within a folder called services
that would be placed within the folder tests
. The file structure would be as follows.
tree --dirsfirst
.
├── services
│ └── helloService.js
└── tests
└── services
└── helloService_test.js
3 directories, 2 files
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 "https://deno.land/std@0.222.1/testing/asserts.ts";
import { getHello } from "../../services/helloService.js";
Deno.test("Function getHello returns 'Hello service world!'", () => {
assertEquals(getHello(), "Hello service world!");
});
When tests are written into files that end with _test.js
, Deno can look for the tests. This means that we do not have to explicitly define the file for which the tests should be run. Instead, we can just run the test
command and Deno finds the test files for us. The following example first demonstrates that we are in the root directory of the application and then runs the tests within that directory.
tree --dirsfirst
.
├── services
│ └── helloService.js
└── tests
└── services
└── helloService_test.js
3 directories, 2 files
deno test
running 1 test from ./tests/services/helloService_test.js
Function getHello returns 'Hello service world!' ... ok (1ms)
ok | 1 passed | 0 failed (84ms)
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 "https://deno.land/std@0.222.1/testing/asserts.ts";
import { getHello, setHello } from "../../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.
deno test
running 2 tests from ./tests/services/helloService_test.js
Function getHello returns 'Hello service world!' ... ok (1ms)
Message set using setHello returned from getHello ... ok (0ms)
ok | 2 passed | 0 failed (93ms)
The above output shows that two tests were run and that both tests passed.