318 lines
7.1 KiB
Markdown
318 lines
7.1 KiB
Markdown
# Testing
|
|
|
|
This guide covers the testing methodology for DMARQ, including unit tests, integration tests, and end-to-end testing.
|
|
|
|
## Testing Philosophy
|
|
|
|
DMARQ follows a comprehensive testing approach to ensure reliability:
|
|
|
|
- **Unit Tests**: Test individual functions and classes in isolation
|
|
- **Integration Tests**: Test components working together
|
|
- **End-to-End Tests**: Test the complete application flow
|
|
- **Performance Tests**: Ensure the system can handle expected load
|
|
|
|
## Test Structure
|
|
|
|
The test directory structure follows the application structure:
|
|
|
|
```
|
|
backend/app/tests/
|
|
├── conftest.py # Pytest fixtures and configuration
|
|
├── test_api.py # API endpoint tests
|
|
├── test_dmarc_parser.py # DMARC parser tests
|
|
├── test_models.py # Database model tests
|
|
├── test_reports_api.py # Reports API tests
|
|
├── unit/ # Unit tests
|
|
│ ├── test_domain_validator.py
|
|
│ ├── test_utils.py
|
|
│ └── ...
|
|
├── integration/ # Integration tests
|
|
│ ├── test_database.py
|
|
│ ├── test_imap.py
|
|
│ └── ...
|
|
└── e2e/ # End-to-end tests
|
|
├── test_report_flow.py
|
|
└── ...
|
|
```
|
|
|
|
## Setting Up the Test Environment
|
|
|
|
### Prerequisites
|
|
|
|
- Python 3.9+
|
|
- pytest and required plugins
|
|
|
|
### Installation
|
|
|
|
```bash
|
|
cd backend
|
|
pip install -r requirements-dev.txt
|
|
```
|
|
|
|
This will install:
|
|
- pytest
|
|
- pytest-cov (for coverage reports)
|
|
- pytest-mock (for mocking)
|
|
- pytest-asyncio (for async tests)
|
|
|
|
## Running Tests
|
|
|
|
### All Tests
|
|
|
|
To run all tests:
|
|
|
|
```bash
|
|
cd backend
|
|
pytest
|
|
```
|
|
|
|
### Specific Tests
|
|
|
|
To run specific test files:
|
|
|
|
```bash
|
|
pytest tests/test_dmarc_parser.py
|
|
```
|
|
|
|
To run tests matching a pattern:
|
|
|
|
```bash
|
|
pytest -k "parser" # Runs tests with "parser" in the name
|
|
```
|
|
|
|
### Test Coverage
|
|
|
|
To generate a coverage report:
|
|
|
|
```bash
|
|
pytest --cov=app
|
|
```
|
|
|
|
For an HTML coverage report:
|
|
|
|
```bash
|
|
pytest --cov=app --cov-report=html
|
|
```
|
|
|
|
Then open `htmlcov/index.html` to view the report.
|
|
|
|
## Writing Tests
|
|
|
|
### Fixtures
|
|
|
|
We use pytest fixtures for test setup and teardown. Common fixtures are defined in `conftest.py`:
|
|
|
|
```python
|
|
import pytest
|
|
from sqlalchemy import create_engine
|
|
from sqlalchemy.orm import sessionmaker
|
|
from app.models.base import Base
|
|
from app.core.database import get_db
|
|
|
|
@pytest.fixture
|
|
def db_engine():
|
|
engine = create_engine("sqlite:///:memory:")
|
|
Base.metadata.create_all(engine)
|
|
return engine
|
|
|
|
@pytest.fixture
|
|
def db_session(db_engine):
|
|
Session = sessionmaker(bind=db_engine)
|
|
session = Session()
|
|
yield session
|
|
session.close()
|
|
|
|
@pytest.fixture
|
|
def test_app(db_session):
|
|
from app.main import app
|
|
app.dependency_overrides[get_db] = lambda: db_session
|
|
return app
|
|
```
|
|
|
|
### Unit Tests
|
|
|
|
Unit tests should focus on testing a single function or class in isolation, using mocks for dependencies:
|
|
|
|
```python
|
|
from app.utils.domain_validator import is_valid_domain
|
|
import pytest
|
|
|
|
def test_is_valid_domain():
|
|
# Valid domains
|
|
assert is_valid_domain("example.com") is True
|
|
assert is_valid_domain("sub.example.com") is True
|
|
|
|
# Invalid domains
|
|
assert is_valid_domain("invalid..com") is False
|
|
assert is_valid_domain("a" * 300 + ".com") is False
|
|
```
|
|
|
|
### API Tests
|
|
|
|
API tests use the FastAPI TestClient:
|
|
|
|
```python
|
|
from fastapi.testclient import TestClient
|
|
|
|
def test_get_domains(test_app, db_session):
|
|
# Add test data to db_session
|
|
# ...
|
|
|
|
client = TestClient(test_app)
|
|
response = client.get("/api/v1/domains")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert len(data["domains"]) == 2 # Assuming 2 domains were added
|
|
```
|
|
|
|
### Mocking
|
|
|
|
We use pytest-mock for mocking:
|
|
|
|
```python
|
|
def test_imap_client(mocker):
|
|
# Mock the imaplib.IMAP4_SSL class
|
|
mock_imap = mocker.patch("imaplib.IMAP4_SSL")
|
|
mock_imap.return_value.login.return_value = ("OK", [])
|
|
mock_imap.return_value.select.return_value = ("OK", [b"10"])
|
|
|
|
from app.services.imap_client import IMAPClient
|
|
client = IMAPClient("imap.example.com", "user", "pass")
|
|
result = client.connect()
|
|
|
|
assert result is True
|
|
mock_imap.return_value.login.assert_called_once()
|
|
```
|
|
|
|
### Testing Async Code
|
|
|
|
For async functions, use pytest-asyncio:
|
|
|
|
```python
|
|
import pytest
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_async_function():
|
|
from app.services.report_processor import process_report_async
|
|
result = await process_report_async("test_data")
|
|
assert result is not None
|
|
```
|
|
|
|
## Testing Database Models
|
|
|
|
When testing database models, use an in-memory SQLite database:
|
|
|
|
```python
|
|
def test_domain_model(db_session):
|
|
from app.models.domain import Domain
|
|
|
|
domain = Domain(name="example.com")
|
|
db_session.add(domain)
|
|
db_session.commit()
|
|
|
|
fetched = db_session.query(Domain).filter_by(name="example.com").first()
|
|
assert fetched is not None
|
|
assert fetched.name == "example.com"
|
|
```
|
|
|
|
## Test Data
|
|
|
|
### Sample Files
|
|
|
|
Sample DMARC report files for testing are stored in:
|
|
```
|
|
backend/app/tests/data/
|
|
```
|
|
|
|
These include:
|
|
- Sample XML reports
|
|
- Compressed reports (ZIP, GZ)
|
|
- Invalid reports for error testing
|
|
|
|
### Factories
|
|
|
|
For generating test data, we use factory_boy:
|
|
|
|
```python
|
|
import factory
|
|
from app.models.domain import Domain
|
|
from app.models.report import Report
|
|
|
|
class DomainFactory(factory.Factory):
|
|
class Meta:
|
|
model = Domain
|
|
|
|
name = factory.Sequence(lambda n: f"domain-{n}.com")
|
|
active = True
|
|
|
|
class ReportFactory(factory.Factory):
|
|
class Meta:
|
|
model = Report
|
|
|
|
domain = factory.SubFactory(DomainFactory)
|
|
report_id = factory.Sequence(lambda n: f"report-{n}")
|
|
begin_date = factory.LazyFunction(lambda: datetime.now() - timedelta(days=1))
|
|
end_date = factory.LazyFunction(lambda: datetime.now())
|
|
org_name = "test-org"
|
|
```
|
|
|
|
## Continuous Integration
|
|
|
|
Tests are automatically run on every pull request using GitHub Actions.
|
|
|
|
The CI workflow:
|
|
1. Sets up the test environment
|
|
2. Runs linting checks
|
|
3. Runs the test suite
|
|
4. Generates coverage reports
|
|
5. Reports test results
|
|
|
|
## Performance Testing
|
|
|
|
For performance testing, we use Locust:
|
|
|
|
```bash
|
|
cd backend/performance_tests
|
|
locust -f locustfile.py
|
|
```
|
|
|
|
This starts a web interface at http://localhost:8089 to configure and run performance tests.
|
|
|
|
## Debugging Tests
|
|
|
|
When tests fail, you can use pytest's verbose mode for more details:
|
|
|
|
```bash
|
|
pytest -vv
|
|
```
|
|
|
|
For even more information, add the `-s` flag to show print statements:
|
|
|
|
```bash
|
|
pytest -vvs
|
|
```
|
|
|
|
## Writing Testable Code
|
|
|
|
To make testing easier:
|
|
|
|
1. **Dependency Injection**: Pass dependencies rather than creating them inside functions
|
|
2. **Single Responsibility**: Keep functions focused on a single task
|
|
3. **Pure Functions**: When possible, write pure functions that don't modify state
|
|
4. **Testable Units**: Structure code in small, testable units
|
|
5. **Configuration**: Make configuration injectable for tests
|
|
|
|
## Code Coverage Goals
|
|
|
|
Our coverage goals are:
|
|
- Overall coverage: 80%+
|
|
- Core modules: 90%+
|
|
- API endpoints: 100%
|
|
|
|
## Reporting Bugs
|
|
|
|
If you find a bug:
|
|
1. Write a failing test that reproduces the issue
|
|
2. File an issue describing the bug
|
|
3. Link the failing test in the issue
|
|
4. If possible, submit a PR with a fix |