スキル一覧に戻る
aamini-stack

test-driven-development

by aamini-stack

0🍴 0📅 2026年1月9日
GitHubで見るManusで実行

SKILL.md


name: test-driven-development description: Writing unit, integration, or E2E for any @aamini-stack app/project compatibility: opencode metadata: audience: developers frameworks: vitest, vitest-browser, playwright, drizzle, msw

What I do

  • Assist with writing high quality tests for any accompanying feature work.
  • Make sure all tests provide value and follow repository standards.
  • Write unit, integration, and E2E tests depending on the feature/granularity.

When to use me

Use this skill when you are doing TDD or feature work and need assistance writing high quality tests.

Test Commands

Run tests from the app directory (e.g., cd apps/paas):

CommandAction
pnpm test:unitRun unit tests
pnpm test:integrationRun integration tests (browser, server)
pnpm e2eRun E2E tests
pnpm e2e:updateUpdate E2E screenshots

Unit Tests (*.test.unit.ts)

  • Plain node environment for pure functions
  • No external dependencies (network, database, browser)
  • Fast, isolated tests
  • Test your own business logic, not library functions

Example (rate limiter with fake timers):

import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
import { RateLimiter } from './rate-limiter'

const TEST_IP = '192.168.1.1'
let limiter: RateLimiter

beforeEach(() => {
	limiter = new RateLimiter()
	vi.useFakeTimers()
	vi.setSystemTime(0)
})

afterEach(() => {
	vi.useRealTimers()
})

describe('RateLimiter', () => {
	test('Block after limit is reached', () => {
		expect(limiter.consume(TEST_IP)).toEqual({ success: true, remaining: 2 })
		expect(limiter.consume(TEST_IP)).toEqual({ success: true, remaining: 1 })
		expect(limiter.consume(TEST_IP)).toEqual({ success: true, remaining: 0 })
		expect(limiter.consume(TEST_IP)).toEqual({ success: false, retryAfter: 15 })
		vi.advanceTimersByTime(15 * 60 * 1000 + 1)
		expect(limiter.consume(TEST_IP)).toEqual({ success: true, remaining: 0 })
	})

	test('Refill tokens after cooldown', () => {
		expect(limiter.consume(TEST_IP)).toEqual({ success: true, remaining: 2 })
		vi.advanceTimersByTime(15 * 60 * 1000 + 1)
		expect(limiter.consume(TEST_IP)).toEqual({ success: true, remaining: 2 })
	})

	test("Don't fill tokens before window passes", () => {
		expect(limiter.consume(TEST_IP)).toEqual({ success: true, remaining: 2 })
		vi.advanceTimersByTime(14 * 60 * 1000)
		expect(limiter.consume(TEST_IP)).toEqual({ success: true, remaining: 1 })
	})

	test('Handling multiple IPs independently', () => {
		const ip1 = '192.168.1.1'
		const ip2 = '192.168.1.2'
		expect(limiter.consume(ip1)).toEqual({ success: true, remaining: 2 })
		expect(limiter.consume(ip2)).toEqual({ success: true, remaining: 2 })
	})
})

Database Tests (*.test.ts)

  1. Use db test fixture: import { test } from '@/mocks/test-extend-db'
  2. Optional: Use initDb() to seed test data:
import { test, initDb } from '@/mocks/test-extend-db'
import { describe, expect } from 'vitest'
import { shows } from './__fixtures__/shows'
import { show } from '@/db/tables'

describe('search tests', () => {
	// Seed database with test data from fixtures
	initDb(async (db) => {
		await db.insert(show).values(shows)
	})

	test('exact title', async ({ db }) => {
		const results = await fetchSuggestions(db, 'Game of Thrones')
		expect(results[0]).toEqual({
			title: 'Game of Thrones',
			imdbId: 'tt0944947',
			rating: 9.2,
		})
	})
})

Browser Tests (*.test.tsx)

  • vitest-browser with real Playwright browser
  • Test React components in isolation
  • Import test extension: import { test } from '@/mocks/test-extend-browser'
  • Use vitest-browser-react for component rendering
  • Use userEvent from vitest/browser for interactions
  • Use expect.element() for async assertions

Example:

import { test } from '@/mocks/test-extend-browser'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import {
	createRootRoute,
	createRoute,
	createRouter,
	RouterContextProvider,
} from '@tanstack/react-router'
import { http, HttpResponse } from 'msw'
import { beforeEach, describe, expect, vi } from 'vitest'
import { render } from 'vitest-browser-react'
import { page, userEvent } from 'vitest/browser'
import { SearchBar } from './search-bar'

const testQueryClient = new QueryClient({
	defaultOptions: {
		queries: {
			retry: false,
		},
	},
})

vi.mock(import('@/lib/react-query'), () => ({
	queryClient: testQueryClient,
}))

beforeEach(() => {
	testQueryClient.clear()
})

function MockRouter(props: { children: React.ReactNode }) {
	const rootRoute = createRootRoute()
	const indexRoute = createRoute({
		getParentRoute: () => rootRoute,
		path: '/',
	})
	const routeTree = rootRoute.addChildren([indexRoute])
	const router = createRouter({ routeTree })

	return (
		<QueryClientProvider client={testQueryClient}>
			<RouterContextProvider router={router}>
				{props.children}
			</RouterContextProvider>
		</QueryClientProvider>
	)
}

test('basic search', async () => {
	const screen = await render(<SearchBar />, {
		wrapper: MockRouter,
	})

	const searchBar = screen.getByRole('combobox')
	await userEvent.fill(searchBar, 'avatar')
	await expect
		.element(page.getByText(/Avatar: The Last Airbender/).first())
		.toBeVisible()
})

test('no results', async ({ worker }) => {
	worker.use(
		http.get('/api/suggestions', () => {
			return HttpResponse.json([])
		}),
	)

	const screen = await render(<SearchBar />, {
		wrapper: MockRouter,
	})
	const searchBar = screen.getByRole('combobox')
	await userEvent.fill(searchBar, 'blah')
	await expect.element(screen.getByText(/No TV Shows Found./i)).toBeVisible()
})

test('error message', async ({ worker }) => {
	worker.use(
		http.get('/api/suggestions', () => {
			return HttpResponse.error()
		}),
	)

	const screen = await render(<SearchBar />, {
		wrapper: MockRouter,
	})
	const searchBar = screen.getByRole('combobox')
	await userEvent.fill(searchBar, 'error')
	await expect
		.element(screen.getByText(/Something went wrong. Please try again./i))
		.toBeVisible()
})

Mocking APIs with MSW

1. Set up mock JSON responses in __mocks__/data/suggestions.json:

[
	{
		"imdbId": "tt0417299",
		"title": "Avatar: The Last Airbender",
		"startYear": "2005",
		"endYear": "2008",
		"rating": 9.3,
		"numVotes": 410746
	},
	{
		"imdbId": "tt9018736",
		"title": "Avatar: The Last Airbender",
		"startYear": "2024",
		"endYear": null,
		"rating": 7.2,
		"numVotes": 80299
	},
	{
		"imdbId": "tt10732794",
		"title": "The King's Avatar",
		"startYear": "2019",
		"endYear": "2019",
		"rating": 8.1,
		"numVotes": 1776
	},
	{
		"imdbId": "tt6859260",
		"title": "The King's Avatar",
		"startYear": "2017",
		"endYear": null,
		"rating": 7.4,
		"numVotes": 1617
	},
	{
		"imdbId": "tt15776622",
		"title": "Avataro Sentai Donbrothers",
		"startYear": "2022",
		"endYear": "2023",
		"rating": 8.1,
		"numVotes": 345
	}
]

2. Set up handlers __mocks__/handlers.ts:

import suggestions from '@/mocks/data/suggestions.json' with { type: 'json' }
import { http, HttpResponse } from 'msw'

export default [
	http.get('/api/suggestions', () => {
		return HttpResponse.json(suggestions)
	}),
]

3. Import @/mocks/test-extend-server if a server test (.ts) or @/mocks/test-extend-browser if a browser test (.tsx):

import { test } from '@/mocks/test-extend-server'
import { describe, expect } from 'vitest'
import { fetchGitHubRepos } from './github-repos'

describe('GitHub Repositories', () => {
	test('fetches repositories from GitHub API', async () => {
		const repos = await fetchGitHubRepos('test_token')
		expect(repos).toHaveLength(3)
		expect(repos[0].name).toBe('test-repo')
	})
})

4. (Optional): Setup inline mocks (For error scenarios or one-off requests):

import { test } from '@/mocks/test-extend-browser'
import { describe, expect } from 'vitest'
import { fetchGitHubRepos } from './github-repos'

test('no results', async ({ worker }) => {
	// One-off mock for error scenario
	worker.use(
		http.get('/api/suggestions', () => {
			return HttpResponse.json([])
		}),
	)

	const screen = await render(<SearchBar />, {
		wrapper: MockRouter,
	})
	const searchBar = screen.getByRole('combobox')
	await userEvent.fill(searchBar, 'blah')
	await expect.element(screen.getByText(/No TV Shows Found./i)).toBeVisible()
})

E2E Tests (Playwright)

  • Full browser automation tests
  • Located in e2e/ directory
  • Test user flows and page interactions
  • Use screenshots for visual regression testing

Example:

import { expect, test } from '@playwright/test'

test.beforeEach(async ({ page }) => {
	await page.goto('/', { waitUntil: 'networkidle' })
})

test('search bar click navigation works', async ({ page }) => {
	const searchBar = page.getByRole('combobox')
	await searchBar.click()
	await searchBar.fill('Avatar')
	await page.getByTestId('loading-spinner').waitFor({ state: 'hidden' })
	const avatarDropdownOption = page.getByText('Avatar: The Last Airbender')
	await expect(avatarDropdownOption).toBeVisible()
	await avatarDropdownOption.click()
	await expect(page).toHaveURL(/.*\/ratings\/tt0417299/)
})

test('Screenshot Homepage', async ({ page }) => {
	await expect(page).toHaveScreenshot()
})

スコア

総合スコア

40/100

リポジトリの品質指標に基づく評価

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

0/10
説明文

100文字以上の説明がある

0/10
人気

GitHub Stars 100以上

0/15
最近の活動

3ヶ月以内に更新がある

0/10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

0/5

レビュー

💬

レビュー機能は近日公開予定です