Skip to main content

Overview

PyqDeck uses a pragmatic testing strategy: unit tests for backend logic and component tests for UI. We prioritize tests that give the most confidence with the least maintenance burden.

Testing Stack

LayerToolPurpose
Backend Unit/IntegrationVitestServices, repositories, controllers, middleware
Backend Load Testingk6API performance and stress testing
Frontend ComponentsVitest + StorybookUI component testing
E2E (future)PlaywrightFull user journey testing

When to Write What

Backend Tests (Vitest)

Write Vitest tests for:
  • Services - Business logic (e.g., paper parsing, question extraction)
  • Repositories - Database queries and aggregations
  • Controllers - Request/response handling, validation
  • Middleware - Auth, pagination, rate limiting, error handling
  • Utilities - Formatters, validators, error classes
Rule of thumb: If it has logic (conditionals, loops, transformations), it needs tests.

Load Tests (k6)

Write k6 tests for:
  • Critical paths - /papers, /questions, /search
  • Before major releases - Verify performance hasn’t degraded
  • After database changes - Ensure query performance is acceptable

Frontend Tests (Storybook + Vitest)

Write component tests for:
  • Complex UI components - Those with multiple states (loading, error, empty)
  • Interactive components - Forms, filters, modals
  • Accessibility-critical components - Navigation, forms, modals
Rule of thumb: If a component has props that change its appearance/behavior, test it.

Backend Testing Guide

Test Structure

Tests live in backend/tests/ mirroring the source structure:
backend/tests/
  setup.js              -- Global setup (MongoMemoryServer, mocks)
  controllers/
    subject.test.js
    paper.test.js
  services/
    paper.test.js
    university.test.js
  repositories/
    university.test.js
    paper.test.js
  middlewares/
    pagination.test.js
    rateLimiter.test.js
  utils/
    formatters.test.js
    validators.test.js
  load/
    smoke.js
    stress.js

Example: Service Test

import { describe, it, expect, vi } from "vitest";
import { PaperService } from "../../src/services/paper.service.js";

describe("PaperService", () => {
  it("should return papers filtered by university", async () => {
    const mockRepo = {
      findByUniversity: vi
        .fn()
        .mockResolvedValue([
          { id: "1", title: "Math 2023", universityId: "u1" },
        ]),
    };
    const service = new PaperService(mockRepo);

    const result = await service.getPapersByUniversity("u1");

    expect(result).toHaveLength(1);
    expect(mockRepo.findByUniversity).toHaveBeenCalledWith("u1");
  });
});

Running Tests

# Run all tests
pnpm test

# Watch mode (for development)
pnpm test:watch

# With coverage
pnpm test:coverage

Coverage Thresholds

The project enforces minimum coverage:
MetricThreshold
Lines80%
Functions80%
Statements80%
Branches65%

Load Testing Guide

Running Load Tests

# Smoke test (3 VUs, 1 minute)
pnpm test:load

# Stress test (ramp to 50 VUs)
pnpm test:stress

Smoke Test Example

// tests/load/smoke.js
import http from "k6/http";
import { check, sleep } from "k6";

export const options = {
  vus: 3,
  duration: "1m",
};

export default function () {
  const res = http.get("http://localhost:3000/api/v1/health");
  check(res, {
    "status is 200": (r) => r.status === 200,
    "response time < 200ms": (r) => r.timings.duration < 200,
  });
  sleep(1);
}

Frontend Testing Guide

Component Testing with Storybook

Each UI component has a matching Storybook story:
// components/ui/button.stories.jsx
import { Button } from "./button";

export default {
  component: Button,
  title: "UI/Button",
};

export const Primary = {
  args: { variant: "primary", children: "Click me" },
};

export const Loading = {
  args: { loading: true, children: "Loading..." },
};
Run Storybook tests:
cd frontend
pnpm storybook

Running Frontend Tests

cd frontend
pnpm test  # Runs Storybook component tests via Vitest

CI Integration

All tests run in CI on every PR:
  1. Backend tests - Vitest with coverage thresholds
  2. Contract check - Verifies openapi.json is in sync
  3. Frontend build - Validates SDK generation and Next.js build
  4. Load tests - Run against a live MongoDB service
Tests must pass before deployment. No exceptions.

Anti-Patterns to Avoid

  • Don’t test implementation details - Test behavior, not internal state
  • Don’t mock everything - Use real MongoDB (via MongoMemoryServer) when possible
  • Don’t skip error path tests - Test failure cases as thoroughly as success cases
  • Don’t write E2E tests for everything - Reserve E2E for critical user journeys only

Next Steps