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
function getMockContext () : Context {
return {
ip: "127.0.0.1" ,
session: {},
user: null , // Default: not logged in
passport: {
login : async () => {},
logout : () => {},
},
naiteStore: Naite . createStore (),
};
}
export const test = async ( title : string , fn : TestFunction ) => {
return vitestTest ( title , async ( context ) => {
await runWithMockContext ( async () => {
await fn ( context );
});
});
};
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 is null
expect ( context . user ). toBeNull ();
expect ( context . ip ). toBe ( "127.0.0.1" );
});
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