Overview
OWASP Nest maintains high code quality with comprehensive test coverage. All pull requests must pass automated tests and maintain minimum coverage thresholds.Backend Coverage
Minimum: 95%Pytest with Django integration
Frontend Coverage
Minimum: 95%Jest with React Testing Library
Pull requests that fail tests or drop below coverage thresholds will not be merged.
Running Tests
All Tests
make test
Specific Test Types
- Backend
- Frontend
make test-backend
make test-frontend-unit
# or
cd frontend && pnpm run test:unit
Backend Tests
Test Configuration
Backend tests use pytest with Django plugin:pyproject.toml
[tool.pytest]
ini_options.DJANGO_CONFIGURATION = "Test"
ini_options.DJANGO_SETTINGS_MODULE = "settings.test"
ini_options.addopts = [
"--cov-config=pyproject.toml",
"--cov-fail-under=95", # Minimum 95% coverage
"--cov-precision=2",
"--cov-report=term-missing", # Show missing lines
"--cov-report=xml", # Generate XML report
"--cov=.", # Coverage for all code
"--dist=loadscope", # Distribute tests
"--numprocesses=auto", # Parallel execution
]
Test Structure
backend/tests/
├── test_models.py # Model tests
├── test_api.py # REST API tests
├── test_graphql.py # GraphQL tests
├── test_commands.py # Management command tests
├── test_integrations.py # External service tests
└── fixtures/ # Test fixtures
├── projects.json
└── users.json
Writing Backend Tests
- Model Tests
- API Tests
- GraphQL Tests
- Command Tests
tests/test_models.py
import pytest
from apps.owasp.models import Project
@pytest.mark.django_db
class TestProjectModel:
def test_create_project(self):
"""Test creating a project."""
project = Project.objects.create(
name="Test Project",
description="A test project",
level="Lab",
type="Code",
)
assert project.name == "Test Project"
assert project.level == "Lab"
assert project.slug == "test-project"
def test_project_str(self):
"""Test project string representation."""
project = Project.objects.create(
name="Test Project",
description="Test",
)
assert str(project) == "Test Project"
def test_project_url_validation(self):
"""Test project URL validation."""
with pytest.raises(ValidationError):
Project.objects.create(
name="Test",
url="not-a-url",
)
tests/test_api.py
import pytest
from django.test import Client
from apps.owasp.models import Project
@pytest.mark.django_db
class TestProjectsAPI:
def test_list_projects(self):
"""Test listing projects."""
# Arrange
Project.objects.create(
name="Project 1",
description="Test 1",
)
Project.objects.create(
name="Project 2",
description="Test 2",
)
# Act
client = Client()
response = client.get("/api/v0/projects/")
# Assert
assert response.status_code == 200
data = response.json()
assert len(data["projects"]) == 2
assert data["projects"][0]["name"] == "Project 1"
def test_get_project_detail(self):
"""Test getting project detail."""
project = Project.objects.create(
name="Test Project",
description="Test description",
)
client = Client()
response = client.get(f"/api/v0/projects/{project.slug}/")
assert response.status_code == 200
data = response.json()
assert data["name"] == "Test Project"
def test_filter_projects_by_level(self):
"""Test filtering projects by level."""
Project.objects.create(name="Lab", level="Lab")
Project.objects.create(name="Flagship", level="Flagship")
client = Client()
response = client.get("/api/v0/projects/?level=Flagship")
assert response.status_code == 200
data = response.json()
assert len(data["projects"]) == 1
assert data["projects"][0]["level"] == "Flagship"
tests/test_graphql.py
import pytest
from django.test import Client
from apps.owasp.models import Project
@pytest.mark.django_db
class TestProjectsGraphQL:
def test_projects_query(self):
"""Test projects GraphQL query."""
# Arrange
Project.objects.create(
name="Test Project",
description="Test",
)
query = '''
query {
projects(first: 10) {
edges {
node {
id
name
description
}
}
}
}
'''
# Act
client = Client()
response = client.post(
"/graphql/",
{"query": query},
content_type="application/json",
)
# Assert
assert response.status_code == 200
data = response.json()["data"]
assert len(data["projects"]["edges"]) == 1
assert data["projects"]["edges"][0]["node"]["name"] == "Test Project"
def test_project_by_key_query(self):
"""Test single project query."""
project = Project.objects.create(
name="Test Project",
description="Test description",
)
query = f'''
query {{
project(key: "{project.slug}") {{
id
name
description
}}
}}
'''
client = Client()
response = client.post(
"/graphql/",
{"query": query},
content_type="application/json",
)
assert response.status_code == 200
data = response.json()["data"]
assert data["project"]["name"] == "Test Project"
tests/test_commands.py
import pytest
from io import StringIO
from django.core.management import call_command
from apps.owasp.models import Project
@pytest.mark.django_db
class TestManagementCommands:
def test_algolia_reindex_command(self):
"""Test algolia_reindex command."""
Project.objects.create(
name="Test Project",
description="Test",
)
out = StringIO()
call_command('algolia_reindex', stdout=out)
assert "Successfully indexed" in out.getvalue()
def test_sync_data_command(self, mocker):
"""Test sync_data command with mocked GitHub API."""
# Mock GitHub API
mock_github = mocker.patch('apps.github.client.Github')
out = StringIO()
call_command('sync_data', stdout=out)
assert mock_github.called
assert "Sync complete" in out.getvalue()
Fixtures
- pytest Fixtures
- Django Fixtures
tests/conftest.py
import pytest
from apps.owasp.models import Project, Chapter
from apps.github.models import GitHubUser
@pytest.fixture
def sample_project():
"""Create a sample project for testing."""
return Project.objects.create(
name="Sample Project",
description="A sample project for testing",
level="Lab",
)
@pytest.fixture
def sample_user():
"""Create a sample GitHub user."""
return GitHubUser.objects.create(
login="testuser",
name="Test User",
email="[email protected]",
)
@pytest.fixture
def authenticated_client(sample_user):
"""Create an authenticated client."""
from django.test import Client
client = Client()
client.force_login(sample_user)
return client
def test_with_fixtures(sample_project, authenticated_client):
response = authenticated_client.get(
f"/api/v0/projects/{sample_project.slug}/"
)
assert response.status_code == 200
tests/fixtures/projects.json
[
{
"model": "owasp.project",
"pk": 1,
"fields": {
"name": "OWASP Top 10",
"description": "Top 10 Web Application Security Risks",
"level": "Flagship",
"type": "Documentation"
}
}
]
@pytest.mark.django_db
def test_with_django_fixture(django_db_setup, django_db_blocker):
from django.core.management import call_command
with django_db_blocker.unblock():
call_command('loaddata', 'tests/fixtures/projects.json')
assert Project.objects.count() == 1
Mocking External Services
tests/test_integrations.py
import pytest
from unittest.mock import Mock, patch
from apps.github.services import GitHubService
@pytest.mark.django_db
class TestGitHubIntegration:
@patch('apps.github.services.Github')
def test_fetch_repositories(self, mock_github):
"""Test fetching repositories from GitHub."""
# Arrange
mock_repo = Mock()
mock_repo.name = "test-repo"
mock_repo.description = "Test repository"
mock_org = Mock()
mock_org.get_repos.return_value = [mock_repo]
mock_github.return_value.get_organization.return_value = mock_org
# Act
service = GitHubService()
repos = service.fetch_repositories("OWASP")
# Assert
assert len(repos) == 1
assert repos[0].name == "test-repo"
mock_github.return_value.get_organization.assert_called_with("OWASP")
Frontend Tests
Test Configuration
Frontend tests use Jest with React Testing Library:jest.config.ts
const config: Config = {
collectCoverage: true,
coverageThreshold: {
global: {
branches: 95,
functions: 95,
lines: 95,
statements: 95,
},
},
testEnvironment: 'jest-environment-jsdom',
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
}
Test Structure
frontend/__tests__/
├── unit/ # Unit tests
│ ├── components/
│ │ ├── ProjectCard.test.tsx
│ │ └── SearchBar.test.tsx
│ └── utils/
│ └── formatDate.test.ts
├── a11y/ # Accessibility tests
│ └── pages.test.tsx
├── e2e/ # End-to-end tests
│ ├── auth.spec.ts
│ └── projects.spec.ts
└── mockData/ # Test data
└── projects.ts
Writing Frontend Tests
- Component Tests
- Hook Tests
- User Interaction
__tests__/unit/components/ProjectCard.test.tsx
import { render, screen } from '@testing-library/react'
import { ProjectCard } from '@/components/ProjectCard'
describe('ProjectCard', () => {
const mockProject = {
id: '1',
name: 'OWASP Top 10',
description: 'Top 10 Web Application Security Risks',
level: 'Flagship',
url: 'https://owasp.org/www-project-top-ten/',
}
it('renders project information', () => {
render(<ProjectCard project={mockProject} />)
expect(screen.getByText('OWASP Top 10')).toBeInTheDocument()
expect(screen.getByText(/Top 10 Web Application/)).toBeInTheDocument()
expect(screen.getByText('Flagship')).toBeInTheDocument()
})
it('renders a link to the project', () => {
render(<ProjectCard project={mockProject} />)
const link = screen.getByRole('link', { name: /view project/i })
expect(link).toHaveAttribute('href', mockProject.url)
})
it('displays project level badge', () => {
render(<ProjectCard project={mockProject} />)
const badge = screen.getByText('Flagship')
expect(badge).toHaveClass('badge-flagship')
})
})
__tests__/unit/hooks/useProjects.test.tsx
import { renderHook, waitFor } from '@testing-library/react'
import { MockedProvider } from '@apollo/client/testing'
import { useProjects } from '@/hooks/useProjects'
import { GetProjectsDocument } from '@/app/projects/queries.generated'
describe('useProjects', () => {
const mocks = [
{
request: {
query: GetProjectsDocument,
variables: { first: 10 },
},
result: {
data: {
projects: {
edges: [
{
node: {
id: '1',
name: 'OWASP Top 10',
description: 'Test',
},
},
],
},
},
},
},
]
it('fetches and returns projects', async () => {
const wrapper = ({ children }) => (
<MockedProvider mocks={mocks} addTypename={false}>
{children}
</MockedProvider>
)
const { result } = renderHook(() => useProjects(10), { wrapper })
expect(result.current.loading).toBe(true)
await waitFor(() => {
expect(result.current.loading).toBe(false)
})
expect(result.current.projects).toHaveLength(1)
expect(result.current.projects[0].name).toBe('OWASP Top 10')
})
})
__tests__/unit/components/SearchBar.test.tsx
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { SearchBar } from '@/components/SearchBar'
describe('SearchBar', () => {
it('updates search query on input', async () => {
const user = userEvent.setup()
const onSearch = jest.fn()
render(<SearchBar onSearch={onSearch} />)
const input = screen.getByPlaceholderText(/search/i)
await user.type(input, 'OWASP')
expect(input).toHaveValue('OWASP')
})
it('calls onSearch when form is submitted', async () => {
const user = userEvent.setup()
const onSearch = jest.fn()
render(<SearchBar onSearch={onSearch} />)
const input = screen.getByPlaceholderText(/search/i)
await user.type(input, 'OWASP{enter}')
expect(onSearch).toHaveBeenCalledWith('OWASP')
})
it('clears search on clear button click', async () => {
const user = userEvent.setup()
render(<SearchBar onSearch={jest.fn()} />)
const input = screen.getByPlaceholderText(/search/i)
await user.type(input, 'OWASP')
const clearButton = screen.getByRole('button', { name: /clear/i })
await user.click(clearButton)
expect(input).toHaveValue('')
})
})
Accessibility Tests
__tests__/a11y/pages.test.tsx
import { render } from '@testing-library/react'
import { axe, toHaveNoViolations } from 'jest-axe'
import Home from '@/app/page'
expect.extend(toHaveNoViolations)
describe('Accessibility', () => {
it('home page has no accessibility violations', async () => {
const { container } = render(<Home />)
const results = await axe(container)
expect(results).toHaveNoViolations()
})
it('projects page has no accessibility violations', async () => {
const { container } = render(<ProjectsPage />)
const results = await axe(container)
expect(results).toHaveNoViolations()
})
})
E2E Tests with Playwright
__tests__/e2e/projects.spec.ts
import { test, expect } from '@playwright/test'
test.describe('Projects', () => {
test('should display project list', async ({ page }) => {
await page.goto('/projects')
// Check page title
await expect(page).toHaveTitle(/Projects/)
// Check projects are displayed
const projects = page.locator('[data-testid="project-card"]')
await expect(projects).toHaveCount(10)
})
test('should filter projects by level', async ({ page }) => {
await page.goto('/projects')
// Select filter
await page.selectOption('[name="level"]', 'Flagship')
// Wait for results
await page.waitForLoadState('networkidle')
// Check filtered results
const projects = page.locator('[data-testid="project-card"]')
const firstProject = projects.first()
await expect(firstProject).toContainText('Flagship')
})
test('should navigate to project detail', async ({ page }) => {
await page.goto('/projects')
// Click first project
await page.click('[data-testid="project-card"]:first-child a')
// Check detail page
await expect(page).toHaveURL(/\/projects\/[^/]+$/)
await expect(page.locator('h1')).toBeVisible()
})
})
Fuzz Testing
Fuzz testing tests API endpoints with random/invalid inputs:make test-fuzz
import schemathesis
schema = schemathesis.from_uri("http://backend:8000/api/docs/openapi.json")
@schema.parametrize()
def test_api_fuzzing(case):
case.call_and_validate()
Security Scanning
Code Security
make security-scan-code
- Semgrep - Static analysis for security patterns
- Trivy - Vulnerability scanning
Image Security
make security-scan-images
- Known vulnerabilities
- Misconfigurations
- Exposed secrets
ZAP Scanning
make security-scan-zap
- XSS vulnerabilities
- SQL injection
- CSRF issues
- Security headers
Coverage Reports
Viewing Coverage
- Backend
- Frontend
# Run tests (generates coverage.xml)
make test-backend
# View in terminal
# Coverage report shown after tests
# Open HTML report
cd backend
coverage html
open htmlcov/index.html
# Run tests (generates coverage/)
make test-frontend-unit
# Open HTML report
cd frontend
open coverage/lcov-report/index.html
Coverage Thresholds
Both backend and frontend require 95% coverage:[tool.pytest]
ini_options.addopts = [
"--cov-fail-under=95",
]
CI/CD Integration
Tests run automatically on every pull request:.github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
backend-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run backend tests
run: make test-backend
- name: Upload coverage
uses: codecov/codecov-action@v3
frontend-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run frontend tests
run: make test-frontend
- name: Upload coverage
uses: codecov/codecov-action@v3
Best Practices
Write Tests First
Write Tests First
Follow Test-Driven Development (TDD):
- Write failing test
- Implement feature
- Make test pass
- Refactor
Test Behavior, Not Implementation
Test Behavior, Not Implementation
// Bad: Testing implementation
expect(component.state.count).toBe(1)
// Good: Testing behavior
expect(screen.getByText('Count: 1')).toBeInTheDocument()
Use Descriptive Test Names
Use Descriptive Test Names
# Bad
def test_project():
pass
# Good
def test_create_project_with_valid_data_succeeds():
pass
Mock External Dependencies
Mock External Dependencies
Always mock:
- External APIs (GitHub, Slack, OpenAI)
- File system operations
- Network requests
- Time-dependent functions
Keep Tests Independent
Keep Tests Independent
Each test should:
- Run independently
- Not depend on other tests
- Clean up after itself
- Use fixtures for setup
Test Edge Cases
Test Edge Cases
Test:
- Empty inputs
- Invalid data
- Boundary conditions
- Error scenarios
Debugging Tests
- Backend
- Frontend
# Add breakpoint
import pdb; pdb.set_trace()
# Run single test
pytest tests/test_models.py::TestProjectModel::test_create_project
# Run with verbose output
pytest -vv
# Show print statements
pytest -s
# Run single test file
pnpm run test:unit ProjectCard.test.tsx
# Run in watch mode
pnpm run test:unit --watch
# Debug in VS Code
# Add to .vscode/launch.json:
{
"type": "node",
"request": "launch",
"name": "Jest Debug",
"program": "${workspaceFolder}/node_modules/.bin/jest",
"args": ["--runInBand"],
"console": "integratedTerminal"
}
Next Steps
Contributing
Submit your changes
Backend Guide
Backend development
Frontend Guide
Frontend development
Architecture
System architecture