This document outlines the testing approach for the HackRPI website project to ensure code quality, reliability, and performance.
Our testing strategy uses a pyramid approach with different types of tests:
- Unit Tests - Test individual functions and components in isolation
- Component Tests - Test React components with their direct dependencies
- Integration Tests - Test interactions between components
- End-to-End Tests - Test complete user flows through the application
We aim for the following coverage targets:
- Critical Utils (timer.ts, schedule.ts): 90% line coverage
- Server Actions (actions.ts): 80% line coverage
- React Components: 70% line coverage
- Overall Project: At least a 40% baseline coverage
- Focus on testing one thing at a time
- Use descriptive test names that explain behavior
- Group related tests using
describeblocks - Arrange-Act-Assert pattern for clear structure
- Mock external dependencies (API calls, etc.)
Example:
// Example unit test for a utility function
describe("calculateDeltaTime", () => {
it("returns zero values when end time is earlier than current time", () => {
// Arrange
const currentTime = new Date("2025-01-02");
const endTime = new Date("2025-01-01");
// Act
const result = calculateDeltaTime(currentTime, endTime);
// Assert
expect(result.seconds).toBe(0);
expect(result.minutes).toBe(0);
expect(result.hours).toBe(0);
expect(result.days).toBe(0);
expect(result.months).toBe(0);
});
});- Test rendering with different props
- Test user interactions (clicks, inputs)
- Test accessibility features
- Use screen queries based on roles and text
- Avoid testing implementation details
Example:
// Example component test
it("renders speaker information when available", () => {
render(
<EventCard
event={{
id: "123",
title: "Workshop",
speaker: "Jane Doe",
location: "Room 101",
// ...other props
}}
/>,
);
// Check for speaker info
expect(screen.getByText("Room 101 • Jane Doe")).toBeInTheDocument();
});
it("omits speaker bullet point when no speaker is provided", () => {
render(
<EventCard
event={{
id: "123",
title: "Workshop",
speaker: "",
location: "Room 101",
// ...other props
}}
/>,
);
// Check location without bullet point
expect(screen.getByText("Room 101")).toBeInTheDocument();
expect(screen.queryByText("Room 101 •")).not.toBeInTheDocument();
});- Test component interactions
- Test routing and navigation
- Use fake timers for predictable timing
- Test page transitions and state management
Example:
// Example integration test
it("navigates to event page when event link is clicked", async () => {
const { user } = renderWithProviders(<Home />);
// Find and click the event link
const eventLink = screen.getByRole("link", { name: /event/i });
await act(async () => {
await user.click(eventLink);
jest.runAllTimers();
});
// Verify navigation
expect(mockRouterPush).toHaveBeenCalledWith("/event");
});- Test keyboard navigation
- Verify all interactive elements have accessible names
- Check heading hierarchy
- Ensure proper focus management
Example:
// Example accessibility test
it("maintains proper focus management for keyboard users", async () => {
const { user } = renderWithProviders(<NavBar />);
// Tab through navigation
const firstLink = screen.getByRole("link", { name: /home/i });
firstLink.focus();
await act(async () => {
await user.tab();
jest.runAllTimers();
});
// Second link should now have focus
const secondLink = screen.getByRole("link", { name: /event/i });
expect(document.activeElement).toBe(secondLink);
});Organize tests to mirror the source code structure:
__tests__/
├── unit/ # Unit tests for utility functions
├── components/ # Component tests
├── integration/ # Integration tests
└── e2e/ # End-to-end tests (future)
npm test- Run all testsnpm run test:watch- Run tests in watch modenpm run test:ci- Run tests with coverage report
Tests are run automatically on every pull request. PRs must pass all tests and maintain coverage thresholds before being merged.
- Use Jest mocks for external dependencies
- Use the
renderWithProvidersutility for consistent component rendering - Use fake timers for predictable time-based testing
- Mock routing for navigation testing
- Increase test coverage for critical components
- Add end-to-end tests with Cypress or Playwright
- Add visual regression testing
- Implement performance testing
This document outlines the best practices for writing maintainable tests for the HackRPI website. Following these guidelines will ensure that tests remain robust even when content changes.
- Avoid hardcoded content assertions - Tests should be resilient to changes in text content
- Use data-testid attributes - Add testid attributes to key elements for reliable selection
- Use pattern matching - Prefer regex patterns over exact string matching
- Test structure not specific content - Focus tests on component structure and behavior
- Use test constants - Define expected values in a central location for easier updates
// ❌ Avoid - This is brittle when content changes
const heading = screen.getByText("Exactly This Heading");
// ✅ Better - Use data-testid for reliable selection
const heading = screen.getByTestId("page-heading");// ❌ Avoid - This will break when the date changes
expect(screen.getByText("November 9-10, 2024")).toBeInTheDocument();
// ✅ Better - Use patterns that focus on structure not exact dates
expect(screen.getByText(/November \d+-\d+, 202\d/)).toBeInTheDocument();
// ✅ Best - Use data-testid and then check flexible pattern
const dateElement = screen.getByTestId("event-date");
expect(dateElement.textContent).toMatch(/November \d+-\d+, 202\d/);// Define in a central location
const CURRENT_THEME = "Retro vs. Modern";
const HACKRPI_YEAR = "2025";
// Then use in tests
expect(themeElement.textContent).toBe(CURRENT_THEME);The test-utils.tsx file provides utility functions to help with content checking:
getCurrentHackrpiYear()- Returns the current HackRPI yeargetHackrpiMonth()- Returns the event monthgetDatePattern()- Creates consistent date patterns for testinggenerateTestId- Utility for creating standardized data-testid values
Following a consistent naming convention for data-testid attributes makes tests more maintainable:
// Component level
<div data-testid="faq-section">...</div>
// List items
<div data-testid="faq-item-0">...</div>
<div data-testid="faq-item-1">...</div>
// Content elements
<h2 data-testid="faq-title-0">...</h2>
<div data-testid="faq-content-0">...</div>Use the generateTestId utility from test-utils.tsx to create consistent IDs:
// Creating ids
const sectionId = generateTestId.section("faq"); // "faq-section"
const listItemId = generateTestId.listItem("faq", 0); // "faq-item-0"
const contentId = generateTestId.content("title", "faq", 0); // "faq-title-0"Instead of testing specific CSS classes or styles, test structure relationships:
// ❌ Avoid - Testing implementation details
expect(container.querySelector(".card-header")).toHaveClass("text-2xl");
// ✅ Better - Test structural relationships
const header = screen.getByTestId("card-header");
const content = screen.getByTestId("card-content");
expect(header.parentElement).toContainElement(content);For components that are used in multiple tests, create standardized mocks:
// In test-utils.tsx or a dedicated mocks file
export const mockRegistrationLink = () => {
jest.mock("@/components/themed-components/registration-link", () => {
return function MockRegistrationLink({ className }) {
return (
<div data-testid="registration-link" className={className} role="link" aria-label="Registration Link">
Registration Link
</div>
);
};
});
};Always include accessibility checks in your component tests:
it("is accessible", () => {
const { container } = render(<MyComponent />);
checkAccessibility(container);
});Structure tests with clear sections:
it("updates counter when button is clicked", async () => {
// Arrange
const { user } = renderWithProviders(<Counter />);
const button = screen.getByRole("button");
// Act
await user.click(button);
// Assert
expect(screen.getByText("Count: 1")).toBeInTheDocument();
});Don't just test the happy path. Include tests for:
it("shows fallback content when data is empty", () => {
render(<DataDisplay data={[]} />);
expect(screen.getByText("No data available")).toBeInTheDocument();
});
it("handles API errors gracefully", async () => {
// Mock API error
mockApi.mockRejectedValueOnce(new Error("API Error"));
render(<DataComponent />);
// Wait for error state
const errorMessage = await screen.findByText(/something went wrong/i);
expect(errorMessage).toBeInTheDocument();
});- Relying on absolute positions or styling
- Using exact text matching for variable content
- Testing third-party component internals
- Asserting on implementation details instead of behavior
- Not isolating tests properly
- Creating brittle time-based tests
- Not testing responsive behavior
When a test is failing:
- Use
screen.debug()to see the current DOM state - Check console errors in tests with a console spy
- Isolate the failing test with
test.only() - Break complex tests into smaller, focused tests
- Verify your mocks are working correctly
To improve test maintainability and reduce redundancies, we've created a centralized mock registry in __tests__/__mocks__/mockRegistry.tsx. This file contains reusable mock implementations for common components and browser APIs used throughout the test suite.
- MockRegistrationLink: A consistent mock for the RegistrationLink component used in multiple test files
- MockIntersectionObserver: An enhanced IntersectionObserver mock with full simulation capabilities
- commonAccessibilityChecks: Standardized accessibility checks that can be reused across component tests
- createMockFormEvent: Helper to create mock form submission events with proper typing
// Import the mocks you need
import { MockRegistrationLink, commonAccessibilityChecks } from "../__mocks__/mockRegistry";
// Use in your jest.mock calls
jest.mock("@/components/themed-components/registration-link", () => {
return MockRegistrationLink;
});
// Use in your tests
it("passes accessibility checks", () => {
const { container } = render(<MyComponent />);
commonAccessibilityChecks(container);
});- Consistency: Ensures all tests use the same implementation of common mocks
- Maintainability: Changes to mock behavior only need to be made in one place
- Reduced Duplication: Eliminates redundant code across test files
- Type Safety: All mocks are properly typed for better IDE support