Back to list
oakoss

testing

by oakoss

Open-source SaaS starter kit with React, TanStack, and Better Auth

0🍴 0📅 Jan 26, 2026

SKILL.md


name: testing description: Write tests with Vitest and Testing Library. Use when creating tests, writing specs, mocking, or setting up test coverage.

Vitest + Testing Library

Running Tests

pnpm test            # Run all
pnpm test:watch      # Watch mode
pnpm test:coverage   # With coverage

Basic Test

import { render, screen } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
import { Button } from '@oakoss/ui';

describe('Button', () => {
  it('renders with text', () => {
    render(<Button>Click me</Button>);
    expect(
      screen.getByRole('button', { name: /click me/i }),
    ).toBeInTheDocument();
  });
});

User Interactions

import userEvent from '@testing-library/user-event';

it('handles click', async () => {
  const user = userEvent.setup();
  const onClick = vi.fn();

  render(<Button onPress={onClick}>Click</Button>);
  await user.click(screen.getByRole('button'));

  expect(onClick).toHaveBeenCalled();
});

Query Priority

  1. getByRole - Accessible elements
  2. getByLabelText - Form inputs
  3. getByText - Text content
  4. getByTestId - Last resort

Mocking

const mockFn = vi.fn().mockReturnValue('result');

vi.mock('@oakoss/ui', () => ({
  Button: vi.fn(({ children }) => <button>{children}</button>),
}));

const spy = vi.spyOn(utils, 'cn');

Async

await screen.findByText('Loaded');

await waitFor(() => {
  expect(screen.getByRole('list')).toHaveTextContent('Item');
});

Testing TanStack Query

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

function createTestQueryClient() {
  return new QueryClient({
    defaultOptions: {
      queries: { retry: false, gcTime: 0 },
      mutations: { retry: false },
    },
  });
}

function renderWithQuery(ui: React.ReactElement) {
  const queryClient = createTestQueryClient();
  return render(
    <QueryClientProvider client={queryClient}>{ui}</QueryClientProvider>,
  );
}

it('loads and displays data', async () => {
  vi.mock('@/lib/api', () => ({
    getPosts: vi.fn().mockResolvedValue([{ id: '1', title: 'Test' }]),
  }));

  renderWithQuery(<PostList />);
  expect(await screen.findByText('Test')).toBeInTheDocument();
});

Testing TanStack Form

import userEvent from '@testing-library/user-event';

it('validates and submits form', async () => {
  const user = userEvent.setup();
  const onSubmit = vi.fn();

  render(<LoginForm onSubmit={onSubmit} />);

  await user.type(screen.getByLabelText(/email/i), 'test@example.com');
  await user.type(screen.getByLabelText(/password/i), 'password123');
  await user.click(screen.getByRole('button', { name: /submit/i }));

  await waitFor(() => {
    expect(onSubmit).toHaveBeenCalledWith({
      email: 'test@example.com',
      password: 'password123',
    });
  });
});

it('shows validation errors', async () => {
  const user = userEvent.setup();
  render(<LoginForm />);

  await user.click(screen.getByRole('button', { name: /submit/i }));

  expect(await screen.findByText(/email is required/i)).toBeInTheDocument();
});

Testing with Router Context

import { RouterProvider, createMemoryHistory } from '@tanstack/react-router';

function renderWithRouter(ui: React.ReactElement, { route = '/' } = {}) {
  const history = createMemoryHistory({ initialEntries: [route] });
  const router = createRouter({ routeTree, history });

  return render(<RouterProvider router={router}>{ui}</RouterProvider>);
}

it('navigates on click', async () => {
  const user = userEvent.setup();
  renderWithRouter(<Navigation />, { route: '/' });

  await user.click(screen.getByRole('link', { name: /about/i }));

  expect(window.location.pathname).toBe('/about');
});

Mocking Server Functions

vi.mock('@/lib/server-functions', () => ({
  getUser: vi.fn(),
  updateUser: vi.fn(),
}));

import { getUser, updateUser } from '@/lib/server-functions';

beforeEach(() => {
  vi.mocked(getUser).mockResolvedValue({ id: '1', name: 'Test User' });
});

it('loads user data', async () => {
  render(<UserProfile userId="1" />);
  expect(await screen.findByText('Test User')).toBeInTheDocument();
  expect(getUser).toHaveBeenCalledWith({ data: { id: '1' } });
});

Common Mistakes

MistakeCorrect Pattern
Using getBy for async contentUse findBy or waitFor for async
Testing implementation detailsTest behavior, not internal state
Using getByTestId firstPrefer getByRole, getByLabelText
Missing userEvent.setup()Always call userEvent.setup() first
Not wrapping in actUse userEvent which handles this
Mocking too muchOnly mock external dependencies
Not cleaning up mocksUse vi.clearAllMocks() in beforeEach
Shared QueryClient in testsCreate fresh QueryClient per test
Retry enabled in test queriesSet retry: false in test QueryClient
Testing third-party componentsTrust the library, test your usage

Delegation

  • Test discovery: For finding untested code paths, use Explore agent
  • Coverage analysis: For comprehensive coverage review, use Task agent
  • Code review: After writing tests, delegate to code-reviewer agent
TaskSkill
Query testing patternstanstack-query
Form testing patternstanstack-form
Router testing patternstanstack-router
Error boundary testingerror-boundaries
Integration flowsintegration-patterns

Score

Total Score

65/100

Based on repository quality metrics

SKILL.md

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

+20
LICENSE

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

+10
説明文

100文字以上の説明がある

0/10
人気

GitHub Stars 100以上

0/15
最近の活動

1ヶ月以内に更新

+10
フォーク

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

0/5
Issue管理

オープンIssueが50未満

+5
言語

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

+5
タグ

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

+5

Reviews

💬

Reviews coming soon