A Complete Reference for Unit Testing, Integration Testing, and Supabase Edge Functions
1. Introduction to Deno Testing
1.1 Overview of Deno's Built-In Test Runner
1.1.1 Core Philosophy and Design Principles
Deno includes a built-in test runner as part of its core runtime, eliminating the need to install external testing frameworks like Jest or Mocha. This design philosophy significantly reduces dependency management overhead and simplifies the development workflow. As noted in the official documentation, "Deno has a built-in test runner that you can use for testing JavaScript or TypeScript code."
The test runner follows a zero-configuration philosophy—it works immediately after Deno installation without requiring any setup files, configuration objects, or initialization commands. This approach dramatically simplifies onboarding for new developers who can begin writing and running tests within minutes of installing Deno.
Deno's test runner inherits the runtime's security-first design through a permission-based sandbox model. Tests run with restricted access by default, meaning they cannot access the file system, network, or environment variables unless explicitly granted permission. This security model ensures that tests can verify code behavior when permissions are denied, catching potential security issues early in the development cycle.
The test runner emphasizes web standard compliance, using standard APIs like fetch rather than proprietary modules. This alignment with web standards means testing strategies developed for Deno are transferable to browser environments and vice versa. When testing HTTP requests, you use the same fetch API that runs in production Edge Functions.
1.1.2 Comparison with External Test Frameworks (Jest, Mocha)
Deno's test runner shares key similarities with popular frameworks:
| Feature | Deno Test | Jest | Mocha |
|---|---|---|---|
| Assertion Functions | Built-in via std library | Built-in | Requires Chai |
| Async Support | Native async/await | Native async/await | Native async/await |
| Test Organization | Deno.test() |
describe/it |
describe/it |
| TypeScript Support | Native | Requires babel/ts-jest | Requires ts-node |
Unique Deno advantages include:
- Native TypeScript support without transpilation or additional configuration
- Single binary approach—no
node_modulesdirectory or npm-based toolchains - Built-in permissions testing for security validation
- URL-based imports with version pinning
Areas where external frameworks excel:
- Richer plugin ecosystems with hundreds of extensions
- More output format options (HTML reports, JUnit XML)
- Snapshot testing with visual diff tools
- Coverage reporting with Istanbul integration
Migration considerations: Existing Jest tests can be adapted to Deno with syntax translations:
// Jest syntax
describe('math', () => {
it('adds numbers', () => {
expect(add(2, 3)).toBe(5);
});
});
// Deno equivalent
Deno.test('math: adds numbers', () => {
assertEquals(add(2, 3), 5);
});
1.1.3 TypeScript-First Architecture
Deno executes TypeScript natively without a separate compilation step. When you run deno test myfile_test.ts, Deno compiles and caches the TypeScript internally, providing immediate feedback without manual build configuration. Type checking integrates seamlessly with test runs, catching type errors before assertions execute.
Benefits for test code quality include:
- Type safety in test assertions prevents comparing incompatible types
- Interfaces define expected shapes for mock objects and test data
- IDE autocompletion works out-of-the-box for test utilities
- Refactoring propagates through test files automatically
Import syntax differs from Node.js:
// Deno: URL-based imports with version pinning
import { assertEquals } from 'jsr:@std/assert@1';
import { createClient } from 'npm:@supabase/supabase-js@2';
// Node.js: Package-based requires
const { assertEquals } = require('assert');
const { createClient } = require('@supabase/supabase-js');
1.2 Test Runner Features and Capabilities
1.2.1 Native Feature Set
1.2.1.1 Parallel Execution Support
The --jobs parameter controls worker count for parallel test execution. As documented in the source materials, "The test runner has an option to run multiple test files (also called suites) in parallel using test workers." Importantly, parallelism operates at the suite level (file level), not individual test level.
deno test --jobs 3 # Run up to 3 test files simultaneously
Performance improvements are substantial. From the source documentation:
"A single run of the above suite takes around 9.5 seconds... A parallel execution of a_test.ts and a2_test.ts takes around 9.7 seconds. It takes almost half the time! This would significantly reduce the CI/CD time when there is a big test suite to execute."
Worker allocation behavior: When you specify --jobs N, Deno spawns N workers. Test files are queued and assigned to workers as they become free. The optimal job count typically matches your CPU core count, though I/O-bound tests may benefit from higher values.
1.2.1.2 Recursive Test Discovery
Deno's test runner recursively finds and runs all tests present in a given directory. The file pattern matching rules follow this specification:
{*_,*.,}test.{js,mjs,ts,jsx,tsx}
Valid test file name examples:
-
user_test.ts(underscore separator) -
user.test.ts(dot separator) -
test.ts(standalone) -
api_test.js,component.test.tsx
Directory traversal automatically scans all subdirectories when you provide a directory path:
deno test myApp/ # Scans myApp/ and all subdirectories
deno test # Scans current directory recursively
If no test files are found, Deno prints: No matching test modules found
1.2.1.3 Permission-Based Sandboxing
Relevant permission flags:
| Flag | Controls | Example |
|---|---|---|
--allow-net |
Network requests via fetch | --allow-net=localhost:54321 |
--allow-env |
Environment variable access | --allow-env=SUPABASE_URL |
--allow-read |
File system reads | --allow-read=./fixtures |
--allow-write |
File system writes | --allow-write=./output |
--allow-all |
All permissions | Development convenience |
Security implications: Tests can verify that code behaves correctly when permissions are denied. For example, testing that an unauthorized network request throws an appropriate error rather than silently failing.
Production parity guidance: CI tests should use the same permission set as production deployments. Avoid over-using --allow-all in CI pipelines to ensure tests catch permission-related bugs.
1.2.2 Current Limitations
1.2.2.1 Console-Only Output
The test runner outputs results only to stdout/stderr—there is no native support for file-based reports or alternative output channels. From the source documentation:
"The output and result of the test is sent only to console. This poses an issue when running tests through scripts (like CI/CD)."
Workarounds include:
- Capturing output via child processes using
Deno.run() - Piping stdout to files:
deno test > results.txt - Building custom result processors that parse console output
1.2.2.2 Text-Based Results Format
Deno's test runner produces only textual output with no native JSON format. This creates challenges for automation:
"The test runner produces only a textual output. There is no support for JSON. A JSON formatted output would have been useful to analyze later and/or generate stats."
Parsing requires regex patterns to extract pass/fail counts:
const pattern = /test\sresult:\s(.*)(\d+)\spassed;\s(\d+)\sfailed;\s(\d+)\signored;.*/gm;
Community libraries and custom utilities can convert text output to structured JSON for CI/CD integration (covered in Section 9).
1.3 When to Use Deno's Test Runner
1.3.1 Unit Testing Use Cases
Appropriate unit testing scenarios:
- Testing pure functions with no side effects
- Validating business logic in isolation
- Testing data transformations and calculations
- Verifying error handling for invalid inputs
The import pattern for unit tests involves importing functions directly from source modules:
import { getLongUuid } from "../lib/a/a.ts";
import { assert, assertThrows } from "jsr:@std/assert@1";
Deno.test('len is 1', () => {
assert(getLongUuid(1).length === 36);
});
Deno.test('len is 0 throws error', () => {
assertThrows(() => getLongUuid(0), Deno.errors.InvalidData);
});
Coverage expectations: Unit tests form the pyramid's base. Aim for high function coverage, with one test file per source module (e.g., src/a.ts → test/a_test.ts).
1.3.2 Integration Testing Use Cases
Integration testing scope covers testing interactions between components—verifying that modules work correctly together. As noted in the documentation:
"Deno's test runner is flexible enough to do integration testing too... The test runner is flexible enough to be used beyond simple unit testing."
The service-under-test model involves running your application as a service and testing it through HTTP requests (blackbox testing):
Deno.test("Integration: GET /data", async () => {
const res = await fetch('http://localhost:5000/data');
assert(res.ok === true);
assert(res.status === 200);
});
Marking and separation strategies use environment variables:
Deno.test({
name: "integration test",
ignore: !Deno.env.get('TEST_TYPE_INTEG'), // Skip unless integration mode
fn: async () => { /* test code */ }
});
1.3.3 Edge Function Testing Use Cases
Edge Function specific requirements include:
- Supabase client integration for database and function invocation
- Testing against the local Supabase stack (
supabase start) - Environment configuration for
SUPABASE_URLand keys
CORS testing necessity: Edge Functions must handle preflight OPTIONS requests:
if (req.method === 'OPTIONS') {
return new Response('ok', { headers: corsHeaders });
}
Function invocation testing uses the Supabase client:
const { data, error } = await client.functions.invoke('hello-world', {
body: { name: 'TestUser' }
});
assertEquals(data.message, 'Hello TestUser!');
2. Environment Setup and Configuration
2.1 Installing Prerequisites
2.1.1 Deno Runtime Installation
Platform-specific installation commands:
# macOS/Linux (official installer)
curl -fsSL https://deno.land/install.sh | sh
# macOS (Homebrew)
brew install deno
# Windows (PowerShell)
irm https://deno.land/install.ps1 | iex
# Windows (Chocolatey)
choco install deno
Version management:
# Check installed version
deno --version
# Upgrade to latest
deno upgrade
# Upgrade to specific version
deno upgrade --version 1.40.0
Verify successful installation by running:
deno eval "console.log('Deno is working!')"
2.1.2 Docker Installation for Supabase Local Stack
Docker's role in local development: Supabase relies on Docker for running PostgreSQL and other services locally. From the documentation:
"Install Docker: Supabase relies on Docker for local development."
Installation guides:
- macOS/Windows: Install Docker Desktop from docs.docker.com/desktop/install/mac-install/
- Linux: Use your distribution's package manager or Docker's official repository
Resource requirements:
- Minimum 4GB RAM allocated to Docker
- Recommended 8GB+ for development with multiple services
- 2+ CPU cores for reasonable performance
Configure Docker Desktop settings under Preferences → Resources.
2.1.3 Supabase CLI Installation
npm installation command:
npm install -g supabase
Initialize a Supabase project:
supabase init
This creates the necessary configuration files including supabase/config.toml.
Start the local stack:
supabase start
This launches Docker containers for:
- PostgreSQL database
- GoTrue (authentication)
- PostgREST (auto-generated API)
- Realtime (websocket server)
- Storage API
- Edge Runtime
2.2 Project Structure and Organization
2.2.1 Recommended Folder Hierarchy
2.2.1.1 Source Code Placement
The standard structure from Supabase documentation:
└── supabase
├── functions
│ ├── function-one
│ │ └── index.ts
│ └── function-two
│ └── index.ts
└── config.toml
Naming conventions: Use hyphens for function names (e.g., hello-world). The function name becomes the URL endpoint: https://your-project.supabase.co/functions/v1/hello-world.
Shared code organization: Create a _shared directory for common utilities:
└── supabase
└── functions
├── _shared
│ ├── cors.ts
│ └── supabase-client.ts
├── function-one
│ └── index.ts
Import using relative paths: import { corsHeaders } from "../_shared/cors.ts".
2.2.1.2 Test File Placement Strategies
The separate tests directory approach (recommended):
└── supabase
├── functions
│ ├── function-one
│ │ └── index.ts
│ └── tests
│ └── function-one-test.ts # Tests for function-one
└── config.toml
The co-located approach places test files alongside source:
└── supabase
└── functions
└── function-one
├── index.ts
└── index_test.ts
Recommendation: Separate directories are preferred for larger projects as they clearly distinguish production code from test code. Co-location works well for small projects or single-function repositories.
2.2.1.3 Shared Utilities and Constants
Create a constants file to reduce magic numbers:
// tests/constants.ts
export const SIZE_OF_SINGLE_UUID = 36;
export const TEST_TIMEOUT_MS = 5000;
export const LOCAL_SUPABASE_URL = "http://localhost:54321";
Build test helper functions:
// tests/helpers.ts
export function createTestUser() {
return { id: crypto.randomUUID(), email: `test-${Date.now()}@example.com` };
}
Organize mock data in fixture files:
// tests/fixtures/users.json
[
{ "id": "user-1", "name": "Test User", "role": "admin" }
]
2.2.2 Test File Naming Conventions
2.2.2.1 Pattern Matching Rules
All valid patterns:
-
*_test.ts→user_test.ts,api_test.ts -
*.test.ts→user.test.ts,api.test.ts -
test.ts→ Standalone test file
Examples of valid test file names:
function-one-test.tsdatabase.test.tsauth_test.ts
Pattern matching order: Deno scans recursively and collects all files matching any valid pattern.
2.2.2.2 File Extension Requirements
Supported extensions: .ts, .js, .tsx, .jsx, .mjs
Recommendation: Use .ts extensions for type safety:
// user_test.ts - TypeScript provides better IDE support
import { UserService } from "../services/user.ts";
import type { User } from "../types.ts";
Use .tsx for testing React/Preact components that render JSX.
2.3 Environment Variables Management
2.3.1 Creating and Loading .env Files
2.3.1.1 Local Development Configuration
Create the .env file:
# Create the file
touch .env.local
# Add Supabase variables
echo "SUPABASE_URL=http://localhost:54321" >> .env.local
echo "SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0" >> .env.local
Load variables in test files:
// Import at the top of your test file
import 'jsr:@std/dotenv/load';
// Now Deno.env is populated
const supabaseUrl = Deno.env.get('SUPABASE_URL');
Secure the file: Add .env.local to .gitignore to prevent committing secrets:
# .gitignore
.env
.env.local
.env.*.local
2.3.1.2 Test-Specific Environment Files
Create separate environment files for different contexts:
# .env.test - Test-specific configuration
SUPABASE_URL=http://localhost:54321
TEST_TYPE_INTEG=1
LOG_LEVEL=debug
Use the --env-file flag:
deno test --env-file=.env.test --allow-all supabase/functions/tests/
This approach differs from importing dotenv—the variables are available before module loading.
2.3.2 Accessing Environment Variables in Tests
Use Deno.env.get():
const supabaseUrl = Deno.env.get('SUPABASE_URL'); // Returns string | undefined
Validate required variables:
const supabaseUrl = Deno.env.get('SUPABASE_URL');
const supabaseKey = Deno.env.get('SUPABASE_ANON_KEY');
if (!supabaseUrl) throw new Error('SUPABASE_URL is required.');
if (!supabaseKey) throw new Error('SUPABASE_ANON_KEY is required.');
Handle optional variables with nullish coalescing:
const logLevel = Deno.env.get('LOG_LEVEL') ?? 'info';
const timeout = Number(Deno.env.get('TIMEOUT_MS') ?? '5000');
2.3.3 Secret Management for CI/CD
Never commit secrets to version control:
# .gitignore
.env*
!.env.example
Use GitHub Secrets for CI:
# .github/workflows/test.yml
- name: Run Tests
run: deno test --allow-all
env:
SUPABASE_URL: ${{ secrets.SUPABASE_URL }}
SUPABASE_ANON_KEY: ${{ secrets.SUPABASE_ANON_KEY }}
Protect sensitive output: Ensure test runners don't log secrets in assertion failures or debug output.
2.4 Editor Configuration (VS Code)
2.4.1 Deno Extension Setup
Install the official Deno extension (ID: denoland.vscode-deno). The extension provides:
- TypeScript autocompletion
- Real-time type checking
- Import suggestions from deno.land and JSR
Enable Deno for specific folders in .vscode/settings.json:
{
"deno.enable": true,
"deno.lint": true,
"deno.unstable": true
}
2.4.2 Multi-Root Workspace Configuration
The Node.js/Deno conflict: Projects often have both frontend (Node.js) and Edge Functions (Deno), causing extension conflicts. TypeScript intellisense may break in one environment.
Create a multi-root workspace by adding separate folders with path-specific settings:
{
"deno.enable": true,
"deno.enablePaths": ["./supabase/functions"]
}
2.4.3 Path-Specific Settings
Create .vscode/settings.json in your project root:
{
"deno.enable": false,
"deno.enablePaths": ["supabase/functions"],
"[typescript]": {
"editor.defaultFormatter": "denoland.vscode-deno"
}
}
This enables Deno only for the functions path while preserving Node.js support elsewhere.
3. Test Runner Fundamentals
3.1 Running Tests
3.1.1 Basic Test Execution Commands
3.1.1.1 Running All Tests in Directory
Basic command:
deno test myApp/
This recursively scans myApp/ and all subdirectories for test files matching the pattern {*_,*.,}test.{js,mjs,ts,jsx,tsx}.
Output format:
running 5 tests from file:///Users/.../test/a_test.ts
test len is 0 ... ok (5ms)
test len is 1 ... ok (3ms)
test len is 3 ... ok (1ms)
...
test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out (93ms)
Summary interpretation:
- passed: Tests that completed without throwing exceptions
- failed: Tests that threw exceptions
-
ignored: Tests skipped via
ignore: true -
measured: Benchmark tests (using
Deno.bench) -
filtered out: Tests excluded by
--filter
3.1.1.2 Running Specific Test Files
Target a single file:
deno test myApp/test/a_test.ts
Target multiple specific files:
deno test a_test.ts b_test.ts c_test.ts
Combine with permission flags:
deno test --allow-net --allow-env myApp/test/api_test.ts
3.1.1.3 Running Tests in Current Directory
Simple command without arguments:
deno test
This scans the current directory recursively. If no test files exist:
No matching test modules found
Troubleshooting: Verify file names match the pattern (e.g., user_test.ts not usertest.ts).
3.1.2 Permission Flags
3.1.2.1 Network Permissions (--allow-net)
When network permission is needed:
- HTTP requests via
fetch() - Supabase client communication
- Any external API calls
Grant full network access:
deno test --allow-net
Restrict to specific hosts:
deno test --allow-net=localhost:54321,api.stripe.com
3.1.2.2 Environment Permissions (--allow-env)
When environment permission is needed:
- Accessing
Deno.env.get() - Reading configuration from environment
Grant full access:
deno test --allow-env
Restrict to specific variables:
deno test --allow-env=SUPABASE_URL,SUPABASE_KEY
3.1.2.3 File System Permissions (--allow-read, --allow-write)
Read permission requirements:
- Loading configuration files
- Reading fixture data
- Accessing seed files
Write permission requirements:
- Writing test artifacts
- Creating log files
- Generating reports
Use path restrictions:
deno test --allow-read=./fixtures --allow-write=./output
3.1.2.4 Using --allow-all for Development
The convenience flag grants all permissions:
deno test --allow-all
Warning for CI usage: Production CI should use specific permissions to catch security issues:
# CI pipeline - explicit permissions
deno test --allow-net=localhost:54321 --allow-env --allow-read=./fixtures
3.2 Test Runner Phases
3.2.1 Registration Phase
3.2.1.1 Directory Scanning Process
Recursive scanning: Deno traverses all subdirectories under the given path, collecting files in alphabetical order by default.
File matching: Only files matching the test pattern are processed:
{*_,*.,}test.{js,mjs,ts,jsx,tsx}
Error handling: If any test case fails parsing (syntax error), the test runner doesn't proceed to execution phase.
3.2.1.2 Test File Pattern Matching
Pattern breakdown:
-
*_test→ Any characters followed by underscore, then "test" -
*.test→ Any characters followed by dot, then "test" -
test→ Just "test" (no prefix required) -
.{js,mjs,ts,jsx,tsx}→ Any of these extensions
Positive examples: user_test.ts, api.test.js, test.ts
Negative examples: usertest.ts (missing separator), test_user.ts (wrong order)
3.2.1.3 Test Case Collection
Each Deno.test() call adds a test to the pending list:
Deno.test('test 1', () => { /* collected */ });
Deno.test('test 2', () => { /* collected */ });
Validation: Test functions must be valid TypeScript/JavaScript. Compile errors abort registration.
Naming requirements: Test names should be unique strings. Duplicate names may cause confusion in output.
3.2.2 Execution Phase
3.2.2.1 Sequential vs Parallel Execution
Default sequential behavior: Without --jobs, tests run one-by-one in the order they were collected.
Parallel activation:
deno test --jobs 4 # 4 concurrent workers
From the documentation:
"The test runner doesn't go inside a file and run the tests cases in parallel. Instead, it runs the suites in parallel."
Execution time comparison (from source materials):
- Sequential (2 files): ~19 seconds
- Parallel
--jobs 2: ~9.7 seconds (50% reduction)
3.2.2.2 Error Recording and Continuation
Default continuation behavior: Failures are recorded but execution continues:
test len is 0 ... FAILED (2ms)
test len is 1 ... ok (3ms)
test len is 3 ... ok (1ms)
Fail-fast mode:
deno test --fail-fast
Stops at first failure with immediate error output.
3.2.2.3 Result Reporting
Per-test output format:
test NAME ... ok (Xms)
test NAME ... FAILED (Xms)
test NAME ... ignored (0ms)
Summary line:
test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out (93ms)
Exit codes: 0 for success, non-zero for failure—enabling CI integration.
3.3 Test Definition Syntax
3.3.1 Simple Test Definition
Basic two-argument syntax:
Deno.test('test name', () => {
// Test code here
});
Simple example:
import { assertEquals } from 'jsr:@std/assert@1';
function add(a: number, b: number): number {
return a + b;
}
Deno.test('addition works', () => {
const result = add(2, 3);
assertEquals(result, 5);
});
Pass/fail criteria: The test passes if no exception is thrown.
3.3.2 Structured Test Definition Object
Object syntax with options:
Deno.test({
name: 'integration test',
ignore: !Deno.env.get('RUN_INTEGRATION'),
fn: async () => {
// test code
}
});
Available options:
| Option | Type | Purpose |
|---|---|---|
name |
string | Test name for output |
fn |
function | Test function to execute |
ignore |
boolean | Skip test if true |
only |
boolean | Run only this test |
sanitizeOps |
boolean | Check for async operation leaks |
sanitizeResources |
boolean | Check for resource leaks |
sanitizeExit |
boolean | Check for Deno.exit() calls |
3.3.3 Async Test Functions
Async test support:
Deno.test('async operation', async () => {
const result = await fetchData();
assertExists(result);
});
Deno automatically awaits async test functions. Long-running tests complete normally without special configuration.
3.4 Understanding Test Results
3.4.1 Pass Criteria (No Exception)
The fundamental rule:
"The test is considered passed if the test function finishes without raising any exception. Pass = No exception."
Assertion behavior: All assertion functions throw AssertionError on failure:
assertEquals(1, 2); // Throws AssertionError: Values are not equal
Implicit passing: A test that returns without throwing succeeds, even if empty:
Deno.test('empty test', () => {}); // Passes
3.4.2 Failure Handling
Failure output includes stack traces:
failures:
len is 0
InvalidData: len must be between 1 and 5
at getLongUuid (file:///Users/.../a.ts:5:15)
at file:///Users/.../a_test.ts:4:5
assertEquals diff output shows color-coded differences:
AssertionError: Values are not equal:
[Diff] Actual / Expected
- 2021-07-01T07:00:00.000Z
+ 2021-07-02T07:00:00.000Z
3.4.3 Output Interpretation
Parse the summary line:
test result: ok. 5 passed; 1 failed; 2 ignored; 0 measured; 3 filtered out (4124ms)
- passed (5): Tests that succeeded
- failed (1): Tests that threw exceptions
-
ignored (2): Tests skipped via
ignore: true -
filtered out (3): Tests excluded by
--filterflag
Timing information: Per-test timing (in milliseconds) and total suite time help identify slow tests.
4. Writing Effective Tests
4.1 Test Case Structure
4.1.1 Arrange-Act-Assert Pattern
The AAA structure:
- Arrange: Set up test data and preconditions
- Act: Execute the code under test
- Assert: Verify the results
Practical example:
Deno.test('user creation', async () => {
// Arrange
const userData = { name: 'Test User', email: 'test@example.com' };
// Act
const user = await createUser(userData);
// Assert
assertExists(user.id);
assertEquals(user.name, 'Test User');
});
Recommendation: Use comments or blank lines to clearly separate phases for improved readability.
4.1.2 Test Naming Conventions
Use descriptive names that describe the scenario and expected outcome:
// Good: Describes scenario and outcome
Deno.test('getLongUuid: throws error when length is 0', ...);
Deno.test('user service: returns null for non-existent user', ...);
// Bad: Generic and unhelpful
Deno.test('test1', ...);
Deno.test('works', ...);
Consistent patterns:
-
"function name: scenario"→"getLongUuid: len is 100000" -
"should do X when Y"→"should throw error when input is empty"
4.1.3 Test Isolation Principles
Ensure tests are independent:
// Bad: Tests share state
let counter = 0;
Deno.test('increment', () => { counter++; assertEquals(counter, 1); });
Deno.test('increment again', () => { counter++; assertEquals(counter, 2); }); // Brittle!
// Good: Each test creates its own state
Deno.test('increment', () => {
let counter = 0;
counter++;
assertEquals(counter, 1);
});
Avoid ordering dependencies: Tests may run in any order (especially with --jobs).
Clean up after tests:
Deno.test('database test', async () => {
const userId = await createTestUser();
try {
// Test code
} finally {
await deleteTestUser(userId); // Cleanup
}
});
4.2 Positive Test Cases
4.2.1 Testing Expected Outputs
Verify return values with assertEquals:
Deno.test('addition returns correct sum', () => {
const result = add(2, 3);
assertEquals(result, 5);
});
Check data structures with assertObjectMatch:
Deno.test('user has expected properties', () => {
const user = getUser(1);
assertObjectMatch(user, { id: 1, role: 'admin' });
// Extra properties (like 'email') are allowed
});
4.2.2 Validating Return Values
Test primitive returns:
assertEquals(getStatus(), 'active'); // string
assertEquals(getCount(), 42); // number
assertEquals(isEnabled(), true); // boolean
Test object returns:
const result = getData();
assertExists(result.a);
assertExists(result.a.c.d);
assertStringIncludes(result.a.c.d, "deno");
Test array returns:
const items = getItems();
assertEquals(items.length, 3);
assertArrayIncludes(items, ['apple', 'banana']);
4.2.3 Checking State Changes
Verify database mutations:
Deno.test('creates user in database', async () => {
await createUser({ name: 'Test' });
const { data } = await supabase.from('users').select().eq('name', 'Test');
assertEquals(data.length, 1);
});
Verify API calls with spies (covered in Section 10).
4.3 Negative Test Cases
4.3.1 Testing Error Conditions
Use assertThrows for sync errors:
import { assertThrows } from 'jsr:@std/assert@1';
Deno.test('throws on invalid input', () => {
assertThrows(
() => validateInput(-1),
Deno.errors.InvalidData,
'must be positive'
);
});
Use assertThrowsAsync for async errors:
import { assertThrowsAsync } from 'jsr:@std/assert@1';
Deno.test('async function throws', async () => {
await assertThrowsAsync(
async () => await fetchUser('invalid-id'),
Error,
'User not found'
);
});
4.3.2 Boundary Value Testing
Test edge cases:
// From source documentation - testing UUID length boundaries
Deno.test('len is 0 throws', () => {
assertThrows(() => getLongUuid(0), Deno.errors.InvalidData);
});
Deno.test('len is 6 throws (max is 5)', () => {
assertThrows(() => getLongUuid(6), Deno.errors.InvalidData);
});
Deno.test('len is 1 (minimum valid)', () => {
assert(getLongUuid(1).length === 36); // Passes
});
Deno.test('len is 5 (maximum valid)', () => {
assert(getLongUuid(5).length === 36 * 5 + 4); // Passes
});
4.3.3 Invalid Input Handling
Test null and undefined:
Deno.test('handles null input', () => {
assertThrows(() => processData(null), TypeError);
});
Test wrong types:
Deno.test('rejects string where number expected', () => {
assertThrows(() => calculate('not a number'), TypeError);
});
4.4 Test Suites Organization
4.4.1 Grouping Related Tests
Use Deno.test steps for sub-tests:
Deno.test('User CRUD operations', async (t) => {
let userId: string;
await t.step('Create user', async () => {
userId = await createUser({ name: 'Test' });
assertExists(userId);
});
await t.step('Read user', async () => {
const user = await getUser(userId);
assertEquals(user.name, 'Test');
});
await t.step('Delete user', async () => {
await deleteUser(userId);
const user = await getUser(userId);
assertEquals(user, null);
});
});
4.4.2 One Test File Per Source Module
Match test files to source files:
src/
├── user.ts → test/user_test.ts
├── auth.ts → test/auth_test.ts
└── database.ts → test/database_test.ts
Import the module under test:
// test/user_test.ts
import { createUser, getUser, deleteUser } from '../src/user.ts';
4.4.3 Shared Setup and Teardown
Create setup helper functions:
// test/helpers.ts
export async function setupTestUser() {
return await supabase.auth.admin.createUser({
email: `test-${crypto.randomUUID()}@example.com`,
email_confirm: true
});
}
Use try/finally for teardown:
Deno.test('user operations', async () => {
const { user } = await setupTestUser();
try {
// Test code
} finally {
await supabase.auth.admin.deleteUser(user.id);
}
});
5. Assertions Library Deep Dive
5.1 Importing Assertions
5.1.1 Individual Import Strategy
Import specific assertions:
import { assert, assertEquals, assertThrows } from 'jsr:@std/assert@1';
Benefits: Clear documentation of which assertions are used; smaller bundle conceptually.
Commonly used assertions:
-
assert- Verify truthy values -
assertEquals- Deep equality comparison -
assertThrows- Verify exceptions -
assertExists- Check not null/undefined
5.1.2 Namespace Import Strategy
Import all assertions:
import * as asserts from 'jsr:@std/assert@1';
// Use with namespace prefix
asserts.assertEquals(result, expected);
asserts.assertExists(user);
Use qualified names for clarity when many assertions are used.
5.1.3 Version Pinning Best Practices
Pin to specific versions for reproducibility:
import { assertEquals } from 'jsr:@std/assert@1.0.0'; // Exact version
import { assertEquals } from 'jsr:@std/assert@1'; // Latest 1.x
Update intentionally: Review changelogs before upgrading assertion library versions.
5.2 Basic Assertions
5.2.1 assert() Function
5.2.1.1 Expression Evaluation
Signature: assert(expr: unknown, msg?: string)
Pass on truthy values: Non-null, non-zero, non-empty values pass:
assert(1); // Passes
assert("hello"); // Passes
assert([1, 2, 3]); // Passes
assert({}); // Passes (empty object is truthy)
Fail on falsy values: false, 0, '', null, undefined fail:
assert(0); // AssertionError
assert(""); // AssertionError
assert(null); // AssertionError
5.2.1.2 Custom Error Messages
Provide meaningful messages:
assert(count === expected, `Expected ${expected} but got ${count}`);
Default message: Omitting the second parameter uses an empty error message.
5.2.2 assertEquals() Function
5.2.2.1 Deep Comparison Behavior
Recursive comparison: Objects and arrays are compared deeply:
assertEquals({ a: { b: 1 } }, { a: { b: 1 } }); // Passes
assertEquals([1, [2, 3]], [1, [2, 3]]); // Passes
Compare by value, not reference: Two identical objects pass even if different instances.
5.2.2.2 Type Flexibility
Accept any types:
assertEquals(10, 10); // number
assertEquals("abc", "abc"); // string
assertEquals(true, true); // boolean
assertEquals({ a: 10 }, { a: 10 }); // object
assertEquals(new Date(2021, 6, 1), new Date(2021, 6, 1)); // Date
5.2.2.3 Pretty Diff Output
Diff format on failure:
AssertionError: Values are not equal:
[Diff] Actual / Expected
- 2021-07-01T07:00:00.000Z
+ 2021-07-02T07:00:00.000Z
The - prefix indicates actual value; + indicates expected value.
5.2.3 assertNotEquals() Function
Inverse behavior: Passes when values differ, fails when they match:
assertNotEquals({ a: 10 }, { a: 11 }); // Passes
assertNotEquals(10, 10); // AssertionError
Note: Does not produce pretty diff output.
5.3 Strict Equality Assertions
5.3.1 assertStrictEquals() Function
5.3.1.1 Reference Checking
Reference equality: Objects must be the same instance:
const xs = [1, 2, 3];
const ys = xs; // Same reference
const zs = [1, 2, 3]; // Different reference, same content
assertStrictEquals(xs, ys); // Passes - same reference
assertStrictEquals(xs, zs); // Fails - different references
Error message:
AssertionError: Values have the same structure but are not reference-equal
5.3.1.2 Primitive Type Behavior
Primitives use value comparison (equivalent to ===):
assertStrictEquals(10, 10); // Passes
assertStrictEquals(1, '1'); // Fails - different types
assertStrictEquals(true, true); // Passes
5.3.2 assertNotStrictEquals() Function
Inverse: Passes when references differ:
const x = { a: 1 };
const y = { a: 1 }; // Same structure, different reference
const z = x; // Same reference
assertNotStrictEquals(x, y); // Passes - different references
assertNotStrictEquals(x, z); // Fails - same reference
5.4 Existence and Type Assertions
5.4.1 assertExists() Function
5.4.1.1 Null Check
Rejects null:
assertExists(null); // AssertionError: Expected actual to exist
5.4.1.2 Undefined Check
Rejects undefined:
let x: string | undefined;
assertExists(x); // AssertionError - x is undefined
Distinguish from falsy: 0, '', and false pass (they exist):
assertExists(0); // Passes
assertExists(''); // Passes
assertExists(false); // Passes
5.5 String Assertions
5.5.1 assertStringIncludes() Function
Substring checking:
assertStringIncludes("Welcome to Deno!", "Deno"); // Passes
assertStringIncludes("Hello", "world"); // Fails
Use for partial matching: Validate error messages or generated content.
5.5.2 assertMatch() Function (Regex)
Regex testing:
assertMatch("user-123", /user-\d+/); // Passes
assertMatch("test@email.com", /.*@.*/); // Passes
assertMatch("no numbers", /\d+/); // Fails
Use for format validation: UUIDs, emails, dates, etc.
5.5.3 assertNotMatch() Function
Inverse behavior: Passes when regex doesn't match:
assertNotMatch("hello world", /\d+/); // Passes - no digits
assertNotMatch("hello 123", /\d+/); // Fails - contains digits
5.6 Collection Assertions
5.6.1 assertArrayIncludes() Function
5.6.1.1 Element Presence Checking
Both parameters are arrays:
const arr = [1, 2, 3, 4, 5];
assertArrayIncludes(arr, [2, 4]); // Passes - both present
assertArrayIncludes(arr, [4, 2]); // Passes - order doesn't matter
assertArrayIncludes(arr, [6]); // Fails - 6 not in array
Single element checking: Wrap in array:
assertArrayIncludes(arr, [3]); // Check if 3 is present
5.6.1.2 Deep Equality for Objects
Objects compared by value:
const users = [{ id: 1 }, { id: 2 }];
assertArrayIncludes(users, [{ id: 1 }]); // Passes
Typed arrays supported:
assertArrayIncludes(Uint8Array.from([1, 2, 3, 4]), Uint8Array.from([1, 2]));
5.6.2 assertObjectMatch() Function
5.6.2.1 Deep Object Comparison
Partial matching: First object must contain second object's structure:
const response = { id: 1, name: "Test", timestamp: Date.now() };
assertObjectMatch(response, { id: 1, name: "Test" }); // Passes
// Extra 'timestamp' property is allowed
5.6.2.2 Partial Matching Behavior
Extra properties allowed:
assertObjectMatch({ a: 1, b: 2, c: 3 }, { a: 1 }); // Passes
Missing properties fail:
assertObjectMatch({ a: 1 }, { a: 1, b: 2 }); // Fails - 'b' missing
5.7 Error Assertions
5.7.1 assertThrows() Function
5.7.1.1 Sync Function Testing
Wrapper function syntax:
assertThrows(
() => getLongUuid(0), // Function that should throw
Deno.errors.InvalidData // Expected error type
);
Pass on exception: Throwing the expected error type causes pass.
Fail on no exception: Normal return causes AssertionError.
5.7.1.2 Error Class Matching
Specify error class:
assertThrows(() => parseConfig(null), Deno.errors.InvalidData);
assertThrows(() => divide(1, 0), RangeError);
assertThrows(() => JSON.parse('invalid'), SyntaxError);
5.7.1.3 Error Message Validation
Use msgIncludes parameter:
assertThrows(
() => validateAge(-1),
RangeError,
"must be positive", // Error message must include this
"Age validation should reject negative values" // Test failure message
);
5.7.2 assertThrowsAsync() Function
5.7.2.1 Async Function Testing
Async wrapper:
await assertThrowsAsync(
async () => await fetchData("invalid-url"),
Error,
"network error"
);
Must await the assertion.
5.7.2.2 Promise Rejection Handling
Rejections treated as thrown errors:
await assertThrowsAsync(
async () => await Promise.reject(new Error("Failed")),
Error,
"Failed"
);
6. Advanced Testing Features
6.1 Parallel Execution
6.1.1 The --jobs Parameter
6.1.1.1 Worker-Based Parallelism
Configure workers:
deno test --jobs 4 # Run up to 4 test files simultaneously
Workers run in isolated execution contexts. Output from different files interleaves.
6.1.1.2 Suite-Level (Not Test-Level) Parallelism
Important clarification:
"It's important to note that the test runner doesn't go inside a file and run the tests cases in parallel. Instead, it runs the suites in parallel."
Design for parallelism by splitting tests across multiple files to benefit from --jobs.
6.1.2 Performance Benefits
Quantified speedup from source documentation:
Sequential (2 files): ~19 seconds
Parallel --jobs 2: ~9.7 seconds (49% reduction)
"It takes almost half the time! This would significantly reduce the CI/CD time when there is a big test suite to execute."
6.1.3 Parallelism Considerations
Handle shared resources: Database tests may conflict if writing to same rows. Use namespace isolation (covered in Section 11).
Avoid port conflicts: Services binding to the same port will fail in parallel execution.
6.2 Conditional Test Execution
6.2.1 The ignore Option
6.2.1.1 Environment-Based Skipping
Deno.test({
name: "slow integration test",
ignore: Deno.env.get('QUICK_TEST') ? true : false,
fn: async () => { /* test code */ }
});
Skip integration tests by default:
Deno.test({
name: "database integration",
ignore: !Deno.env.get('TEST_TYPE_INTEG'), // Skip unless integration mode
fn: async () => { /* ... */ }
});
6.2.1.2 Platform-Specific Tests
Deno.test({
name: "Windows-specific test",
ignore: Deno.build.os !== "windows",
fn: () => { /* ... */ }
});
6.2.2 The only Option
6.2.2.1 Focused Test Execution
Deno.test({
name: "debug this test",
only: true, // Run only this test
fn: () => { /* ... */ }
});
Use during development to isolate failing tests.
6.2.2.2 Warning About only in CI
CI fails if only is present:
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 5 filtered out
FAILED because the "only" option was used
This safety feature prevents accidentally deploying code that runs only one test.
6.2.3 The --filter Flag
6.2.3.1 Name-Based Filtering
Filter by substring:
deno test --filter "user" # Run tests with "user" in name
deno test --filter "database" # Run tests with "database" in name
6.2.3.2 Running Test Subsets
From source documentation:
deno test --filter 100 a_test.ts
# Output:
# running 2 tests from file:///Users/.../a_test.ts
# test len is 100000 ... ok (400ms)
# test len is 1000000 ... ok (3666ms)
# test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 3 filtered out
6.3 Fail-Fast Mode
6.3.1 The --fail-fast Flag
deno test --fail-fast
Stops at first failure with immediate error details. Useful for rapid iteration during development.
6.3.2 Use Cases for Early Termination
- Development: Quick identification of failures during TDD
- Debugging: Focus on first failing test without noise
- CI consideration: May want full reports instead—disable for complete results
6.4 Resource Sanitization
6.4.1 sanitizeOps Option
Deno tracks async operations. Leaked operations (unresolved promises) fail tests by default.
Deno.test({
name: "my test",
sanitizeOps: false, // Disable operation leak checking
fn: () => { /* ... */ }
});
6.4.2 sanitizeResources Option
Tracks file handles, network connections, etc.
Deno.test({
name: "my test",
sanitizeResources: false, // Disable resource leak checking
fn: () => { /* ... */ }
});
Warning: Disabling masks real issues. Fix leaks properly with try/finally.
6.4.3 sanitizeExit Option
Detects calls to Deno.exit():
Deno.test({
name: "test exit behavior",
sanitizeExit: false, // Allow Deno.exit() in test
fn: () => { /* ... */ }
});
6.4.4 Debugging Resource Leaks
Identify leaked resources from sanitizer error messages:
AssertionError: Test case is leaking resources.
- file (rid 3)
Fix leaks systematically:
- Close file handles:
file.close() - Clear timers:
clearTimeout(timer) - Close connections:
conn.close()
7. Testing Supabase Edge Functions
7.1 Understanding Edge Functions Architecture
7.1.1 Serverless Computing Model
Supabase Edge Functions run on Deno at the edge, close to users globally. From the documentation:
"Supabase Edge Functions are an integral part of the Supabase ecosystem, empowering developers to extend the functionality of their applications with serverless computing."
Benefits:
- Automatic scaling based on demand
- Reduced latency through edge deployment
- No server infrastructure management
7.1.2 TypeScript Support
Native TypeScript without compilation:
"Edge Functions can be written in TypeScript, a statically-typed superset of JavaScript. TypeScript brings type safety to your code, enabling early detection of potential errors."
7.1.3 Supabase Client Integration
Seamless integration with Supabase client:
"Edge Functions seamlessly integrates with the Supabase client, enabling easy communication with your Supabase backend."
7.2 Local Development Setup
7.2.1 Starting the Supabase Server
supabase start
Output includes:
- API URL:
http://localhost:54321 - anon key: JWT for anonymous access
- service_role key: JWT for admin access
7.2.2 Serving Edge Functions Locally
supabase functions serve
This starts a local server with hot-reloading for Edge Functions.
7.2.3 Environment Configuration
7.2.3.1 SUPABASE_URL Configuration
# .env.local
SUPABASE_URL=http://localhost:54321
7.2.3.2 SUPABASE_ANON_KEY Configuration
# .env.local (demo key for local development)
SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0
Key roles:
- anon: Public access, subject to RLS policies
- service_role: Admin access, bypasses RLS
7.3 Writing Edge Function Tests
7.3.1 Supabase Client Setup
7.3.1.1 Client Configuration Options
import { createClient, SupabaseClient } from 'npm:@supabase/supabase-js@2';
import 'jsr:@std/dotenv/load';
const supabaseUrl = Deno.env.get('SUPABASE_URL') ?? '';
const supabaseKey = Deno.env.get('SUPABASE_ANON_KEY') ?? '';
const options = {
auth: {
autoRefreshToken: false,
persistSession: false,
detectSessionInUrl: false,
},
};
const client: SupabaseClient = createClient(supabaseUrl, supabaseKey, options);
7.3.1.2 Authentication Options
- autoRefreshToken: false — Prevent background token refresh for test stability
- persistSession: false — Avoid localStorage usage for test isolation
- detectSessionInUrl: false — Prevent URL parsing for clean test contexts
7.3.2 Testing Database Connectivity
const testClientCreation = async () => {
if (!supabaseUrl) throw new Error('supabaseUrl is required.');
if (!supabaseKey) throw new Error('supabaseKey is required.');
const { data: table_data, error: table_error } = await client
.from('my_table')
.select('*')
.limit(1);
if (table_error) {
throw new Error('Invalid Supabase client: ' + table_error.message);
}
assert(table_data, 'Data should be returned from the query.');
};
Deno.test('Client Creation Test', testClientCreation);
7.3.3 Testing Function Invocation
7.3.3.1 Using functions.invoke()
const { data, error } = await client.functions.invoke('hello-world', {
body: { name: 'TestUser' }
});
7.3.3.2 Request Body Construction
Pass JSON-serializable data in the body property. Include all required fields per your function's schema.
7.3.3.3 Response Validation
const testHelloWorld = async () => {
const { data: func_data, error: func_error } = await client.functions.invoke('hello-world', {
body: { name: 'bar' }
});
if (func_error) {
throw new Error('Invalid response: ' + func_error.message);
}
console.log(JSON.stringify(func_data, null, 2)); // Debug output
assertEquals(func_data.message, 'Hello bar!');
};
Deno.test('Hello-world Function Test', testHelloWorld);
7.4 CORS Handling in Tests
7.4.1 Understanding CORS Headers
Required headers for Edge Functions:
const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type",
};
7.4.2 OPTIONS Request Handling
Preflight request handler:
if (req.method === 'OPTIONS') {
return new Response('ok', { headers: corsHeaders });
}
7.4.3 Testing CORS Compliance
Deno.test('OPTIONS returns CORS headers', async () => {
const res = await fetch('http://localhost:54321/functions/v1/hello-world', {
method: 'OPTIONS'
});
assertEquals(res.status, 200);
assertExists(res.headers.get('Access-Control-Allow-Origin'));
});
8. Integration Testing
8.1 Integration vs Unit Testing
8.1.1 Defining Integration Tests
Integration tests verify interactions between components—ensuring modules work correctly together. Unlike unit tests that isolate individual functions, integration tests exercise the system as a whole.
From the documentation:
"There are three entities in integration testing: Tester (test runner), SUT (system under test running as a service), and Other services (dependencies)."
8.1.2 When to Use Integration Tests
- Test API endpoints: Verify HTTP request/response cycles
- Test database interactions: Verify queries, constraints, triggers
- Test service communication: Verify external API integrations
8.1.3 Test Pyramid Considerations
- More unit tests, fewer integration tests (pyramid shape)
- Integration tests are slower—budget time accordingly
- Focus on critical paths and high-risk integrations
8.2 Integration Test Architecture
8.2.1 Tester-SUT-Services Model
┌──────────┐ HTTP ┌──────────┐ depends ┌──────────────┐
│ Tester │ ──────────── │ SUT │ ────────────── │ Services │
│ │ requests │ │ │ (real/mock) │
└──────────┘ └──────────┘ └──────────────┘
8.2.2 Local vs Remote SUT
-
Local: Use
supabase startfor local testing - Staging: Configure environment variables to point to staging URLs
8.2.3 Mock Services Configuration
Decide whether to use mocks or real services for dependencies based on test requirements (covered in Section 12).
8.3 Marking Tests by Type
8.3.1 Environment Variable Based Marking
// Unit test - runs by default
Deno.test({
name: "unit test",
ignore: Deno.env.get('TEST_TYPE_INTEG') ? true : false,
fn: () => { /* unit test code */ }
});
// Integration test - requires explicit opt-in
Deno.test({
name: "integration test",
ignore: Deno.env.get('TEST_TYPE_INTEG') ? false : true,
fn: async () => { /* integration test code */ }
});
8.3.2 Separating Unit and Integration Suites
Alternative: Use separate directories:
tests/
├── unit/
│ ├── user_test.ts
│ └── auth_test.ts
└── integration/
├── api_test.ts
└── database_test.ts
Run selectively:
deno test tests/unit/ # Unit tests only
deno test tests/integration/ # Integration tests only
8.3.3 Conditional Execution Patterns
Default to unit tests (no special configuration needed):
deno test --allow-env # Runs unit tests, skips integration
Opt-in integration tests:
export TEST_TYPE_INTEG=1
deno test --allow-env --allow-net # Runs integration tests, skips unit
8.4 Writing Integration Test Cases
8.4.1 HTTP-Based Test Structure
8.4.1.1 Fetch Request Construction
const res = await fetch('http://localhost:5000/data', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ key: 'value' })
});
8.4.1.2 Response Awaiting
const res = await fetch('http://localhost:5000/data');
const data = await res.json(); // or res.text() for non-JSON
8.4.1.3 Multi-Assert Validation
assert(res.ok === true);
assert(res.status === 200);
assert(res.redirected === false);
assertExists(res.headers.get('content-type'));
assertExists(data);
8.4.2 Testing Different HTTP Methods
8.4.2.1 GET Request Testing
Deno.test("GET /data returns JSON", async () => {
const res = await fetch('http://localhost:5000/data');
const data = await res.json();
assert(res.ok);
assertEquals(res.status, 200);
assertExists(data);
assertStringIncludes(data.a.c.d, "deno");
});
8.4.2.2 POST Request Testing
Deno.test("POST /data creates record", async () => {
const res = await fetch('http://localhost:5000/data', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'test' })
});
assertEquals(res.status, 201);
});
8.4.2.3 Error Status Code Validation
Deno.test("GET / returns 404", async () => {
const res = await fetch('http://localhost:5000');
assert(res.ok === false);
assert(res.status === 404);
});
Deno.test("POST / returns 405 Method Not Allowed", async () => {
const res = await fetch('http://localhost:5000', { method: 'POST' });
assert(res.status === 405);
});
8.5 Running Integration Tests
8.5.1 Starting the Service Under Test
# Terminal 1: Start the server in background
deno run --allow-net --unstable myServer.ts &
8.5.2 Setting Environment Variables
# Terminal 2: Enable integration tests
export TEST_TYPE_INTEG=1
8.5.3 Executing the Test Suite
deno test --allow-env --allow-net myServer_test.ts
Expected output:
running 6 tests from file:///Users/.../myServer_test.ts
test unit test - no numRids ... ignored (0ms)
test unit test - numRids=10 ... ignored (0ms)
test Integration test: GET / ... ok (31ms)
test Integration test: POST / ... ok (4ms)
test Integration test: GET /data ... ok (7ms)
test Integration test: GET /data numRids=100 ... ok (6ms)
test result: ok. 4 passed; 0 failed; 2 ignored; 0 measured; 0 filtered out (75ms)
9. CI/CD Integration
9.1 Processing Test Results
9.1.1 Limitations of Console Output
From the documentation:
"Deno's test runner comes with a number of useful features. However, it has some limitations as well. The biggest one is that the results are always produced in textual format and goes only to the console."
This creates challenges for CI/CD pipelines that need structured data.
9.1.2 Programmatic Result Processing
Workaround: Capture output via child processes and parse with regex.
9.2 Building a Test Executor
9.2.1 Child Process Execution
9.2.1.1 Using Deno.run()
const p = Deno.run({
cmd: ["deno", "test", "--allow-all"],
stdout: 'piped',
stderr: 'piped',
stdin: 'null'
});
const status = await p.status();
const output = await p.output();
9.2.1.2 Piping stdout and stderr
Configure stdout: 'piped' to capture output. Use TextDecoder to convert bytes to string:
const rawOutput = await p.output();
const textOutput = new TextDecoder().decode(rawOutput);
9.2.2 Basic Output Processing
9.2.2.1 Status Object (success, code)
const status = await p.status();
// status.success: boolean - true if all tests passed
// status.code: number - exit code (0 = success)
9.2.2.2 Raw Output Capture
import { stripColor } from "jsr:@std/fmt/colors";
const out: any = {};
out.status = status.success;
if (!status.success) {
out.log = stripColor(new TextDecoder().decode(await p.output()));
}
console.log(out);
// { status: true } or { status: false, log: "..." }
9.2.3 JSON Output Generation
9.2.3.1 Basic JSON Structure
const out = { status: s.success };
if (!s.success) {
out.log = stripColor(new TextDecoder().decode(await p.output()));
}
console.log(JSON.stringify(out));
9.2.3.2 Summarized JSON Output
Parse the summary line with regex:
const tkns = l.matchAll(
/test\sresult:\s(.*)(\d+)\spassed;\s(\d+)\sfailed;\s(\d+)\signored;\s(\d+)\smeasured;\s(\d+)\sfiltered\sout\s\((\d+)ms\)/gm
);
const tknsArr = Array.from(tkns)[0];
if (tknsArr) {
out.passed = Number(tknsArr[2]);
out.failed = Number(tknsArr[3]);
out.ignored = Number(tknsArr[4]);
out.measured = Number(tknsArr[5]);
out.filteredOut = Number(tknsArr[6]);
out.totalTimeTaken = Number(tknsArr[7]);
}
9.2.3.3 Detailed JSON with Test Lists
{
passedCases: ["test A", "test B"],
failedCases: ["test C"],
status: false,
passed: 2,
failed: 1,
totalTimeTaken: 4561,
errorLog: "..."
}
9.3 Parsing Test Results
9.3.1 Regex-Based Parsing
Pattern for individual tests:
const tkns = l.matchAll(/test\s(.*)\s\.\.\.\s(.*)\s.*/gm);
const tknsArr = Array.from(tkns)[0];
if (tknsArr) {
if (tknsArr[2] === "ok") {
out.passedCases.push(tknsArr[1]);
} else {
out.failedCases.push(tknsArr[1]);
}
}
9.3.2 Extracting Pass/Fail Counts
Use Number() to convert matched strings to integers:
out.passed = Number(tknsArr[2]);
out.failed = Number(tknsArr[3]);
9.3.3 Capturing Error Logs
Split on failures marker:
out.errorLog = decOutput.split("failures:")[1];
9.4 GitHub Actions Configuration
9.4.1 Workflow File Structure
name: Deno Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: denoland/setup-deno@v1
with:
deno-version: v1.x
- uses: supabase/setup-cli@v1
with:
version: latest
9.4.2 Installing Dependencies
The setup-deno and setup-cli actions install Deno and Supabase CLI respectively.
9.4.3 Running Supabase Stack in CI
- name: Start Supabase (Lightweight)
run: supabase start -x realtime,storage-api,imgproxy,studio
Exclude unnecessary services to reduce boot time.
9.4.4 Executing Tests with Proper Permissions
- name: Run Tests
run: deno test --allow-all supabase/functions/tests/
env:
SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }}
10. Mocking and Dependency Injection
10.1 Architectural Patterns for Testability
10.1.1 The Humble Handler Pattern
10.1.1.1 Thin Entry Points
Anti-pattern (coupled code):
// index.ts - Hard to test
Deno.serve(async (req) => {
const supabase = createClient(Deno.env.get('SUPABASE_URL')!, ...);
const { data } = await supabase.from('users').select('*');
const response = await fetch('https://api.stripe.com/...');
return new Response("Done");
});
Problem: Cannot inject mock clients for testing.
10.1.1.2 Extracted Business Logic
Recommended pattern:
// core.ts - Testable business logic
export interface Dependencies {
supabase: SupabaseClient;
httpClient: (url: string, init?: RequestInit) => Promise<Response>;
config: { stripeKey: string };
}
export async function processRequest(
req: Request,
deps: Dependencies
): Promise<Response> {
const { data } = await deps.supabase.from('users').select('*');
const response = await deps.httpClient('https://api.stripe.com/...', {
headers: { Authorization: `Bearer ${deps.config.stripeKey}` }
});
return new Response("Done");
}
// index.ts - Thin handler
Deno.serve(async (req) => {
const deps = {
supabase: createClient(...),
httpClient: globalThis.fetch.bind(globalThis),
config: { stripeKey: Deno.env.get('STRIPE_KEY')! }
};
return processRequest(req, deps);
});
10.1.2 Functional Core, Imperative Shell
10.1.2.1 Pure Functions for Logic
Write functions with no side effects:
function calculateDiscount(price: number, tier: string): number {
if (tier === 'premium') return price * 0.2;
if (tier === 'basic') return price * 0.1;
return 0;
}
Test with simple assertions—no mocking needed.
10.1.2.2 Side Effects at the Boundary
Push I/O to handler level:
// Handler does I/O
const userData = await db.getUser(userId);
const discount = calculateDiscount(userData.price, userData.tier); // Pure
await db.applyDiscount(userId, discount); // I/O
10.1.3 Service Layer Extraction
10.1.3.1 Hexagonal Architecture Concepts
Define interfaces as ports:
interface PaymentService {
createCharge(amount: number, customerId: string): Promise<ChargeResult>;
}
Implement adapters for real and mock services:
class StripePaymentService implements PaymentService {
async createCharge(amount, customerId) { /* real Stripe call */ }
}
class MockPaymentService implements PaymentService {
async createCharge(amount, customerId) { return { id: 'mock_charge' }; }
}
10.1.3.2 Semantic Mocking Benefits
Mock at service level rather than HTTP level:
// Semantic: Test business logic
mockPaymentService.createCharge(1000, 'cust_123');
// vs Low-level: Test HTTP implementation details
fetch('https://api.stripe.com/v1/charges', { ... });
10.2 Dependency Injection Techniques
10.2.1 Constructor Injection
class OrderService {
constructor(
private db: DatabaseService,
private payment: PaymentService
) {}
async createOrder(data: OrderData) {
await this.db.save(data);
await this.payment.createCharge(data.amount, data.customerId);
}
}
10.2.2 Interface-Based Dependencies
export interface Dependencies {
supabase: SupabaseClient;
httpClient: typeof fetch;
config: { stripeKey: string };
}
export async function processRequest(req: Request, deps: Dependencies) {
// Use deps.supabase, deps.httpClient, etc.
}
10.2.3 Configuration Objects
const config = {
stripeKey: Deno.env.get('STRIPE_KEY')!,
timeout: Number(Deno.env.get('TIMEOUT') ?? '5000'),
debug: Deno.env.get('DEBUG') === 'true'
};
// Override in tests
const testConfig = { ...config, stripeKey: 'test_key', debug: true };
10.3 Mocking the Fetch API
10.3.1 Using stub() from @std/testing/mock
10.3.1.1 Stubbing globalThis.fetch
import { stub } from "jsr:@std/testing/mock";
const fetchStub = stub(globalThis, "fetch", (url) => {
return Promise.resolve(new Response(JSON.stringify({ success: true })));
});
// Run test code that uses fetch()
fetchStub.restore(); // Always restore!
10.3.1.2 Restoring Original Behavior
Use try/finally for guaranteed restoration:
const fetchStub = stub(globalThis, "fetch", ...);
try {
await runTestCode();
} finally {
fetchStub.restore();
}
10.3.2 Pattern-Based Mocking
10.3.2.1 URL Pattern Matching
const fetchStub = stub(globalThis, "fetch", (input) => {
const url = input.toString();
if (url.includes("api.stripe.com")) {
return Promise.resolve(new Response(JSON.stringify({ id: "ch_123" })));
}
if (url.includes("api.sendgrid.com")) {
return Promise.resolve(new Response(JSON.stringify({ sent: true })));
}
return Promise.reject(new Error("Unknown URL: " + url));
});
10.3.2.2 Returning Controlled Responses
new Response(
JSON.stringify({ data: "mock" }),
{
status: 200,
headers: { "Content-Type": "application/json" }
}
);
10.4 Spying on Function Calls
10.4.1 Creating Spy Functions
import { spy } from "jsr:@std/testing/mock";
const mockStripe = {
createCheckoutSession: spy(async (priceId, customerId) => {
return { id: "cs_123", url: "https://checkout.stripe.com/..." };
})
};
10.4.2 Verifying Call Count (assertSpyCalls)
import { assertSpyCalls } from "jsr:@std/testing/mock";
// After running code that should call the function
assertSpyCalls(mockStripe.createCheckoutSession, 1); // Called exactly once
10.4.3 Inspecting Call Arguments
// Check what arguments were passed
assertEquals(mockStripe.createCheckoutSession.calls[0].args[0], "price_premium");
assertEquals(mockStripe.createCheckoutSession.calls[0].args[1], "cust_123");
10.5 Mocking the Supabase Client
10.5.1 When to Mock vs Use Real Client
- Mock for unit tests: Fast, isolated, no database needed
- Real for integration tests: Verify actual SQL, constraints, RLS
10.5.2 Creating Mock Client Objects
const mockSupabase = {
from: (table: string) => ({
select: () => ({
eq: () => ({
single: () => Promise.resolve({
data: { id: 1, name: 'Test User' },
error: null
})
})
}),
insert: (data: any) => Promise.resolve({ data, error: null })
})
};
10.5.3 Simulating Database Responses
Match the { data, error } response pattern:
// Successful response
{ data: [{ id: 1 }, { id: 2 }], error: null }
// Error response
{ data: null, error: { message: 'Row not found' } }
11. Database Testing and Isolation
11.1 Database Isolation Strategies
11.1.1 Transaction Rollback Method
11.1.1.1 BEGIN...ROLLBACK Wrapping
BEGIN;
-- Test operations here
INSERT INTO users (name) VALUES ('Test');
SELECT * FROM users WHERE name = 'Test';
ROLLBACK; -- Undoes all changes
Perfect isolation: Database returns to original state.
11.1.1.2 Limitations with HTTP Clients
The Supabase JavaScript client uses HTTP requests through PostgREST, which are stateless. You cannot maintain a transaction across multiple HTTP requests.
Recommendation: Use namespace isolation for Supabase JS tests.
11.1.2 Namespace Isolation Method
11.1.2.1 Unique ID Generation
const userId = crypto.randomUUID();
const email = `test-${userId}@example.com`;
Every test generates unique identifiers.
11.1.2.2 Scoped Data Creation
All test data links to the unique user:
await supabase.from('orders').insert({
user_id: userId, // Scoped to this test
amount: 100
});
11.1.2.3 Cleanup with service_role
Use the admin client to delete test users:
// service_role bypasses RLS
await adminClient.auth.admin.deleteUser(userId);
With ON DELETE CASCADE foreign keys, related data is automatically cleaned.
11.1.3 Truncation Method
11.1.3.1 DELETE FROM Approach
await supabase.from('orders').delete().neq('id', 0); // Delete all
await supabase.from('users').delete().neq('id', 0);
Problems:
- Slow for large tables
- Cannot run tests in parallel
- Risk of running against production
11.1.3.2 Risks and Limitations
Prefer namespace isolation for most scenarios.
11.1.4 Docker Per Test Method
Ultimate isolation but impractical:
- Docker startup takes seconds per test
- High resource usage
- Not suitable for regular development
11.2 Implementing Namespace Isolation
11.2.1 Unique User Creation
const userId = crypto.randomUUID();
const email = `test-${userId}@example.com`;
await adminClient.auth.admin.createUser({
id: userId,
email,
email_confirm: true
});
11.2.2 Cascading Deletes with Foreign Keys
Configure in database schema:
CREATE TABLE orders (
id UUID PRIMARY KEY,
user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE
);
Deleting the user automatically deletes their orders.
11.2.3 Teardown Patterns
Deno.test("User operations", async (t) => {
const userId = crypto.randomUUID();
// ... setup and test steps ...
await t.step("Teardown", async () => {
const { error } = await adminClient.auth.admin.deleteUser(userId);
if (error) console.error("Cleanup failed:", error);
});
});
11.3 Database Seeding with Factories
11.3.1 The Factory Pattern
const Factory = {
createUser: async (opts: { plan?: string } = {}) => {
const id = crypto.randomUUID();
await supabase.from('users').insert({
id,
plan: opts.plan ?? 'free'
});
return { id };
},
createOrder: async (opts: { userId: string; amount: number }) => {
const id = crypto.randomUUID();
await supabase.from('orders').insert({
id,
user_id: opts.userId,
amount: opts.amount
});
return { id };
}
};
11.3.2 Creating User Factories
const user = await Factory.createUser({ plan: 'premium' });
11.3.3 Creating Related Data Factories
// Complex scenario: User with pending invoices
const user = await Factory.createUser({ plan: 'basic' });
await Factory.createOrder({ userId: user.id, amount: 100 });
await Factory.createInvoice({ userId: user.id, status: 'pending' });
11.4 Testing Row Level Security (RLS)
11.4.1 Creating Admin and User Clients
// Admin client (bypasses RLS)
const adminClient = createClient(url, serviceRoleKey);
// User client (subject to RLS)
const userClient = createClient(url, anonKey, {
global: {
headers: { Authorization: `Bearer ${userJwt}` }
}
});
11.4.2 JWT Generation for Testing
import { create } from "https://deno.land/x/djwt@v2.8/mod.ts";
async function mintToken(userId: string, secret: string) {
const key = await crypto.subtle.importKey(
"raw",
new TextEncoder().encode(secret),
{ name: "HMAC", hash: "SHA-256" },
true,
["sign", "verify"]
);
return create(
{ alg: "HS256", typ: "JWT" },
{ sub: userId, role: "authenticated", exp: Date.now() / 1000 + 3600 },
key
);
}
11.4.3 Verifying Access Restrictions
Deno.test("User cannot access others' data", async () => {
// User 1 creates data
const { data: created } = await user1Client.from('todos').insert({ task: 'Private' });
// User 2 tries to read it
const { data: read } = await user2Client.from('todos').select().eq('id', created.id);
// Should be empty due to RLS
assertEquals(read.length, 0);
});
12. Third-Party Service Testing
12.1 Testing Strategy Tiers
12.1.1 Tier 1: Virtual Mocks (Spying)
Speed: <10ms
Reliability: High (no external dependencies)
const fetchStub = stub(globalThis, "fetch", () => {
return Promise.resolve(new Response(JSON.stringify({ id: "evt_123" })));
});
Best for: Testing internal logic paths ("If Stripe returns 402, downgrade the user")
12.1.1.1 Speed and Reliability Benefits
- Instantaneous execution
- Works offline
- Never fails due to external service outages
12.1.1.2 Testing Internal Logic Paths
- Test error handling for various status codes
- Test retry logic and fallback behavior
- Limitation: Doesn't verify request correctness
12.1.2 Tier 2: VCR Pattern (Record/Replay)
Speed: <20ms (after recording)
Reliability: High (frozen responses)
12.1.2.1 Recording Live Responses
First run makes real requests and saves responses to JSON files.
UPDATE_SNAPSHOTS=true deno test
12.1.2.2 Replaying from Cassettes
Subsequent runs read from saved files instead of network.
12.1.2.3 Snapshot Update Mechanism
Re-record when APIs change:
UPDATE_SNAPSHOTS=true deno test
git diff cassettes/ # Review changes
12.1.3 Tier 3: Live Sandbox Execution
Speed: 500ms-5s
Reliability: Low (external dependencies)
12.1.3.1 When to Use Live Tests
- Nightly or pre-release runs
- Verifying API contracts haven't changed
- Testing authentication credentials
12.1.3.2 Dedicated Sandbox Credentials
- Never use production keys
- Use Stripe Test Mode, Mailgun Sandbox, etc.
- Isolate from production traffic
12.1.3.3 Cleanup Requirements
- Delete created customers, subscriptions
- Cancel test charges
- Avoid "zombie data" in sandbox dashboards
12.2 Comparing Testing Strategies
| Feature | Tier 1: Mock | Tier 2: VCR | Tier 3: Live |
|---|---|---|---|
| Speed | <10ms | <20ms | 500ms-5s |
| Reliability | High | High | Low |
| Fidelity | Low | Medium | High |
| Cost | Free | Free | API usage |
| Offline | Yes | Yes | No |
12.3 Implementing Service Mocks
12.3.1 Stripe Service Mocking
const mockStripe = {
createCheckoutSession: spy(async (priceId) => ({
id: "cs_test_123",
url: "https://checkout.stripe.com/pay/cs_test_123"
})),
constructWebhookEvent: spy((body, sig) => ({
type: "checkout.session.completed",
data: { object: { id: "cs_test_123" } }
}))
};
12.3.2 Email Service Mocking
const mockEmail = {
send: spy(async (opts) => {
// Verify email content in test assertions
return { messageId: "mock_msg_123" };
})
};
12.3.3 OpenAI/LLM Service Mocking
const mockOpenAI = {
chat: {
completions: {
create: spy(async (opts) => ({
choices: [{ message: { content: "Mock AI response" } }]
}))
}
}
};
12.4 Webhook Testing
12.4.1 Signature Verification Testing
Do not disable verification in tests. Instead, generate valid signatures.
12.4.2 Generating Valid Test Signatures
async function generateStripeSignature(payload: string, secret: string) {
const timestamp = Math.floor(Date.now() / 1000);
const data = `${timestamp}.${payload}`;
const key = await crypto.subtle.importKey(
"raw",
new TextEncoder().encode(secret),
{ name: "HMAC", hash: "SHA-256" },
false,
["sign"]
);
const signature = await crypto.subtle.sign(
"HMAC",
key,
new TextEncoder().encode(data)
);
const sigHex = Array.from(new Uint8Array(signature))
.map(b => b.toString(16).padStart(2, '0'))
.join('');
return `t=${timestamp},v1=${sigHex}`;
}
12.4.3 Testing Webhook Handlers
Deno.test("Webhook processes payment_intent.succeeded", async () => {
const payload = JSON.stringify({ type: "payment_intent.succeeded", ... });
const signature = await generateStripeSignature(payload, webhookSecret);
const res = await fetch('http://localhost:54321/functions/v1/stripe-webhook', {
method: 'POST',
headers: { 'stripe-signature': signature },
body: payload
});
assertEquals(res.status, 200);
// Verify side effects
const { data } = await supabase.from('payments').select().eq('stripe_id', '...');
assertEquals(data[0].status, 'completed');
});
13. Best Practices and Anti-Patterns
13.1 Test Organization Best Practices
13.1.1 One Assertion Purpose Per Test
Focus tests narrowly:
// Good: Single focus
Deno.test("returns error when input is empty", ...);
Deno.test("returns user object when found", ...);
// Bad: Multiple behaviors in one test
Deno.test("handles all edge cases", ...);
13.1.2 Descriptive Test Names
// Good: Describes scenario and outcome
Deno.test("getLongUuid: throws InvalidData when length exceeds maximum", ...);
// Bad: Generic
Deno.test("test 1", ...);
Deno.test("works", ...);
13.1.3 Avoiding Test Interdependencies
Each test stands alone:
// Bad: Tests depend on each other
let userId: string;
Deno.test("create user", async () => { userId = await createUser(); });
Deno.test("get user", async () => { await getUser(userId); }); // Fails if first test skipped!
// Good: Each test creates its own state
Deno.test("get user returns data", async () => {
const userId = await createUser(); // Self-contained
const user = await getUser(userId);
assertEquals(user.id, userId);
});
13.2 Common Anti-Patterns
13.2.1 Coupled Handler Code
Problem: Business logic mixed with infrastructure makes testing impossible without mocking everything.
Solution: Extract logic to testable functions (Humble Handler pattern).
13.2.2 Global State Pollution
Problem: Shared mutable state causes tests to affect each other.
Solution: Create fresh state per test; clean up after.
13.2.3 Unreliable Async Handling
Problem: Missing await causes false positives.
// Bad: Missing await
Deno.test("async test", () => {
assertThrowsAsync(async () => await fetch(...)); // Returns immediately!
});
// Good: Await the assertion
Deno.test("async test", async () => {
await assertThrowsAsync(async () => await fetch(...));
});
13.2.4 Forgetting to Restore Stubs
Problem: Leaked stubs affect subsequent tests.
// Always use try/finally
const stub = stub(globalThis, "fetch", ...);
try {
// test code
} finally {
stub.restore();
}
13.3 Performance Optimization
13.3.1 Minimizing Cold Starts
- Keep functions small
- Limit dependencies
- Use lazy loading for heavy imports
13.3.2 Using Fat Functions Strategy
Group related logic in single functions to keep runtime warm:
// Instead of many tiny functions
/functions/create-user/
/functions/update-user/
/functions/delete-user/
// Consider one function with routing
/functions/user-api/ // Handles all user operations
13.3.3 Excluding Unnecessary Services in CI
supabase start -x realtime,storage-api,imgproxy,studio
Significant time savings by starting only required services.
13.4 Security Testing
13.4.1 Testing with Different User Personas
// Admin client
const adminClient = createClient(url, serviceRoleKey);
// Regular user client
const userClient = createClient(url, anonKey);
// Anonymous client (no auth)
const anonClient = createClient(url, anonKey);
13.4.2 Verifying RLS Policy Enforcement
Test that users:
- Can read their own data
- Cannot read others' data
- Cannot modify data they don't own
13.4.3 Protecting Test Credentials
- Never log secrets in assertions
- Use GitHub Secrets, not code
- Rotate immediately if exposed
14. Complete Examples and Recipes
14.1 Unit Test Suite Example
import { assertEquals, assertThrows, assertExists } from "jsr:@std/assert@1";
// Function under test
function getRandomString(len: number): string {
if (len === 0) throw new Deno.errors.InvalidData("len cannot be 0");
if (len % 11 !== 0) throw new Deno.errors.InvalidData("len must be a multiple of 11");
const rand = () => Math.random().toString(36).slice(2);
let output = "";
for (let i = 0; i < len; i += 11) output += rand();
return output;
}
// Unit Tests
Deno.test("getRandomString: throws on zero length", () => {
assertThrows(
() => getRandomString(0),
Deno.errors.InvalidData,
"cannot be 0"
);
});
Deno.test("getRandomString: throws on non-multiple of 11", () => {
assertThrows(
() => getRandomString(5),
Deno.errors.InvalidData,
"must be a multiple"
);
});
Deno.test("getRandomString: returns correct length for 11", () => {
const result = getRandomString(11);
assertExists(result);
assertEquals(typeof result, 'string');
assertEquals(result.length, 11);
});
Deno.test("getRandomString: returns correct length for 55", () => {
const result = getRandomString(55);
assertExists(result);
assertEquals(result.length, 55);
});
14.2 Integration Test Suite Example
import { assert, assertEquals, assertExists, assertStringIncludes } from "jsr:@std/assert@1";
// Unit tests (run by default)
Deno.test({
name: "unit test - data processing",
ignore: Deno.env.get('TEST_TYPE_INTEG') ? true : false,
fn: () => {
const data = processData({ input: "test" });
assertExists(data);
assertEquals(data.status, "processed");
}
});
// Integration tests (require TEST_TYPE_INTEG=1)
Deno.test({
name: "Integration: GET / returns 404",
ignore: !Deno.env.get('TEST_TYPE_INTEG'),
fn: async () => {
const res = await fetch('http://localhost:5000');
assert(res.ok === false);
assertEquals(res.status, 404);
}
});
Deno.test({
name: "Integration: GET /data returns JSON",
ignore: !Deno.env.get('TEST_TYPE_INTEG'),
fn: async () => {
const res = await fetch('http://localhost:5000/data');
const data = await res.json();
assert(res.ok);
assertEquals(res.status, 200);
assertExists(data);
assertStringIncludes(data.message, "Welcome");
}
});
Deno.test({
name: "Integration: POST / returns 405",
ignore: !Deno.env.get('TEST_TYPE_INTEG'),
fn: async () => {
const res = await fetch('http://localhost:5000', { method: 'POST' });
assertEquals(res.status, 405);
}
});
14.3 Edge Function Test Example
import { assert, assertEquals, assertExists } from 'jsr:@std/assert@1';
import { createClient, SupabaseClient } from 'npm:@supabase/supabase-js@2';
import 'jsr:@std/dotenv/load';
// Configuration
const supabaseUrl = Deno.env.get('SUPABASE_URL') ?? '';
const supabaseKey = Deno.env.get('SUPABASE_ANON_KEY') ?? '';
const options = {
auth: {
autoRefreshToken: false,
persistSession: false,
detectSessionInUrl: false,
},
};
// Test: Client Creation
const testClientCreation = async () => {
const client: SupabaseClient = createClient(supabaseUrl, supabaseKey, options);
if (!supabaseUrl) throw new Error('supabaseUrl is required.');
if (!supabaseKey) throw new Error('supabaseKey is required.');
const { data, error } = await client
.from('test_table')
.select('*')
.limit(1);
if (error) {
throw new Error('Invalid Supabase client: ' + error.message);
}
assert(data, 'Data should be returned from the query.');
};
// Test: Edge Function Invocation
const testHelloWorld = async () => {
const client: SupabaseClient = createClient(supabaseUrl, supabaseKey, options);
const { data, error } = await client.functions.invoke('hello-world', {
body: { name: 'Deno' },
});
if (error) {
throw new Error('Invalid response: ' + error.message);
}
console.log(JSON.stringify(data, null, 2));
assertEquals(data.message, 'Hello Deno!');
};
// Register Tests
Deno.test('Client Creation Test', testClientCreation);
Deno.test('Hello-world Function Test', testHelloWorld);
14.4 Full CI/CD Pipeline Example
name: Deno Tests
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Deno
uses: denoland/setup-deno@v1
with:
deno-version: v1.x
- name: Setup Supabase CLI
uses: supabase/setup-cli@v1
with:
version: latest
- name: Start Supabase (Lightweight)
run: supabase start -x realtime,storage-api,imgproxy,studio
- name: Run Database Migrations
run: supabase db push
- name: Run Unit Tests
run: deno test --allow-all supabase/functions/tests/
- name: Run Integration Tests
run: deno test --allow-all supabase/functions/tests/
env:
TEST_TYPE_INTEG: "1"
SUPABASE_URL: "http://localhost:54321"
SUPABASE_ANON_KEY: ${{ secrets.SUPABASE_ANON_KEY }}
SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}
- name: Stop Supabase
if: always()
run: supabase stop
Appendix
A. Quick Reference: Common Assertions
| Assertion | Purpose | Example |
|---|---|---|
assert(expr) |
Verify truthy | assert(count > 0) |
assertEquals(a, b) |
Deep equality | assertEquals(result, expected) |
assertNotEquals(a, b) |
Deep inequality | assertNotEquals(a, b) |
assertStrictEquals(a, b) |
Reference equality | assertStrictEquals(obj1, obj1) |
assertExists(val) |
Not null/undefined | assertExists(user) |
assertThrows(fn) |
Expect sync error | assertThrows(() => fn()) |
assertThrowsAsync(fn) |
Expect async error | await assertThrowsAsync(async () => ...) |
assertMatch(str, re) |
Regex match | assertMatch(id, /^[a-z]+$/) |
assertStringIncludes(a, b) |
Substring check | assertStringIncludes("hello", "ell") |
assertArrayIncludes(a, b) |
Array contains | assertArrayIncludes([1,2,3], [2]) |
assertObjectMatch(a, b) |
Partial object match | assertObjectMatch(obj, {id: 1}) |
B. Quick Reference: CLI Commands
| Command | Purpose |
|---|---|
deno test |
Run all tests in current directory |
deno test path/ |
Run tests in specific directory |
deno test file.ts |
Run specific test file |
deno test --jobs 4 |
Run tests in parallel (4 workers) |
deno test --filter "name" |
Run tests matching name |
deno test --fail-fast |
Stop on first failure |
deno test --allow-all |
Grant all permissions |
deno test --allow-net --allow-env |
Grant specific permissions |
supabase start |
Start local Supabase stack |
supabase functions serve |
Serve Edge Functions locally |
supabase start -x studio,realtime |
Start with excluded services |
This guide was compiled from official Supabase documentation, Deno documentation, and community best practices. For the latest updates, consult the official documentation at supabase.com/docs and deno.land/manual.
Top comments (0)