Skip to content

setup-testing

Access: /template setup-testing or /t setup-testing

Configures comprehensive testing infrastructure for your MakerKit application, including unit tests, integration tests, and end-to-end testing.

What It Does

The setup-testing command helps you:

  • Configure Jest for unit and integration testing
  • Set up Playwright for E2E testing
  • Create test utilities and helpers
  • Add test database configuration
  • Implement CI/CD test pipelines
  • Create example test suites

Usage

bash
/template setup-testing "Description"
# or
/t setup-testing "Description"

When prompted, specify:

  • Testing frameworks to configure
  • Test coverage requirements
  • CI/CD integration preferences
  • Test database setup

Prerequisites

  • A MakerKit project with basic structure
  • Understanding of testing requirements
  • Commercial MakerKit license from MakerKit

What Gets Created

1. Jest Configuration

Unit and integration test setup:

javascript
// jest.config.js
const nextJest = require('next/jest');

const createJestConfig = nextJest({
  dir: './',
});

const customJestConfig = {
  setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/$1',
    '^@kit/(.*)$': '<rootDir>/packages/$1/src',
  },
  testEnvironment: 'jest-environment-jsdom',
  testPathIgnorePatterns: ['/node_modules/', '/.next/', '/e2e/'],
  collectCoverageFrom: [
    'app/**/*.{js,jsx,ts,tsx}',
    'packages/**/*.{js,jsx,ts,tsx}',
    '!**/*.d.ts',
    '!**/node_modules/**',
    '!**/.next/**',
    '!**/coverage/**',
    '!**/jest.config.js',
  ],
  coverageThreshold: {
    global: {
      branches: 70,
      functions: 70,
      lines: 70,
      statements: 70,
    },
  },
};

module.exports = createJestConfig(customJestConfig);

2. Test Setup File

Global test configuration:

typescript
// jest.setup.js
import '@testing-library/jest-dom';
import { TextEncoder, TextDecoder } from 'util';
import { server } from './test/mocks/server';

// Polyfills
global.TextEncoder = TextEncoder;
global.TextDecoder = TextDecoder;

// Mock environment variables
process.env.NEXT_PUBLIC_SUPABASE_URL = 'http://localhost:54321';
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY = 'test-anon-key';
process.env.SUPABASE_SERVICE_ROLE_KEY = 'test-service-key';

// MSW server setup
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

// Mock Next.js router
jest.mock('next/navigation', () => ({
  useRouter: () => ({
    push: jest.fn(),
    replace: jest.fn(),
    prefetch: jest.fn(),
  }),
  usePathname: () => '/test',
  useSearchParams: () => new URLSearchParams(),
}));

3. Test Utilities

Helper functions for testing:

typescript
// test/utils/test-utils.tsx
import { ReactElement } from 'react';
import { render, RenderOptions } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { createBrowserClient } from '@supabase/ssr';

// Create a test query client
function createTestQueryClient() {
  return new QueryClient({
    defaultOptions: {
      queries: {
        retry: false,
        cacheTime: 0,
      },
    },
  });
}

// Custom render function
function customRender(
  ui: ReactElement,
  options?: Omit<RenderOptions, 'wrapper'>
) {
  const queryClient = createTestQueryClient();
  const supabase = createBrowserClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
  );

  function Wrapper({ children }: { children: React.ReactNode }) {
    return (
      <QueryClientProvider client={queryClient}>
        <SupabaseProvider client={supabase}>
          {children}
        </SupabaseProvider>
      </QueryClientProvider>
    );
  }

  return render(ui, { wrapper: Wrapper, ...options });
}

// Re-export everything
export * from '@testing-library/react';
export { customRender as render };

// Test data factories
export function createMockUser(overrides?: Partial<User>): User {
  return {
    id: 'user-123',
    email: 'test@example.com',
    display_name: 'Test User',
    created_at: new Date().toISOString(),
    ...overrides,
  };
}

export function createMockTeam(overrides?: Partial<Team>): Team {
  return {
    id: 'team-123',
    name: 'Test Team',
    slug: 'test-team',
    created_at: new Date().toISOString(),
    ...overrides,
  };
}

4. Mock Service Worker Setup

API mocking for tests:

typescript
// test/mocks/handlers.ts
import { rest } from 'msw';

export const handlers = [
  // Auth endpoints
  rest.post('/auth/v1/token', (req, res, ctx) => {
    return res(
      ctx.json({
        access_token: 'mock-access-token',
        refresh_token: 'mock-refresh-token',
        user: createMockUser(),
      })
    );
  }),

  // API endpoints
  rest.get('/api/v1/teams', (req, res, ctx) => {
    return res(
      ctx.json({
        data: [createMockTeam()],
        pagination: { total: 1, limit: 10, offset: 0 },
      })
    );
  }),

  // Supabase endpoints
  rest.get(`${process.env.NEXT_PUBLIC_SUPABASE_URL}/rest/v1/users`, 
    (req, res, ctx) => {
      return res(ctx.json([createMockUser()]));
    }
  ),
];

// test/mocks/server.ts
import { setupServer } from 'msw/node';
import { handlers } from './handlers';

export const server = setupServer(...handlers);

5. Component Tests

Example component test:

typescript
// components/__tests__/team-switcher.test.tsx
import { render, screen, fireEvent, waitFor } from '@/test/utils/test-utils';
import { TeamSwitcher } from '../team-switcher';
import { server } from '@/test/mocks/server';
import { rest } from 'msw';

describe('TeamSwitcher', () => {
  it('displays current team', () => {
    render(<TeamSwitcher currentTeam={createMockTeam()} />);
    
    expect(screen.getByText('Test Team')).toBeInTheDocument();
  });

  it('switches teams on selection', async () => {
    const mockPush = jest.fn();
    jest.mocked(useRouter).mockReturnValue({
      push: mockPush,
    } as any);

    render(<TeamSwitcher currentTeam={createMockTeam()} />);
    
    // Open dropdown
    fireEvent.click(screen.getByRole('button'));
    
    // Click different team
    fireEvent.click(screen.getByText('Other Team'));
    
    await waitFor(() => {
      expect(mockPush).toHaveBeenCalledWith('/home/other-team');
    });
  });

  it('handles API errors gracefully', async () => {
    server.use(
      rest.get('/api/v1/teams', (req, res, ctx) => {
        return res(ctx.status(500));
      })
    );

    render(<TeamSwitcher currentTeam={createMockTeam()} />);
    
    await waitFor(() => {
      expect(screen.getByText('Error loading teams')).toBeInTheDocument();
    });
  });
});

6. Integration Tests

Testing complete features:

typescript
// test/integration/auth-flow.test.tsx
import { render, screen, fireEvent, waitFor } from '@/test/utils/test-utils';
import { SignInPage } from '@/app/auth/sign-in/page';

describe('Authentication Flow', () => {
  it('completes sign in flow', async () => {
    render(<SignInPage />);
    
    // Fill form
    fireEvent.change(screen.getByLabelText(/email/i), {
      target: { value: 'test@example.com' },
    });
    fireEvent.change(screen.getByLabelText(/password/i), {
      target: { value: 'password123' },
    });
    
    // Submit
    fireEvent.click(screen.getByRole('button', { name: /sign in/i }));
    
    await waitFor(() => {
      expect(mockRouter.push).toHaveBeenCalledWith('/dashboard');
    });
  });

  it('displays validation errors', async () => {
    render(<SignInPage />);
    
    // Submit empty form
    fireEvent.click(screen.getByRole('button', { name: /sign in/i }));
    
    await waitFor(() => {
      expect(screen.getByText(/email is required/i)).toBeInTheDocument();
      expect(screen.getByText(/password is required/i)).toBeInTheDocument();
    });
  });
});

7. Playwright E2E Configuration

End-to-end testing setup:

typescript
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './e2e',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: 'html',
  
  use: {
    baseURL: process.env.BASE_URL || 'http://localhost:3000',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
  },

  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] },
    },
    {
      name: 'Mobile Chrome',
      use: { ...devices['Pixel 5'] },
    },
  ],

  webServer: {
    command: 'pnpm dev',
    port: 3000,
    reuseExistingServer: !process.env.CI,
  },
});

8. E2E Test Examples

Full user journey tests:

typescript
// e2e/auth.spec.ts
import { test, expect } from '@playwright/test';

test.describe('Authentication', () => {
  test('user can sign up, verify email, and sign in', async ({ page }) => {
    // Go to sign up
    await page.goto('/auth/sign-up');
    
    // Fill sign up form
    await page.fill('[name="email"]', 'test@example.com');
    await page.fill('[name="password"]', 'SecurePass123!');
    await page.fill('[name="confirmPassword"]', 'SecurePass123!');
    
    // Submit
    await page.click('button[type="submit"]');
    
    // Check for verification message
    await expect(page.locator('text=Check your email')).toBeVisible();
    
    // Simulate email verification (in real test, check email)
    await page.goto('/auth/verify?token=mock-token');
    
    // Should redirect to sign in
    await expect(page).toHaveURL('/auth/sign-in');
    
    // Sign in
    await page.fill('[name="email"]', 'test@example.com');
    await page.fill('[name="password"]', 'SecurePass123!');
    await page.click('button[type="submit"]');
    
    // Should be logged in
    await expect(page).toHaveURL('/dashboard');
    await expect(page.locator('text=Welcome')).toBeVisible();
  });
});

// e2e/subscription.spec.ts
test.describe('Subscription Flow', () => {
  test.use({ storageState: 'e2e/.auth/user.json' }); // Authenticated state
  
  test('user can subscribe to pro plan', async ({ page }) => {
    await page.goto('/billing');
    
    // Click upgrade on pro plan
    await page.click('text=Pro >> ../.. >> button:has-text("Subscribe")');
    
    // Should redirect to Stripe checkout
    await page.waitForURL(/checkout\.stripe\.com/);
    
    // Fill test card details
    await page.fill('[placeholder="Card number"]', '4242424242424242');
    await page.fill('[placeholder="MM / YY"]', '12/25');
    await page.fill('[placeholder="CVC"]', '123');
    
    // Complete purchase
    await page.click('button:has-text("Subscribe")');
    
    // Should redirect back with success
    await page.waitForURL('/billing/success');
    await expect(page.locator('text=Subscription activated')).toBeVisible();
  });
});

9. Test Database Setup

Isolated test database:

typescript
// test/setup/database.ts
import { execSync } from 'child_process';

export async function setupTestDatabase() {
  // Reset database
  execSync('pnpm supabase db reset', { 
    env: { ...process.env, DATABASE_URL: process.env.TEST_DATABASE_URL } 
  });
  
  // Seed test data
  await seedTestData();
}

export async function cleanupTestDatabase() {
  // Clean up after tests
  const client = createTestClient();
  await client.from('users').delete().neq('id', '00000000-0000-0000-0000-000000000000');
}

// Run before all tests
global.beforeAll(async () => {
  await setupTestDatabase();
});

global.afterAll(async () => {
  await cleanupTestDatabase();
});

10. CI/CD Integration

GitHub Actions workflow:

yaml
# .github/workflows/test.yml
name: Tests

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-latest
    
    services:
      postgres:
        image: postgres:14
        env:
          POSTGRES_PASSWORD: postgres
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 5432:5432

    steps:
      - uses: actions/checkout@v3
      
      - uses: pnpm/action-setup@v2
        with:
          version: 8
          
      - uses: actions/setup-node@v3
        with:
          node-version: 18
          cache: 'pnpm'
          
      - run: pnpm install
      
      - name: Run unit tests
        run: pnpm test:unit
        
      - name: Run integration tests
        run: pnpm test:integration
        env:
          DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test
          
      - name: Install Playwright
        run: pnpm exec playwright install --with-deps
        
      - name: Run E2E tests
        run: pnpm test:e2e
        
      - name: Upload coverage
        uses: codecov/codecov-action@v3
        with:
          files: ./coverage/lcov.info
          
      - uses: actions/upload-artifact@v3
        if: always()
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 30

Test Scripts

Add to package.json:

json
{
  "scripts": {
    "test": "jest",
    "test:unit": "jest --testPathPattern='.test.ts$'",
    "test:integration": "jest --testPathPattern='.integration.ts$'",
    "test:e2e": "playwright test",
    "test:e2e:ui": "playwright test --ui",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage"
  }
}

Best Practices

  1. Test Pyramid: More unit tests, fewer E2E tests
  2. Isolation: Each test should be independent
  3. Mocking: Mock external dependencies
  4. Factories: Use test data factories
  5. Parallel: Run tests in parallel when possible

License Requirement

Important: This command requires a commercial MakerKit license from https://makerkit.dev?atp=MqaGgc MakerKit is a premium SaaS starter kit and requires proper licensing for commercial use.

Built with ❤️ for the AI Coding community, by Praney Behl