Additional Notes on Testing
Learning objectives
- Knows how to have a separate test configuration for running tests.
- Knows what mocking is.
Test configuration
When running tests for an application, it is often necessary to use configurations that are specific to the tests. For example, testing an application with a database should be done using a database for testing, as otherwise there is the risk of using the production database for tests.
In practice, we would need a way to determine whether the application is currently launched for testing or not. A simple way for doing this would be the use of environmental variables, which are demonstrated next. The following application accesses environmental variables. If a variable called TEST_ENVIRONMENT
is found, the application will output the message TEST ENVIRONMENT
. Otherwise, the application will output the message NOT TEST ENVIRONMENT
.
const test = Deno.env.get("TEST_ENVIRONMENT");
let message = "NOT TEST ENVIRONMENT";
if (test) {
message = "TEST ENVIRONMENT";
}
console.log(message);
Now, when we run the application as is (--allow-env
) flag is needed for accessing environmental variables), we see the following output.
deno run --allow-env app.js
NOT TEST ENVIRONMENT
Setting the environmental variable on the command line is straightforward. In the following example, the same program is launched. Now, however, at launch, an environmental variable is set.
TEST_ENVIRONMENT=true deno run --allow-env app.js
TEST ENVIRONMENT
In practice, the same approach could be used to define database credentials. The following is an example of a database configuration, where the variables hostname
, database
, and password
are read from the environmental variables. In the configuration below, the database is also used as the username.
let config = {};
config.database = {
hostname: Deno.env.get("PGHOST"),
database: Deno.env.get("PGDATABASE"),
user: Deno.env.get("PGDATABASE"),
password: Deno.env.get("PGPASSWORD"),
port: 5432,
};
console.log(config.database);
export { config };
Running the above application with the proper environmental variables would produce the following output.
PGHOST='some-address' PGDATABASE='some-database' PGPASSWORD='my-password' deno run --allow-env app.js
{
hostname: "some-address",
database: "some-database",
user: "some-database",
password: "my-password",
port: 5432
}
Other options for adding environmental variables also exists. For example, the denv library provides means for reading environmental/configuration variables from a file.
Mocking
Mocking refers to the act of creating function or object mocks that provide testable interfaces that can be used to verify the behavior of an application. Mocks are used as substitute parts of a program to help test the program or parts of it in a deterministic way. Here, we showcase a few simple techniques for testing parts of applications.
While when working with oak, we can test controllers and APIs through the HTTP interfaces that the application provides. We could, also, provide mocks for testing. We, for example, know that controllers and APIs are passed a context object as a parameter, and we know how the context object is used.
If we know, for example, that the controller should call the render function from the context with 'index.eta' as a parameter, we can create the functionality for checking what was called as follows. In the example below, hello
is the controller function that is being called.
// import hello from controller file
// in the test function
let usedParameterValue = null;
const myRenderFunction = (parameterValue) => {
usedParameterValue = parameterValue;
};
const myContext = {
render: myRenderFunction,
};
hello(myContext);
console.log(usedParameterValue);
In the above example, we would log the used parameter value. If we would instead wish to check that the hello
function for example was given index.eta
as the file to render, then we would use assertEquals
to verify the content.
assertEquals(usedParameterValue, "index.eta");
Similarly, assume that we have a situation where we would wish to test specific middleware functionality that would check whether the user is authenticated from the session. A very simple mock would return true
from all requests that attempt to get a value from the session.
let usedParameterValue = null;
const myRenderFunction = (parameterValue) => {
usedParameterValue = parameterValue;
};
const getAuthenticated = (param) => {
return true;
};
const myContext = {
state: {
session: {
get: getAuthenticated,
},
},
render: myRenderFunction,
};
const next = () => {
};
checkIsAuthenticated(myContext, next);
console.log(usedParameterValue);
In practice, one could also set study the response from the server after a login with superoak, and use the cookie data from headers to subsequently authenticate the user.
Mocking libraries
There exists Mocking libraries for Deno such as mock. As Aalto University has a separate testing course that covers a wide variety of testing techniques, we do not go into mocking in more detail here.
Handling leaking resources
If you work on tests that might create a situation where some resources are not properly closed when the test has been finished, you will see an error from the test indicating that a test case is leaking resources. To create tests that can handle such cases, you can use test sanitizers and write tests in a bit more verbose way, which works as follows. Instead of writing:
Deno.test("Test /", async () => {
const testClient = await superoak(app);
await testClient.get("/").expect(200);
});
You would write:
Deno.test({
name: "Test /",
async fn() {
const testClient = await superoak(app);
await testClient.get("/").expect(200);
},
sanitizeResources: false,
sanitizeOps: false,
});
This format is useful also when testing applications that use a database connection pool (which leaves connections open).