Skip to main content
Learn about Sonamu’s Vitest-based test system and structure.

Test System Overview

Vitest

Fast test executionVite integration

Context Based

Authentication testingPermission simulation

Transaction

Auto rollbackIsolated tests

Fixture System

Test dataReusable

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.
api/vitest.config.ts
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.
OptionDescriptionDefault
includeTest file patterns["src/**/*.test.ts"]
excludePatterns to exclude["**/node_modules/**"]
globalsEnable global APItrue
globalSetupGlobal 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.
api/vitest.config.ts
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:
  1. Sonamu initialization: Initialize framework in test mode
  2. Transaction management: Auto rollback for each test
  3. Timer reset: Reset Vitest’s fake timers
  4. 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:
  1. bootstrap(vi) required: Call in every test file
  2. async required: All test functions must be async
  3. Transaction based: Auto rollback after test ends
  4. Context injection: test/testAs auto-configure Context
  5. Isolated tests: No dependencies between tests

Next Steps