Writing Our First Test in Deno
Explore how to write your first unit and integration tests in Deno using the built-in test APIs and assertion methods. Understand testing concepts, including isolating business logic with mocks and verifying the behavior of your application components. This lesson helps you ensure code reliability and maintainability while working with Deno applications.
We'll cover the following...
Before we start writing our test, it’s important to remember a few things. The most important of them is, why are we testing?
There might be multiple answers to this question, but most of them will gesture toward guaranteeing that the code is working. We might also say that utilizing them provides flexibility when it comes to refactoring or that valuing short feedback cycles during implementation is highly beneficial—we can agree with both of these perspectives. Since we didn’t write a test before implementing these features, the latter doesn’t apply too much to us.
We’ll keep these objectives in mind throughout this chapter. In this lesson, we’ll write our first test. We’ll use the application we wrote in the previous chapters and add tests to it. We’ll write two types of tests: integration and unit tests.
Integration tests will test how different components of the application interact. Unit tests test layers in isolation. If we think of it as a spectrum, unit tests are closer to the code, while integration tests are closer to the user. On the very end of the user side, there are also end-to-end tests. Those are the tests that test the application by emulating the user behavior, which we won’t cover in this chapter.
Parts of the patterns we used when developing the actual application, such as dependency injection and inversion of control, are of great use when it comes to testing. Since we developed our code by injecting all its dependencies, now, it’s just a matter of mocking those dependencies on tests. Remember: code that is easy to test is normally easy to maintain.
The first thing we’ll do is write tests for the business logic. Currently, since our API is quite simple, it doesn’t have much business logic. Most of it is living on UserController since MuseumController is very simple. We’ll start with the latter.
To write tests in Deno, we’ll need to use the following:
- The Deno test runner
- The
testmethod from theDenonamespace - The assertion methods from the Deno standard library
These are all part of Deno, distributed and maintained by the core team. There are many other libraries that can be used in tests that we can find in the community. We’ll use what’s provided by default in Deno since it works just fine and allows us to write clear and readable tests.
Let’s go and learn how we can define a test!
Defining a test
Deno provides an API to define tests. This API, Deno.test, provides two different ways to define a test.
One of them is the one we showed previously and consists of calling it with two arguments, that is, the test name and a test function. This can be seen in the following example:
Deno.test("my first test", () => {})
The other way we can do this is by calling the same API, this time sending an object as an argument. We can send the function and the name of the test, plus a few other options, to this object, as we can see in the following example:
These flags’ behaviors are very well-explained in the documentation, but here’s a summary:
Line 4: only runs only the tests that have this set to true and makes the test suite fail, so this should only be used as a temporary measure.
Line 5: sanitizeOps makes the test fail if all the operations that started on Deno’s core are not successful. This flag is true by default.
Line 6: sanitizeResources makes the test fail if there are still resources running after the test finishes (this can indicate memory leaks). This flag makes sure tests have to have a teardown phase where resources are stopped, and it’s true by default.
Now that we know about the APIs, let’s go write our first test—a unit test for the MuseumController function.
A unit test for MuseumController
In this lesson, we’ll be writing a very simple test that will cover only the functionality we wrote in MuseumController and no more.
It lists all the museums in the application, though it’s currently not doing much and is only working as a proxy for MuseumRepository. We can create the test file and logic for this simple functionality by following these steps:
- Create the
src/museums/controller.test.tsfile. The test runner will automatically consider files that have.testin their name as test files, among other conventions, as explained previously. - Declare the first test with the help of
Deno.test:
- Now, export the assertion methods from the standard library under a namespace named
tso that we can then use them on the test files, by adding the following tosrc/deps.ts:
To know what assertion methods are available in the standard library, check out this section of Deno’s documentation.
- We can now use the assertion methods from the standard library to write a test that instantiates
MuseumControllerand calls thegetAllmethod:
Note how we’re instantiating MuseumController and sending in a mocked version of museumRepository, which returns a static array. This is how we’re sure we’re testing only the logic inside MuseumController and nothing more. Closer to the end of the snippet, we’re making sure the getAll method’s result is returning the museum being returned by the mocked repository. We are doing this by using the assertion methods we exported from the dependencies file.
- Let’s run the test and verify that it’s working:
deno test --unstable --allow-env --allow-read --allow-write --allow-net src/museums
This shows the following:
Run the widget below to see the code in action.
const headers = new Headers()
headers.set("content-type", "application/json")
export function getClient(config) {
let token = null
return {
register: ({ username, password }) => {
return fetch(`${config.baseURL}/api/users/register`, {
body: JSON.stringify({ username, password }),
method: "POST",
headers
}).then(r => r.json())
},
login: ({ username, password }) => {
return fetch(`${config.baseURL}/api/login`, {
body: JSON.stringify({ username, password }),
method: "POST",
headers
})
.then(response => {
if (response.status < 300) {
return response.json()
}
throw response.json()
})
.then(response => {
token = response.token
return response
})
},
getMuseums: () => {
const authenticatedHeaders = new Headers()
authenticatedHeaders.set("authorization", `Bearer ${token}`)
return fetch(`${config.baseURL}/api/museums`, {
headers: authenticatedHeaders
}).then(r => r.json())
}
}
}
And our first test works!
Note how the test’s output lists the name of the test, its status, and the time it took to run, together with a summary of the test run.
The logic inside MuseumController is quite simple, so this was also a very simple test. However, it isolated the controller’s behavior, allowing us to write a very focused test.