← スキル一覧に戻る

test-resource-service
by madooei
This is a template for a backend service built with TypeScript and Hono.js. It provides a basic structure for creating RESTful APIs.
⭐ 1🍴 0📅 2026年1月5日
SKILL.md
name: test-resource-service description: Test resource services that handle CRUD operations. Use when testing services that extend BaseService with authorization and event emission. Triggers on "test note service", "test resource service", "test crud service".
Test Resource Service
Tests resource services that extend BaseService and perform CRUD operations with authorization and event emission.
Quick Reference
Location: tests/services/{entity-name}.service.test.ts
Dependencies: MockDB repository, vitest, user fixtures
Test Structure
import { describe, it, expect, beforeEach, vi } from "vitest";
import { {Entity}Service } from "@/services/{entity-name}.service";
import { MockDb{Entity}Repository } from "@/repositories/mockdb/{entity-name}.mockdb.repository";
import type { Create{Entity}Type, {Entity}Type } from "@/schemas/{entity-name}.schema";
import type { AuthenticatedUserContextType } from "@/schemas/user.schemas";
import { UnauthorizedError } from "@/errors";
import { appEvents } from "@/events/event-emitter";
// User fixtures
const adminUser: AuthenticatedUserContextType = {
userId: "admin-1",
globalRole: "admin",
};
const regularUser: AuthenticatedUserContextType = {
userId: "user-1",
globalRole: "user",
};
const otherUser: AuthenticatedUserContextType = {
userId: "user-2",
globalRole: "user",
};
describe("{Entity}Service", () => {
let repository: MockDb{Entity}Repository;
let service: {Entity}Service;
beforeEach(() => {
repository = new MockDb{Entity}Repository();
service = new {Entity}Service(repository);
repository.clear();
appEvents.removeAllListeners();
});
// Test groups...
});
Test Categories
1. Create Tests
describe("create", () => {
it("allows admin to create", async () => {
const data: Create{Entity}Type = { /* valid data */ };
const result = await service.create(data, adminUser);
expect(result.createdBy).toBe(adminUser.userId);
});
it("allows regular user to create", async () => {
const data: Create{Entity}Type = { /* valid data */ };
const result = await service.create(data, regularUser);
expect(result.createdBy).toBe(regularUser.userId);
});
});
2. GetAll Tests (Authorization Filtering)
describe("getAll", () => {
beforeEach(async () => {
await service.create(
{
/* data */
},
regularUser,
);
await service.create(
{
/* data */
},
adminUser,
);
await service.create(
{
/* data */
},
otherUser,
);
});
it("returns all items for admin", async () => {
const result = await service.getAll({}, adminUser);
expect(result.data.length).toBe(3);
});
it("returns only user's items for regular user", async () => {
const result = await service.getAll({}, regularUser);
expect(result.data.length).toBe(1);
expect(
result.data.every((item) => item.createdBy === regularUser.userId),
).toBe(true);
});
it("supports search and pagination", async () => {
const result = await service.getAll(
{ search: "term", page: 1, limit: 10 },
adminUser,
);
// Assert filtered results
});
});
3. GetById Tests (Authorization)
describe("getById", () => {
let item: {Entity}Type;
beforeEach(async () => {
item = await service.create({ /* data */ }, regularUser);
});
it("allows owner to get their item", async () => {
const found = await service.getById(item.id, regularUser);
expect(found).not.toBeNull();
expect(found!.id).toBe(item.id);
});
it("allows admin to get any item", async () => {
const found = await service.getById(item.id, adminUser);
expect(found).not.toBeNull();
});
it("denies other users", async () => {
await expect(service.getById(item.id, otherUser)).rejects.toThrow(UnauthorizedError);
});
it("returns null for non-existent item", async () => {
await expect(service.getById("not-exist", adminUser)).resolves.toBeNull();
});
});
4. Update Tests (Authorization)
describe("update", () => {
let item: {Entity}Type;
beforeEach(async () => {
item = await service.create({ /* data */ }, regularUser);
});
it("allows owner to update", async () => {
const updated = await service.update(item.id, { /* new data */ }, regularUser);
expect(updated).not.toBeNull();
});
it("allows admin to update", async () => {
const updated = await service.update(item.id, { /* new data */ }, adminUser);
expect(updated).not.toBeNull();
});
it("denies other users", async () => {
await expect(service.update(item.id, { /* data */ }, otherUser)).rejects.toThrow(UnauthorizedError);
});
it("returns null for non-existent item", async () => {
await expect(service.update("not-exist", { /* data */ }, adminUser)).resolves.toBeNull();
});
});
5. Delete Tests (Authorization)
describe("delete", () => {
let item: {Entity}Type;
beforeEach(async () => {
item = await service.create({ /* data */ }, regularUser);
});
it("allows owner to delete", async () => {
const result = await service.delete(item.id, regularUser);
expect(result).toBe(true);
});
it("allows admin to delete", async () => {
const result = await service.delete(item.id, adminUser);
expect(result).toBe(true);
});
it("denies other users", async () => {
await expect(service.delete(item.id, otherUser)).rejects.toThrow(UnauthorizedError);
});
it("returns false for non-existent item", async () => {
await expect(service.delete("not-exist", adminUser)).resolves.toBe(false);
});
});
6. Event Emission Tests
describe("Event Emission", () => {
it("emits created event after successful creation", async () => {
const eventSpy = vi.fn();
appEvents.on("{entities}:created", eventSpy);
const item = await service.create(
{
/* data */
},
regularUser,
);
expect(eventSpy).toHaveBeenCalledWith({
id: expect.any(String),
action: "created",
data: item,
user: expect.objectContaining({
userId: regularUser.userId,
globalRole: regularUser.globalRole,
}),
timestamp: expect.any(Date),
resourceType: "{entities}",
});
});
it("emits updated event after successful update", async () => {
const item = await service.create(
{
/* data */
},
regularUser,
);
const eventSpy = vi.fn();
appEvents.on("{entities}:updated", eventSpy);
await service.update(
item.id,
{
/* new data */
},
regularUser,
);
expect(eventSpy).toHaveBeenCalledWith(
expect.objectContaining({
action: "updated",
resourceType: "{entities}",
}),
);
});
it("emits deleted event after successful deletion", async () => {
const item = await service.create(
{
/* data */
},
regularUser,
);
const eventSpy = vi.fn();
appEvents.on("{entities}:deleted", eventSpy);
await service.delete(item.id, regularUser);
expect(eventSpy).toHaveBeenCalledWith(
expect.objectContaining({
action: "deleted",
resourceType: "{entities}",
}),
);
});
it("does not emit events for failed operations", async () => {
const eventSpy = vi.fn();
appEvents.on("{entities}:updated", eventSpy);
appEvents.on("{entities}:deleted", eventSpy);
await service.update(
"non-existent",
{
/* data */
},
adminUser,
);
await service.delete("non-existent", adminUser);
expect(eventSpy).not.toHaveBeenCalled();
});
it("does not emit events for unauthorized operations", async () => {
const item = await service.create(
{
/* data */
},
regularUser,
);
const eventSpy = vi.fn();
appEvents.on("{entities}:updated", eventSpy);
try {
await service.update(
item.id,
{
/* data */
},
otherUser,
);
} catch {
// Expected
}
expect(eventSpy).not.toHaveBeenCalled();
});
});
Key Patterns
Use Real MockDB Repository
Inject the actual MockDB repository for integration-like tests:
beforeEach(() => {
repository = new MockDbNoteRepository();
service = new NoteService(repository);
repository.clear(); // Reset state
});
Clear Event Listeners
Prevent test pollution by removing listeners:
beforeEach(() => {
appEvents.removeAllListeners();
});
Test Authorization Matrix
| User Type | Own Resource | Other's Resource | Non-existent |
|---|---|---|---|
| Admin | Allow | Allow | null/false |
| Owner | Allow | Deny (throw) | null/false |
| Other | Deny (throw) | Deny (throw) | null/false |
Complete Example
See REFERENCE.md for a complete note.service.test.ts implementation.
What NOT to Do
- Do NOT mock the repository methods (use real MockDB)
- Do NOT forget to clear repository between tests
- Do NOT forget to remove event listeners between tests
- Do NOT skip testing authorization for all user types
- Do NOT skip testing event emission for all operations
See Also
test-service- Guide for choosing test typetest-utility-service- Testing utility servicescreate-resource-service- Creating the service to test
スコア
総合スコア
70/100
リポジトリの品質指標に基づく評価
✓SKILL.md
SKILL.mdファイルが含まれている
+20
✓LICENSE
ライセンスが設定されている
+10
✓説明文
100文字以上の説明がある
+10
○人気
GitHub Stars 100以上
0/15
○最近の活動
3ヶ月以内に更新がある
0/10
○フォーク
10回以上フォークされている
0/5
✓Issue管理
オープンIssueが50未満
+5
✓言語
プログラミング言語が設定されている
+5
○タグ
1つ以上のタグが設定されている
0/5
レビュー
💬
レビュー機能は近日公開予定です