Skip to main content

Testing Basics

Sonamu uses Vitest.Features:
  • Very fast execution based on Vite
  • Jest-compatible API
  • Native TypeScript support
  • HMR support
# Run all tests
pnpm test

# Run specific file only
pnpm test user.model.test.ts

# Watch mode (detect file changes)
pnpm test --watch

# Coverage report
pnpm test --coverage

# UI mode
pnpm test --ui
Generate with Scaffolding:
  1. Access Sonamu UI
  2. Scaffolding tab
  3. Select Entity
  4. Template: Select model_test
  5. Click Generate
Auto-generated file:
// user.model.test.ts
import { beforeAll, describe, expect, test } from "vitest";
import { UserModel } from "./user.model";

beforeAll(async () => {
  await UserModel.setTestData();
});

describe("UserModel", () => {
  test("findById", async () => {
    const user = await UserModel.findById(1);
    expect(user).toBeDefined();
    expect(user.id).toBe(1);
  });

  test("save", async () => {
    const user = await UserModel.save({
      name: "Test User",
      email: "test@example.com"
    });
    expect(user.id).toBeGreaterThan(0);
    expect(user.name).toBe("Test User");
  });
});

Test Data

setTestData() method:
// user.model.ts
class UserModelClass extends BaseModelClass {
  static async setTestData() {
    await this.truncate();

    await this.bulkInsert([
      { id: 1, name: "John", email: "john@example.com" },
      { id: 2, name: "Jane", email: "jane@example.com" },
      { id: 3, name: "Bob", email: "bob@example.com" }
    ]);
  }
}
Using in tests:
import { beforeAll, describe, test } from "vitest";

beforeAll(async () => {
  await UserModel.setTestData();
  await PostModel.setTestData();
  await CommentModel.setTestData();
});

describe("UserModel", () => {
  test("findById", async () => {
    const user = await UserModel.findById(1);
    expect(user.name).toBe("John");
  });
});
Using beforeEach:
import { beforeEach, describe, test } from "vitest";

beforeEach(async () => {
  await UserModel.setTestData();
});

describe("UserModel", () => {
  test("create user", async () => {
    const user = await UserModel.save({ name: "New User" });
    expect(user.id).toBeGreaterThan(3);
  });

  test("delete user", async () => {
    await UserModel.del(1);
    const user = await UserModel.findById(1);
    expect(user).toBeNull();
  });
});

Model Testing

describe("UserModel CRUD", () => {
  test("findById", async () => {
    const user = await UserModel.findById(1);
    expect(user).toBeDefined();
    expect(user.id).toBe(1);
  });

  test("findMany", async () => {
    const result = await UserModel.findMany("A", { num: 10, page: 1 });
    expect(result.rows.length).toBeGreaterThan(0);
    expect(result.total).toBeGreaterThan(0);
  });

  test("save - create", async () => {
    const user = await UserModel.save({
      name: "New User",
      email: "new@example.com"
    });
    expect(user.id).toBeGreaterThan(0);
    expect(user.name).toBe("New User");
  });

  test("save - update", async () => {
    const user = await UserModel.save({
      id: 1,
      name: "Updated Name"
    });
    expect(user.id).toBe(1);
    expect(user.name).toBe("Updated Name");
  });

  test("del", async () => {
    await UserModel.del(1);
    const user = await UserModel.findById(1);
    expect(user).toBeNull();
  });
});
describe("UserModel Subsets", () => {
  test("Subset A", async () => {
    const result = await UserModel.findMany("A", { num: 10, page: 1 });
    expect(result.rows[0]).toHaveProperty("id");
    expect(result.rows[0]).toHaveProperty("name");
  });

  test("Subset WithPosts", async () => {
    const result = await UserModel.findMany("WithPosts", { num: 10, page: 1 });
    expect(result.rows[0]).toHaveProperty("posts");
    expect(Array.isArray(result.rows[0].posts)).toBe(true);
  });

  test("Subset field existence check", async () => {
    const user = await UserModel.findById(1, "WithProfile");
    expect(user).toHaveProperty("profile");
    expect(user.profile).toBeDefined();
  });
});
describe("User Relations", () => {
  test("User -> Posts (HasMany)", async () => {
    const user = await UserModel.findById(1, "WithPosts");
    expect(user.posts).toBeDefined();
    expect(Array.isArray(user.posts)).toBe(true);
    expect(user.posts.length).toBeGreaterThan(0);
  });

  test("Post -> User (BelongsToOne)", async () => {
    const post = await PostModel.findById(1, "WithAuthor");
    expect(post.author).toBeDefined();
    expect(post.author.id).toBe(post.user_id);
  });

  test("User -> Profile (OneToOne)", async () => {
    const user = await UserModel.findById(1, "WithProfile");
    expect(user.profile).toBeDefined();
    expect(user.profile.user_id).toBe(user.id);
  });
});

API Testing

describe("User API", () => {
  test("listUsers", async () => {
    const result = await UserModel.listUsers({ num: 10, page: 1 });
    expect(result.rows.length).toBeLessThanOrEqual(10);
    expect(result.total).toBeGreaterThan(0);
  });

  test("createUser", async () => {
    const user = await UserModel.createUser({
      name: "API Test User",
      email: "apitest@example.com"
    });
    expect(user.id).toBeGreaterThan(0);
    expect(user.name).toBe("API Test User");
  });

  test("updateUser", async () => {
    const user = await UserModel.updateUser(1, {
      name: "Updated via API"
    });
    expect(user.id).toBe(1);
    expect(user.name).toBe("Updated via API");
  });

  test("deleteUser", async () => {
    await UserModel.deleteUser(1);
    const user = await UserModel.findById(1);
    expect(user).toBeNull();
  });
});
import { BadRequestError, NotFoundError } from "sonamu";

describe("User API Errors", () => {
  test("createUser - duplicate email", async () => {
    await expect(
      UserModel.createUser({
        name: "Test",
        email: "john@example.com"  // Already exists
      })
    ).rejects.toThrow(BadRequestError);
  });

  test("findById - non-existent ID", async () => {
    await expect(
      UserModel.findById(99999)
    ).rejects.toThrow(NotFoundError);
  });

  test("deleteUser - no permission", async () => {
    await expect(
      UserModel.deleteUser(1)  // Permission check fails
    ).rejects.toThrow(ForbiddenError);
  });
});

Advanced Testing

describe("Transactions", () => {
  test("transaction success", async () => {
    await UserModel.transaction(async (trx) => {
      const user = await UserModel.save(
        { name: "Tx User" },
        { trx }
      );
      const profile = await ProfileModel.save(
        { user_id: user.id, bio: "Bio" },
        { trx }
      );

      expect(user.id).toBeGreaterThan(0);
      expect(profile.user_id).toBe(user.id);
    });
  });

  test("transaction rollback", async () => {
    const initialCount = await UserModel.count();

    try {
      await UserModel.transaction(async (trx) => {
        await UserModel.save({ name: "Rollback User" }, { trx });
        throw new Error("Forced error");
      });
    } catch (error) {
      // Ignore error
    }

    const finalCount = await UserModel.count();
    expect(finalCount).toBe(initialCount);  // No change
  });
});
describe("Async Operations", () => {
  test("email sending", async () => {
    const result = await UserModel.sendWelcomeEmail(1);
    expect(result.success).toBe(true);
  });

  test("file upload", async () => {
    const file = {
      buffer: Buffer.from("test content"),
      originalname: "test.txt"
    } as Express.Multer.File;

    const result = await UserModel.uploadAvatar(file);
    expect(result.url).toBeDefined();
    expect(result.url).toContain("avatar");
  });

  test("external API call", async () => {
    const result = await UserModel.fetchExternalData();
    expect(result).toBeDefined();
  });
});
import { vi } from "vitest";

describe("Mocking", () => {
  test("external service Mock", async () => {
    // Mock EmailService.send
    const sendMock = vi.spyOn(EmailService, "send")
      .mockResolvedValue({ success: true });

    await UserModel.sendWelcomeEmail(1);

    expect(sendMock).toHaveBeenCalledTimes(1);
    expect(sendMock).toHaveBeenCalledWith(
      expect.objectContaining({
        to: "john@example.com",
        subject: "Welcome!"
      })
    );

    sendMock.mockRestore();
  });

  test("Date.now Mock", async () => {
    const mockDate = new Date("2025-01-01");
    vi.setSystemTime(mockDate);

    const user = await UserModel.save({ name: "Test" });
    expect(user.created_at).toEqual(mockDate);

    vi.useRealTimers();
  });
});

Test Patterns

describe("Validation", () => {
  test.each([
    ["john@example.com", true],
    ["invalid-email", false],
    ["", false],
    ["test@", false]
  ])("email validation: %s -> %s", async (email, expected) => {
    const isValid = await UserModel.validateEmail(email);
    expect(isValid).toBe(expected);
  });
});

describe("UserRole", () => {
  test.each([
    ["admin", true],
    ["user", false],
    ["guest", false]
  ])("admin permission: %s -> %s", async (role, expected) => {
    const user = await UserModel.findById(1);
    user.role = role;
    const isAdmin = await UserModel.isAdmin(user);
    expect(isAdmin).toBe(expected);
  });
});
describe("UserModel", () => {
  describe("CRUD Operations", () => {
    test("create", async () => { });
    test("read", async () => { });
    test("update", async () => { });
    test("delete", async () => { });
  });

  describe("Validations", () => {
    test("email validation", async () => { });
    test("password strength", async () => { });
  });

  describe("Relations", () => {
    test("posts", async () => { });
    test("profile", async () => { });
  });
});

Best Practices

DO:
  • βœ… Each test should be independently executable
  • βœ… Initialize test data with beforeAll or beforeEach
  • βœ… Use meaningful test names
  • βœ… Test only one feature per test
  • βœ… Always test error cases
DON’T:
  • ❌ Don’t create dependencies between tests
  • ❌ Don’t call real external services (use Mocks)
  • ❌ Don’t use production DB
  • ❌ Avoid hard-coded IDs or dates
  • ❌ Don’t ignore test failures