Our Testing Philosophy
Understanding the framework's comprehensive testing strategy and quality standards.
Testing Principles
Strapi-NextGen Framework was built with testing as a first-class concern, not an afterthought.
1. Production-Ready Quality
Goal: 96.59% code coverage with real-world test scenarios
✅ 220 tests passing
✅ 96.59% statements covered
✅ 96% functions covered
✅ 94.52% branches covered
✅ 12 test suitesWhy? Because frameworks used in production must be bulletproof.
2. Test What Matters
We don't chase 100% coverage. We test:
- ✅ Critical user paths
- ✅ Error handling
- ✅ Edge cases
- ✅ Integration points
- ❌ Not: trivial getters/setters
- ❌ Not: type definitions
3. Fail Fast, Fix Faster
Automated Quality Gates:
- CI/CD blocks PRs with failing tests
- Coverage regression prevention
- Automated accessibility testing
- Performance benchmarking
Testing Pyramid
┌─────────────┐
│ E2E │ 15% - User workflows
│ (115 tests)│
├─────────────┤
│ Integration │ 15% - API interactions
│ (18 tests) │
├─────────────┤
│ Unit │ 70% - Component logic
│ (87 tests) │
└─────────────┘Unit Tests (70%)
Purpose: Test individual functions in isolation
Example:
// src/helpers/__tests__/metadata.test.ts
describe('generateStrapiMetadata', () => {
it('should generate basic metadata from SEO', () => {
const seo = {
metaTitle: 'Test Page',
metaDescription: 'Test description',
};
const result = generateStrapiMetadata(seo);
expect(result.title).toBe('Test Page');
expect(result.description).toBe('Test description');
});
it('should handle null SEO gracefully', () => {
const result = generateStrapiMetadata(null);
expect(result).toEqual({});
});
});Coverage: All helper functions, utilities, SDK methods
Integration Tests (15%)
Purpose: Test interactions with external systems
Example:
// src/sdk/__tests__/integration/strapi-connection.test.ts
describe('Strapi SDK Integration', () => {
it('should connect to real Strapi instance', async () => {
const sdk = createStrapiSDK({
url: process.env.TEST_STRAPI_URL!,
});
const data = await sdk.rawQuery(GetHealthDocument, {});
expect(data).toBeDefined();
});
});Coverage: GraphQL client, cache tags, authentication
E2E Tests (15%)
Purpose: Test complete user workflows
Example:
// e2e/blog.spec.ts
test('user can read blog article', async ({ page }) => {
await page.goto('/blog/test-article');
// Verify content loads
await expect(page.locator('h1')).toContainText('Test Article');
// Verify image displays
await expect(page.locator('img[alt="Hero"]')).toBeVisible();
// Verify navigation works
await page.click('text=Back to Blog');
await expect(page).toHaveURL('/blog');
});Coverage: Full pages, navigation, user interactions
Testing Tools
Vitest (Unit & Integration)
Why Vitest?
- ⚡ Fast (Vite-powered)
- 🔧 Great DX (hot reload, watch mode)
- 📊 Built-in coverage (V8)
- 🎯 Jest-compatible API
Configuration:
// vitest.config.ts
export default defineConfig({
test: {
globals: true,
environment: 'node',
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html', 'lcov'],
exclude: [
'node_modules/',
'dist/',
'**/*.d.ts',
'**/__tests__/**',
],
},
},
});Playwright (E2E)
Why Playwright?
- 🌐 Cross-browser (Chromium, Firefox, WebKit)
- 📱 Mobile emulation
- 🎭 Network mocking
- 📸 Visual regression
- ♿ Accessibility testing
Configuration:
// playwright.config.ts
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
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'] } },
{ name: 'Mobile Safari', use: { ...devices['iPhone 12'] } },
],
});Test Organization
Directory Structure
src/
├── components/
│ ├── StrapiImage.tsx
│ └── __tests__/
│ └── StrapiImage.test.tsx
├── helpers/
│ ├── metadata.ts
│ └── __tests__/
│ └── metadata.test.ts
├── sdk/
│ ├── index.ts
│ └── __tests__/
│ ├── sdk-core.test.ts
│ └── integration/
│ └── strapi-connection.test.ts
└── renderer/
├── index.tsx
└── __tests__/
└── renderer.test.tsx
e2e/
├── accessibility.spec.ts
├── performance.spec.ts
├── visual.spec.ts
└── load.spec.tsNaming Conventions
Component: StrapiImage.tsx
Unit Test: StrapiImage.test.tsx
Integration: strapi-connection.test.ts
E2E: blog.spec.tsWhat We Test
1. Happy Paths ✅
Core functionality must work:
it('should render image with valid data', () => {
const data = {
url: '/uploads/image.jpg',
width: 800,
height: 600,
};
render(<StrapiImage data={data} />);
const img = screen.getByRole('img');
expect(img).toBeInTheDocument();
expect(img).toHaveAttribute('src', expect.stringContaining('image.jpg'));
});2. Error Handling ✅
Graceful degradation is critical:
it('should return null when data is null', () => {
const { container } = render(<StrapiImage data={null} />);
expect(container.firstChild).toBeNull();
});
it('should log warning in development for missing URL', () => {
const consoleWarn = vi.spyOn(console, 'warn');
process.env.NODE_ENV = 'development';
render(<StrapiImage data={{}} />);
expect(consoleWarn).toHaveBeenCalledWith(
expect.stringContaining('StrapiImage: Missing required field')
);
});3. Edge Cases ✅
Unexpected inputs must be handled:
it('should handle empty string slug', () => {
const result = generateCacheTag('');
expect(result).toBe('page-default');
});
it('should handle special characters in slug', () => {
const result = generateCacheTag('test/page?id=1');
expect(result).toBe('page-test-page-id-1');
});
it('should handle extremely long slugs', () => {
const longSlug = 'a'.repeat(1000);
const result = generateCacheTag(longSlug);
expect(result.length).toBeLessThan(256); // Max cache key length
});4. Accessibility ✅
WCAG 2.1 Level AA compliance:
// e2e/accessibility.spec.ts
test('homepage is accessible', async ({ page }) => {
await page.goto('/');
const results = await new AxeBuilder({ page }).analyze();
expect(results.violations).toEqual([]);
});90 accessibility tests across 5 browsers
5. Performance ✅
Core Web Vitals monitoring:
// e2e/performance.spec.ts
test('LCP is under 2.5s', async ({ page }) => {
await page.goto('/');
const lcp = await page.evaluate(() => {
return new Promise((resolve) => {
new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
resolve(lastEntry.renderTime || lastEntry.loadTime);
}).observe({ entryTypes: ['largest-contentful-paint'] });
});
});
expect(lcp).toBeLessThan(2500);
});Coverage Goals
Current Coverage
File | Stmts | Branch | Funcs | Lines
----------------------|-------|--------|-------|-------
All files | 96.59 | 94.52 | 96 | 96.59
components | 100 | 92.3 | 100 | 100
StrapiImage.tsx | 100 | 92.3 | 100 | 100
helpers | 100 | 93.1 | 100 | 100
metadata.ts | 100 | 93.1 | 100 | 100
preview | 100 | 100 | 100 | 100
index.ts | 100 | 100 | 100 | 100
renderer | 100 | 100 | 100 | 100
error-boundary.tsx| 100 | 100 | 100 | 100
revalidation | 93.89 | 88.23 | 90 | 93.89
index.ts | 93.89 | 88.23 | 90 | 93.89
sdk | 100 | 95.23 | 100 | 100
index.ts | 100 | 95.23 | 100 | 100Thresholds (CI/CD Enforced)
// vitest.config.ts
thresholds: {
statements: 96, // Locked at current baseline
branches: 94, // Prevents regression
functions: 95, // Must maintain quality
lines: 96, // Enforced in CI/CD
}Testing Best Practices
1. Arrange-Act-Assert
it('should generate cache tag', () => {
// Arrange
const slug = 'test-page';
// Act
const result = generateCacheTag(slug);
// Assert
expect(result).toBe('page-test-page');
});2. One Assertion Per Test
// ❌ Multiple assertions (hard to debug)
it('should handle SEO data', () => {
const result = generateStrapiMetadata(seo);
expect(result.title).toBe('Test');
expect(result.description).toBe('Description');
expect(result.keywords).toEqual(['test']);
});
// ✅ Focused tests (easy to debug)
it('should generate title from SEO', () => {
const result = generateStrapiMetadata({ metaTitle: 'Test' });
expect(result.title).toBe('Test');
});
it('should generate description from SEO', () => {
const result = generateStrapiMetadata({ metaDescription: 'Description' });
expect(result.description).toBe('Description');
});3. Test Behavior, Not Implementation
// ❌ Testing implementation details
it('should call generateCacheTag function', () => {
const spy = vi.spyOn(utils, 'generateCacheTag');
getData('test');
expect(spy).toHaveBeenCalled();
});
// ✅ Testing behavior
it('should cache data with correct tag', async () => {
const result = await getData('test');
expect(result).toBeDefined();
// Verify caching works, not how it's implemented
});4. Use Descriptive Test Names
// ❌ Vague
it('works', () => { /* ... */ });
it('test 1', () => { /* ... */ });
// ✅ Clear
it('should render image when data is provided', () => { /* ... */ });
it('should return null when data is missing', () => { /* ... */ });
it('should log warning in development mode', () => { /* ... */ });Continuous Testing
CI/CD Pipeline
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
unit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run test:coverage
e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npx playwright install --with-deps
- run: npm run test:e2e
accessibility:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npx playwright install --with-deps
- run: npm run test:e2e:a11yQuality Gates
PRs are blocked if:
- ❌ Any test fails
- ❌ Coverage drops below baseline
- ❌ Accessibility violations found
- ❌ Performance budgets exceeded
Running Tests
Unit Tests
# Run all unit tests
npm run test:unit
# Watch mode (development)
npm run test:watch
# Coverage report
npm run test:coverage
# Coverage with UI
npm run test:coverage:uiE2E Tests
# Run all E2E tests
npm run test:e2e
# Run with UI (debug)
npm run test:e2e:ui
# Run specific suite
npm run test:e2e:a11y # Accessibility
npm run test:e2e:perf # Performance
npm run test:e2e:visual # Visual regressionCI Simulation
# Run everything CI runs
npm run build
npm run test:unit
npm run test:e2e
npm run test:coverageTest-Driven Development
TDD Workflow
-
Write failing test
it('should generate metadata', () => { const result = generateStrapiMetadata(seo); expect(result.title).toBe('Test'); }); -
Run test (fails) ❌
npm run test:watch # FAIL: generateStrapiMetadata is not defined -
Write minimal code
export function generateStrapiMetadata(seo: StrapiSEO) { return { title: seo?.metaTitle }; } -
Run test (passes) ✅
# PASS: should generate metadata -
Refactor & repeat
Debugging Tests
Vitest Debug
# Run specific test file
npm run test:unit src/helpers/__tests__/metadata.test.ts
# Run specific test
npm run test:unit -- -t "should generate metadata"
# Debug in VS Code
# Add breakpoint → F5 → "Debug Vitest Tests"Playwright Debug
# Run with UI mode
npm run test:e2e:ui
# Debug specific test
npx playwright test e2e/blog.spec.ts --debug
# Headed mode (see browser)
npx playwright test --headedNext Steps
- Quick Start Tutorial - Build your first test
- Caching Strategy - Understand what to test
- API Reference - Test all framework APIs