Learn about Sonamu’s Vitest-based test system and structure.
Test System Overview
Vitest Fast test execution Vite integration
Context Based Authentication testing Permission simulation
Transaction Auto rollback Isolated tests
Fixture System Test data Reusable
Vitest Configuration
Sonamu project test environment is configured with vitest.config.ts and global.ts.
vitest.config.ts
Sonamu provides the getSonamuTestConfig() function to easily configure test settings.
import { getSonamuTestConfig } from "sonamu/test" ;
import { defineConfig } from "vitest/config" ;
export default defineConfig ( async () => ({
test: await getSonamuTestConfig ({
include: [ "src/**/*.test.ts" ],
exclude: [ "**/node_modules/**" , "**/dist/**" ],
globals: true ,
globalSetup: [ "./src/testing/global.ts" ],
}) ,
})) ;
getSonamuTestConfig Options
getSonamuTestConfig() supports all Vitest options and provides Sonamu-optimized defaults.
Option Description Default includeTest file patterns ["src/**/*.test.ts"]excludePatterns to exclude ["**/node_modules/**"]globalsEnable global API trueglobalSetupGlobal setup file - setupFilesSetup to run before each test file -
global.ts Setup
global.ts is a global setup file that runs once before all tests. Export Sonamu’s setup function to initialize the test environment.
api/src/testing/global.ts
import dotenv from "dotenv" ;
dotenv . config ();
// Configure test environment based on sonamu.config.ts test settings.
export { setup } from "sonamu/test" ;
export { setup } from "sonamu/test" reads the test settings from sonamu.config.ts to automatically configure parallel test environments (multiple test DBs).
Advanced Configuration Example
Here’s an advanced configuration with custom sequencer and reporters.
import { getSonamuTestConfig , NaiteVitestReporter } from "sonamu/test" ;
import { defineConfig } from "vitest/config" ;
import { PrioritySequencer } from "./custom-sequencer" ;
export default defineConfig ( async () => ({
plugins: [] ,
test: await getSonamuTestConfig ({
include: [ "src/**/*.test.ts" ],
exclude: [ "src/**/*.test-hold.ts" , "**/node_modules/**" , "**/.yarn/**" , "**/dist/**" ],
globals: true ,
globalSetup: [ "./src/testing/global.ts" ],
setupFiles: [ "./src/testing/setup-mocks.ts" ],
sequence: {
sequencer: PrioritySequencer , // Custom test order control
},
reporters: [ "default" , NaiteVitestReporter ], // Naite reporter
restoreMocks: true ,
typecheck: {
enabled: true ,
tsconfig: "./tsconfig.json" ,
include: [ "src/**/*type-safety.test.ts" ],
},
coverage: {
provider: "v8" ,
reporter: [ "text" , "html" ],
include: [ "src/**/*.ts" ],
exclude: [ "**/*.test.ts" , "**/testing/**" , "**/node_modules/**" , "**/dist/**" ],
},
includeTaskLocation: true ,
server: {
deps: {
inline: [ "sonamu" ],
},
},
}) ,
})) ;
bootstrap Function
Sonamu initializes the test environment with the bootstrap function.
// api/src/application/sonamu.test.ts
import { bootstrap , test } from "sonamu/test" ;
import { expect , vi } from "vitest" ;
// Initialize test environment
bootstrap ( vi );
test ( "Create user" , async () => {
// Test code
});
What bootstrap Does
// sonamu/src/testing/bootstrap.ts
export function bootstrap ( vi : VitestUtils ) {
beforeAll ( async () => {
// Initialize Sonamu in test mode
await Sonamu . initForTesting ();
});
beforeEach ( async () => {
// Start Transaction for each test
await DB . createTestTransaction ();
});
afterEach ( async () => {
// Reset timers
vi . useRealTimers ();
// Rollback Transaction (auto cleanup)
await DB . clearTestTransaction ();
});
afterAll (() => {
// Cleanup
});
}
Key features :
Sonamu initialization : Initialize framework in test mode
Transaction management : Auto rollback for each test
Timer reset : Reset Vitest’s fake timers
Test reporting : Pass results to Naite system
test Function
Sonamu provides a custom test function that wraps Vitest’s test.
Basic Usage
import { test } from "sonamu/test" ;
import { expect } from "vitest" ;
import { UserModel } from "@/models/user.model" ;
test ( "Create user" , async () => {
const userModel = new UserModel ();
const { user } = await userModel . create ({
username: "john" ,
email: "john@example.com" ,
password: "password123" ,
});
expect ( user . id ). toBeGreaterThan ( 0 );
expect ( user . username ). toBe ( "john" );
});
test ( "Get user" , async () => {
const userModel = new UserModel ();
// Create test data
const { user } = await userModel . create ({
username: "jane" ,
email: "jane@example.com" ,
password: "password123" ,
});
// Retrieve
const { user : found } = await userModel . getUser ( "C" , user . id );
expect ( found . id ). toBe ( user . id );
expect ( found . username ). toBe ( "jane" );
});
Context Injection
The test function automatically injects Mock Context.
// Internal operation (src/testing/bootstrap.ts)
function getMockContext () : Context {
return {
transport: "http" ,
request: null as unknown as Context [ "request" ],
reply: null as unknown as Context [ "reply" ],
headers: {},
createSSE: (() => {
throw new Error ( "createSSE is not available in mock context" );
}) as Context [ "createSSE" ],
session: null ,
user: null , // Default: not logged in
naiteStore: Naite . createStore (),
locale: "" ,
};
}
export const test = async ( title : string , fn : TestFunction ) => {
return vitestTest ( title , async ( context ) => {
await runWithMockContext ( async () => {
await fn ( context );
});
});
};
Full Mock Context Properties
Mock Context is composed of the minimum values that satisfy the actual Context type (src/api/context.ts). The request/reply fields, which are populated from real HTTP requests, are not used in the Mock.
Base Properties
Property Type Mock Value Description transport"http""http"Transport channel (always http in Mock) headersIncomingHttpHeaders{}Request headers (empty in Mock) localestring""Current request locale (falls back via i18n config)
Authentication
Property Type Mock Value Description userUser | nullnullAuthenticated user (set by testAs) sessionSession | nullnullSession info (set by testAs)
Naite Logging
Property Type Mock Value Description naiteStoreNaiteStoreNaite.createStore()Naite log store
Disabled in Mock
Property Type Mock Behavior Description requestFastifyRequestnull (type-asserted)Real Fastify Request — do not access in Mock replyFastifyReplynull (type-asserted)Real Fastify Reply — do not access in Mock createSSE(events) => SSEThrows on call SSE is not supported in Mock context
Context Usage Example
import { test } from "sonamu/test" ;
import { Sonamu } from "sonamu" ;
import { expect } from "vitest" ;
test ( "Access Context properties" , async () => {
const context = Sonamu . getContext ();
// Authentication state (unauthenticated under test())
expect ( context . user ). toBeNull ();
expect ( context . session ). toBeNull ();
// Base properties
expect ( context . transport ). toBe ( "http" );
expect ( context . headers ). toEqual ({});
expect ( context . locale ). toBe ( "" );
// Naite log store
expect ( context . naiteStore ). toBeDefined ();
});
Context under testAs
import { testAs } from "sonamu/test" ;
import { Sonamu } from "sonamu" ;
import { expect } from "vitest" ;
testAs (
{ id: 1 , username: "testuser" , role: "admin" } as any ,
"Authenticated Context" ,
async () => {
const context = Sonamu . getContext ();
// user is set
expect ( context . user ). not . toBeNull ();
expect ( context . user ?. id ). toBe ( 1 );
// session is set as well
expect ( context . session ). not . toBeNull ();
// Rest is identical to test()
expect ( context . locale ). toBeDefined ();
expect ( context . naiteStore ). toBeDefined ();
}
);
Customizing Context
You can directly modify mutable Mock Context properties when needed:
import { test } from "sonamu/test" ;
import { Sonamu } from "sonamu" ;
test ( "Force locale" , async () => {
const context = Sonamu . getContext ();
// Modify Context property directly (not recommended)
context . locale = "ko" ;
// Verify locale-based behavior
console . log ( context . locale ); // "ko"
});
Cautions :
Mock Context is for tests : it may differ from real HTTP requests.
No session persistence : all state is reset when the test ends.
Direct mutation discouraged : prefer testAs() over hand-editing Context.
Context property summary :
Base : transport, headers, locale
Auth : user, session
Naite : naiteStore
HTTP-only (disabled in Mock) : request, reply, createSSE
testAs Function
Test in an authenticated state as a specific user.
Basic Usage
import { testAs } from "sonamu/test" ;
import { expect } from "vitest" ;
import { PostModel } from "@/models/post.model" ;
testAs (
{ id: 1 , username: "admin" , role: "admin" }, // Authenticated user
"Admin can delete any post" ,
async () => {
const postModel = new PostModel ();
// Context.user is set to above user
await postModel . deletePost ( 123 );
// Verify deletion
const deleted = await postModel . findById ( "C" , 123 );
expect ( deleted ). toBeNull ();
}
);
testAs (
{ id: 2 , username: "user" , role: "user" }, // Regular user
"Regular user can only delete own posts" ,
async () => {
const postModel = new PostModel ();
// Context.user.id === 2
await expect (
postModel . deletePost ( 999 ) // Another user's post
). rejects . toThrow ( "Permission denied" );
}
);
Permission Testing
import { testAs } from "sonamu/test" ;
// Admin test
testAs (
{ id: 1 , role: "admin" },
"Admin can view user list" ,
async () => {
const userModel = new UserModel ();
const { users } = await userModel . getUsers ({ page: 1 , pageSize: 10 });
expect ( users . length ). toBeGreaterThan ( 0 );
}
);
// Regular user test
testAs (
{ id: 2 , role: "user" },
"Regular user cannot view user list" ,
async () => {
const userModel = new UserModel ();
await expect (
userModel . getUsers ({ page: 1 , pageSize: 10 })
). rejects . toThrow ();
}
);
test.skip, test.only, test.todo
You can use Vitest’s features as-is.
import { test } from "sonamu/test" ;
// Skip
test . skip ( "Test not yet implemented" , async () => {
// This test will not run
});
// Run only this test
test . only ( "Run only this test" , async () => {
// Other tests are ignored and only this runs
});
// TODO marker
test . todo ( "Test to write later" );
test.each
Repeat the same test with multiple input values.
import { test } from "sonamu/test" ;
import { expect } from "vitest" ;
test . each ([
{ input: "user1@example.com" , expected: true },
{ input: "invalid-email" , expected: false },
{ input: "user@domain" , expected: false },
{ input: "user@example.co.kr" , expected: true },
])( "Email validation: $input → $expected" , async ({ input , expected }) => {
const isValid = validateEmail ( input );
expect ( isValid ). toBe ( expected );
});
Transaction Auto Rollback
Each test runs in an isolated Transaction and automatically rolls back .
import { test } from "sonamu/test" ;
test ( "User creation test" , async () => {
const userModel = new UserModel ();
// Insert data into database
await userModel . create ({
username: "test-user" ,
email: "test@example.com" ,
password: "password" ,
});
// Auto rollback after test ends
// → Database maintains clean state
});
test ( "Next test starts with clean DB" , async () => {
const userModel = new UserModel ();
// "test-user" from previous test doesn't exist
const { users } = await userModel . getUsers ({ page: 1 , pageSize: 10 });
expect ( users . find ( u => u . username === "test-user" )). toBeUndefined ();
});
Benefits :
Isolation between tests
No cleanup needed
Fast execution (no actual INSERT/DELETE)
Test File Structure
// api/src/models/user.model.test.ts
import { bootstrap , test , testAs } from "sonamu/test" ;
import { expect , vi } from "vitest" ;
import { UserModel } from "./user.model" ;
// 1. Initialize test environment with bootstrap
bootstrap ( vi );
// 2. Test groups (describe is optional)
test ( "Create user" , async () => {
const userModel = new UserModel ();
const { user } = await userModel . create ({
username: "john" ,
email: "john@example.com" ,
password: "password123" ,
});
expect ( user . id ). toBeGreaterThan ( 0 );
expect ( user . username ). toBe ( "john" );
});
test ( "Duplicate email validation" , async () => {
const userModel = new UserModel ();
// Create first user
await userModel . create ({
username: "user1" ,
email: "duplicate@example.com" ,
password: "password" ,
});
// Try to create with same email
await expect (
userModel . create ({
username: "user2" ,
email: "duplicate@example.com" ,
password: "password" ,
})
). rejects . toThrow ( "Email already exists" );
});
testAs (
{ id: 1 , role: "admin" },
"Admin can delete users" ,
async () => {
const userModel = new UserModel ();
// Create user
const { user } = await userModel . create ({
username: "temp" ,
email: "temp@example.com" ,
password: "password" ,
});
// Delete
await userModel . deleteUser ( user . id );
// Verify
const deleted = await userModel . findById ( "C" , user . id );
expect ( deleted ). toBeNull ();
}
);
Accessing Context
You can directly use Context within tests.
import { test } from "sonamu/test" ;
import { Sonamu } from "sonamu" ;
test ( "Check user info in Context" , async () => {
const context = Sonamu . getContext ();
// Mock context so user/session are null
expect ( context . user ). toBeNull ();
expect ( context . session ). toBeNull ();
});
import { testAs } from "sonamu/test" ;
import { Sonamu } from "sonamu" ;
testAs (
{ id: 1 , username: "admin" },
"Check authenticated Context" ,
async () => {
const context = Sonamu . getContext ();
expect ( context . user ). not . toBeNull ();
expect ( context . user ?. id ). toBe ( 1 );
expect ( context . user ?. username ). toBe ( "admin" );
}
);
Async Tests
All tests must be async functions.
import { test } from "sonamu/test" ;
// ✅ Correct usage
test ( "Async test" , async () => {
const result = await someAsyncFunction ();
expect ( result ). toBe ( "expected" );
});
// ❌ Wrong usage
test ( "Sync test" , () => { // No async!
const result = someAsyncFunction (); // No await!
expect ( result ). toBe ( "expected" ); // Compared with Promise object
});
Cautions
Cautions when writing tests :
bootstrap(vi) required : Call in every test file
async required : All test functions must be async
Transaction based : Auto rollback after test ends
Context injection : test/testAs auto-configure Context
Isolated tests : No dependencies between tests
Next Steps
Test Scaffolding Auto test generation
Test Helpers Fixtures and utilities
Model System Understanding Models