Testing is critical in software development to ensure your code works as expected, and continues to work as you make changes. Tests verify that your functions, modules, and applications behave correctly, handle edge cases appropriately, and maintain expected performance characteristics.
Why testing matters Jump to heading#Testing your code allows you to catch bugs, issues or regressions before they reach production, saving time and resources. Tests are also useful to help plan out the logic of your application, they can serve as a human readable description of how your code is meant to be used.
Deno provides built-in testing capabilities, making it straightforward to implement robust testing practices in your projects.
Writing tests withDeno.test
Jump to heading#
Defining a test in Deno is straightforward - use the Deno.test()
function to register your test with the test runner. This function accepts either a test name and function, or a configuration object with more detailed options. All test functions in files that match patterns like *_test.{ts,js,mjs,jsx,tsx}
or *.test.{ts,js,mjs,jsx,tsx}
are automatically discovered and executed when you run the deno test
command.
Here are the basic ways to define tests:
Deno.test("my first test", () => {
});
Deno.test({
name: "my configured test",
fn: () => {
},
ignore: false,
only: false,
permissions: {
read: true,
write: false,
},
});
A simple example test Jump to heading#
Let's start with a simple test. Create a file called main_test.ts
, in it we will test a basic addition operation using Deno's testing API and the assertEquals
function from the Deno Standard Library.
We use Deno.test
and provide a name that describes what the test will do:
main_test.ts
import { assertEquals } from "jsr:@std/assert";
function add(a: number, b: number): number {
return a + b;
}
Deno.test("basic addition test", () => {
const a = 1;
const b = 2;
const result = add(a, b);
assertEquals(result, 3);
});
To run this test, use the deno test
command:
You should see output indicating that your test has passed:
running 1 test from ./hello_test.ts
basic addition test ... ok (2ms)
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out (2ms)
Try changing the function implementation to make the test fail:
function add(a: number, b: number): number {
return a - b;
}
You'll see an error message that clearly shows what went wrong:
running 1 test from ./hello_test.ts
basic addition test ... FAILED (3ms)
failures:
basic addition test => ./hello_test.ts:12:3
error: AssertionError: Values are not equal:
[Diff] Actual / Expected
- -1
+ 3
at assertEquals (https://jsr.io/@std/assert@0.218.2/assert_equals.ts:31:9)
at Object.fn (file:///path/to/hello_test.ts:12:3)
at asyncOpSanitizer (ext:core/01_core.js:199:13)
at Object.sanitizeOps (ext:core/01_core.js:219:15)
at runTest (ext:test/06_test_runner.js:319:29)
at test (ext:test/06_test_runner.js:593:7)
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out (3ms)
This clear feedback helps you quickly identify and fix issues in your code.
Test structure and organization Jump to heading#Deno will automatically find and run tests that match naming patterns like *_test.{ts,js,mjs,jsx,tsx}
or *.test.{ts,js,mjs,jsx,tsx}
. There are plenty of ways to organize your test files, we recommend co-locating your unit tests with the code they are testing, and keeping integration tests and configuration in a tests
directory. This allows for immediate discovery of unit tests and simplified imports, while keeping a separation between different types of tests.
Here's an example of how you might structure your project with tests:
my-deno-project/
├── src/
│ ├── models/
│ │ ├── user.ts
│ │ ├── user_test.ts // Unit tests for user model
│ │ ├── product.ts
│ │ └── product_test.ts // Unit tests for product model
│ ├── services/
│ │ ├── auth-service.ts
│ │ ├── auth-service_test.ts // Unit tests for auth service
│ │ ├── data-service.ts
│ │ └── data-service_test.ts // Unit tests for data service
│ └── utils/
│ ├── helpers.ts
│ └── helpers_test.ts // Unit tests for helpers
├── tests/
│ ├── integration/ // Integration tests directory
│ │ ├── api_test.ts // Tests API endpoints
│ │ └── db_test.ts // Tests database interactions
│ ├── e2e/ // End-to-end tests
│ │ └── user_flow_test.ts // Tests complete user workflows
│ └── fixtures/ // Shared test data and utilities
│ ├── test_data.ts // Test data used across tests
│ └── setup.ts // Common setup functions
├── main.ts
└── deno.json // Project configuration
This kind of structure offers a centralized place for test configuration while maintaining the benefits of co-locating unit tests with their relevant files. With this structure, you can:
deno test
deno test src/
deno test tests/integration/
deno test src/models/
deno test src/models/user_test.ts
Assertions Jump to heading#
Assertions are the building blocks of effective tests, allowing you to verify that your code behaves as expected. They check if a specific condition is true and throw an error if it's not, causing the test to fail. Good assertions are clear, specific, and help identify exactly what went wrong when a test fails.
Deno doesn't include assertions in its core library, but you can import them from the Deno standard library:
import {
assertArrayIncludes,
assertEquals,
assertExists,
assertMatch,
assertNotEquals,
assertObjectMatch,
assertRejects,
assertStrictEquals,
assertStringIncludes,
assertThrows,
} from "jsr:@std/assert";
Deno.test("assertion examples", () => {
assertEquals(1 + 1, 2);
assertNotEquals("hello", "world");
assertExists("Hello");
assertStringIncludes("Hello, world!", "world");
assertMatch("deno@1.0.0", /^deno@\d+\.\d+\.\d+$/);
assertObjectMatch(
{ name: "Jane", age: 25, city: "Tokyo" },
{ name: "Jane" },
);
assertStrictEquals("deno", "deno");
assertThrows(
() => {
throw new Error("Something went wrong");
},
Error,
"Something went wrong",
);
});
For those that prefer fluent assertions (familiar to users of Jest), you can use the expect
module:
import { expect } from "jsr:@std/expect";
Deno.test("expect style assertions", () => {
expect(5).toBe(5);
expect({ name: "deno" }).toEqual({ name: "deno" });
expect([1, 2, 3]).toContain(2);
expect(true).toBeTruthy();
expect(0).toBeFalsy();
expect(null).toBeNull();
expect(undefined).toBeUndefined();
expect(100).toBeGreaterThan(99);
expect(1).toBeLessThan(2);
expect("Hello world").toMatch(/world/);
expect(() => {
throw new Error("fail");
}).toThrow();
});
Real-world Example Jump to heading#
Here's a more realistic example testing a function that processes user data:
export function validateUser(user: any): { valid: boolean; errors: string[] } {
const errors: string[] = [];
if (!user.name || typeof user.name !== "string") {
errors.push("Name is required and must be a string");
}
if (!user.email || !user.email.includes("@")) {
errors.push("Valid email is required");
}
if (
user.age !== undefined && (typeof user.age !== "number" || user.age < 18)
) {
errors.push("Age must be a number and at least 18");
}
return {
valid: errors.length === 0,
errors,
};
}
import { assertEquals } from "jsr:@std/assert";
import { validateUser } from "./user_processor.ts";
Deno.test("validateUser", async (t) => {
await t.step("should validate a correct user object", () => {
const user = {
name: "John Doe",
email: "john@example.com",
age: 30,
};
const result = validateUser(user);
assertEquals(result.valid, true);
assertEquals(result.errors.length, 0);
});
await t.step("should return errors for invalid user", () => {
const user = {
name: "",
email: "invalid-email",
age: 16,
};
const result = validateUser(user);
assertEquals(result.valid, false);
assertEquals(result.errors.length, 3);
assertEquals(result.errors[0], "Name is required and must be a string");
assertEquals(result.errors[1], "Valid email is required");
assertEquals(result.errors[2], "Age must be a number and at least 18");
});
await t.step("should handle missing properties", () => {
const user = {
name: "Jane Doe",
};
const result = validateUser(user);
assertEquals(result.valid, false);
assertEquals(result.errors.length, 1);
assertEquals(result.errors[0], "Valid email is required");
});
});
Async testing Jump to heading#
Deno handles async tests naturally. Just make your test function async and use await:
import { assertEquals } from "jsr:@std/assert";
Deno.test("async test example", async () => {
const response = await fetch("https://deno.land");
const status = response.status;
assertEquals(status, 200);
});
Testing async functions Jump to heading#
When testing functions that return promises, you should always await the result:
export async function fetchUserData(userId: string) {
const response = await fetch(`https://api.example.com/users/${userId}`);
if (!response.ok) {
throw new Error(`Failed to fetch user: ${response.status}`);
}
return await response.json();
}
import { assertEquals, assertRejects } from "jsr:@std/assert";
import { fetchUserData } from "./async-function.ts";
Deno.test("fetchUserData success", async () => {
globalThis.fetch = async (url: string) => {
const data = JSON.stringify({ id: "123", name: "Test User" });
return new Response(data, { status: 200 });
};
const userData = await fetchUserData("123");
assertEquals(userData.id, "123");
assertEquals(userData.name, "Test User");
});
Deno.test("fetchUserData failure", async () => {
globalThis.fetch = async (url: string) => {
return new Response("Not Found", { status: 404 });
};
await assertRejects(
async () => await fetchUserData("nonexistent"),
Error,
"Failed to fetch user: 404",
);
});
Mocking in tests Jump to heading#
Mocking is an essential technique for isolating the code being tested from its dependencies. Deno provides built-in utilities and third-party libraries for creating mocks.
Basic Mocking Jump to heading#You can create simple mocks by replacing functions or objects with your own implementations. This allows you to control the behavior of dependencies and test how your code interacts with them.
const api = {
fetchData: async () => {
const response = await fetch("https://api.example.com/data");
return response.json();
},
};
Deno.test("basic mocking example", async () => {
const originalFetchData = api.fetchData;
api.fetchData = async () => {
return { id: 1, name: "Test Data" };
};
try {
const result = await api.fetchData();
assertEquals(result, { id: 1, name: "Test Data" });
} finally {
api.fetchData = originalFetchData;
}
});
Using Spy Functions Jump to heading#
Spies allow you to track function calls without changing their behavior:
import { spy } from "jsr:@std/testing/mock";
Deno.test("spy example", () => {
const consoleSpy = spy(console, "log");
console.log("Hello");
console.log("World");
assertEquals(consoleSpy.calls.length, 2);
assertEquals(consoleSpy.calls[0].args, ["Hello"]);
assertEquals(consoleSpy.calls[1].args, ["World"]);
consoleSpy.restore();
});
For more advanced mocking techniques, check our dedicated guide on mocking in Deno.
Coverage Jump to heading#Code coverage is a metric that helps you understand how much of your code is being tested. It measures which lines, functions, and branches of your code are executed during your tests, giving you insight into areas that might lack proper testing.
Coverage analysis helps you to:
Note
High coverage doesn't guarantee high-quality tests. It simply shows what code was executed, not whether your assertions are meaningful or if edge cases are handled correctly.
Deno provides built-in coverage tools to help you analyze your test coverage. To collect coverage information:
deno test --coverage=coverage_dir
This generates coverage data in a specified directory (here, coverage_dir
). To view a human-readable report:
deno coverage coverage_dir
You'll see output like:
file:///projects/my-project/src/utils.ts 85.7% (6/7)
file:///projects/my-project/src/models/user.ts 100.0% (15/15)
file:///projects/my-project/src/services/auth.ts 78.3% (18/23)
total: 87.5% (39/45)
For more detailed insights, you can also generate an HTML report:
deno coverage --html coverage_dir
This creates an interactive HTML report in the specified directory that shows exactly which lines are covered and which are not.
By default, the coverage tool automatically excludes:
test.ts
or test.js
)file:
)This ensures your coverage reports focus on your application code rather than test files or external dependencies.
Coverage Configuration Jump to heading#You can exclude files from coverage reports by using the --exclude
flag:
deno coverage --exclude="test_,vendor/,_build/,node_modules/" coverage_dir
Integrating with CI Jump to heading#
For continuous integration environments, you might want to enforce a minimum coverage threshold:
# In your GitHub Actions workflow
- name: Run tests with coverage
run: deno test --coverage=coverage_dir
- name: Check coverage meets threshold
run: |
COVERAGE=$(deno coverage coverage_dir | grep "total:" | grep -o '[0-9]\+\.[0-9]\+')
if (( $(echo "$COVERAGE < 80" | bc -l) )); then
echo "Test coverage is below 80%: $COVERAGE%"
exit 1
fi
When working on your test coverage, remember to set realistic goals, aim for meaningful coverage with high quality tests over 100% coverage.
Comparison with other testing frameworks Jump to heading#If you're coming from other JavaScript testing frameworks, here's how Deno's testing capabilities compare:
Feature Deno Jest Mocha Jasmine Setup Built-in Requires installation Requires installation Requires installation SyntaxDeno.test()
test()
, describe()
it()
, describe()
it()
, describe()
Assertions From std library Built-in expect Requires assertion library Built-in expect Mocking From std library Built-in jest.mock() Requires sinon or similar Built-in spies Async support Native Needs special handling Supports promises Supports promises File watching --watch
flag watch mode Requires nodemon Requires extra tools Code coverage Built-in Built-in Requires istanbul Requires istanbul Testing Style Comparison Jump to heading#
Deno:
import { assertEquals } from "jsr:@std/assert";
Deno.test("add function", () => {
assertEquals(1 + 2, 3);
});
Jest:
test("add function", () => {
expect(1 + 2).toBe(3);
});
Mocha:
import { assert } from "chai";
describe("math", () => {
it("should add numbers", () => {
assert.equal(1 + 2, 3);
});
});
Jasmine:
describe("math", () => {
it("should add numbers", () => {
expect(1 + 2).toBe(3);
});
});
Next steps Jump to heading#
🦕 Deno's built-in testing capabilities make it easy to write and run tests without needing to install extra testing frameworks or tools. By following the patterns and practices outlined in this tutorial, you can ensure your Deno applications are well-tested and reliable.
For more information about testing in Deno, check out:
RetroSearch is an open source project built by @garambo | Open a GitHub Issue
Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo
HTML:
3.2
| Encoding:
UTF-8
| Version:
0.7.4