Skip to content

Frontend Testing

Granit-front uses a mixed testing approach:

  • Unit tests for pure functions (@granit/utils, @granit/logger)
  • React integration tests for hooks and contexts (@granit/react-authentication, @granit/react-authorization)
  • Coverage ≥ 80% on all new code — blocking requirement
ToolRole
Vitest 3Test runner, native ESM compatible
@testing-library/react 16render(), renderHook(), screen, waitFor()
jsdom 26DOM environment for React tests
v8 (coverage)Coverage provider — lcov, HTML, Cobertura reports

The root vitest.config.ts configures the entire workspace:

export default defineConfig({
test: {
globals: true,
environment: 'jsdom',
include: [
'packages/@granit/*/src/**/*.test.ts',
'packages/@granit/*/src/**/*.test.tsx',
],
coverage: {
provider: 'v8',
reporter: ['text', 'lcov', 'html', 'cobertura'],
reportsDirectory: './coverage',
include: ['packages/@granit/*/src/**/*.{ts,tsx}'],
exclude: ['**/*.d.ts', '**/*.test.ts', '**/*.test.tsx', '**/*.spec.ts'],
},
},
});
Terminal window
pnpm test # Watch mode — development
pnpm test:coverage # Single run with coverage
pnpm --filter @granit/react-authentication test # Target a specific package

Tests are co-located with sources in src/__tests__/:

packages/@granit/react-authentication/
└── src/
├── index.ts
├── keycloak-core.ts
├── use-auth-context.ts
├── mock-provider.tsx
└── __tests__/
├── keycloak-core.test.tsx
├── use-auth-context.test.tsx
└── mock-provider.test.tsx
ElementConventionExample
Test file<module>.test.ts(x)logger.test.ts
describe blockFunction or hook namedescribe('createLogger', …)
it blockExpected behavior in Englishit('should log warn in production', …)
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
logger.warn('Token expired');
expect(warnSpy).toHaveBeenCalledWith(
expect.stringContaining('[MyApp]'),
'Token expired',
);
vi.mock('axios', () => ({
default: {
create: vi.fn(() => ({
interceptors: {
request: { use: vi.fn() },
response: { use: vi.fn() },
},
})),
},
}));

Hoisted mocks with mutable state (@granit/react-authentication)

Section titled “Hoisted mocks with mutable state (@granit/react-authentication)”
const mockKeycloak = vi.hoisted(() => ({
init: vi.fn().mockResolvedValue(true),
authenticated: true,
token: 'mock-token',
loadUserInfo: vi.fn().mockResolvedValue({ sub: '123', name: 'Test' }),
updateToken: vi.fn().mockResolvedValue(true),
}));
vi.mock('keycloak-js', () => ({
default: vi.fn(() => mockKeycloak),
}));
import { renderHook } from '@testing-library/react';
const wrapper = ({ children }: { children: React.ReactNode }) => (
<AuthContext.Provider value={mockAuthValue}>
{children}
</AuthContext.Provider>
);
const { result } = renderHook(() => useAuth(), { wrapper });
expect(result.current.authenticated).toBe(true);
  1. One describe per exported function/hook — no flat tests
  2. Self-contained tests — each it is independent, no inter-test dependencies
  3. mockImplementation(() => {}) on console spies to suppress noise
  4. vi.clearAllMocks() in beforeEach for clean state
  5. Exact assertions — prefer toEqual() over toMatchObject() when possible
  6. act() and waitFor() for all async tests involving React hooks
  7. No shared test utilities — each test file is self-sufficient
FormatFileUsage
TextterminalDeveloper (quick summary)
HTMLcoverage/index.htmlDeveloper (detailed exploration)
LCOVcoverage/lcov.infoSonarQube
Coberturacoverage/cobertura-coverage.xmlCI (PR coverage report)