| name | testing-strategies |
| description | Comprehensive testing patterns and examples for Dagster, dbt, FastAPI, and React in the economic data project. |
Testing Strategies for Economic Data Project
This skill provides comprehensive testing patterns and examples for all components of the economic data project: Dagster (orchestration), dbt (transformations), FastAPI (backend), and React (frontend).
Quick Reference
| Component | Framework | Run Command | Location |
|---|
| Dagster | pytest | make test or cd macro_agents && uv run pytest tests/ -v | macro_agents/tests/ |
| dbt | dbt test | dbt test or dbt test --select model_name | dbt_project/tests/ + schema.yml |
| Backend API | pytest | cd api && PYTHONPATH=.. uv run pytest tests/ -v | api/tests/ |
| Frontend | vitest | cd frontend && npm test -- --run | frontend/tests/ |
1. Dagster Testing
Test Categories
- Unit Tests - Test individual resources and functions
- Integration Tests - Test Dagster definitions load correctly
- Schedule/Sensor Tests - Test automation configuration
- Data Validation Tests - Test data schemas and integrity
Resource Unit Tests
Test Dagster resources in isolation with mocked dependencies:
import pytest
from unittest.mock import Mock, patch
from macro_agents.defs.resources.motherduck import MotherDuckResource
class TestMotherDuckResource:
def test_initialization_dev_environment(self):
resource = MotherDuckResource(
md_token="test_token",
environment="dev",
local_path="test.duckdb"
)
assert resource.environment == "dev"
assert resource.db_connection == "test.duckdb"
def test_initialization_prod_environment(self):
resource = MotherDuckResource(
md_token="test_token",
environment="prod",
md_database="test_db"
)
assert resource.environment == "prod"
assert resource.db_connection == "md:?motherduck_token=test_token"
Testing with Temporary Database
Use tempfile for tests requiring actual database operations:
import tempfile
import os
import polars as pl
def test_table_exists(self):
with tempfile.NamedTemporaryFile(suffix=".duckdb", delete=False) as tmp_file:
resource = MotherDuckResource(
md_token="test_token",
environment="dev",
local_path=tmp_file.name
)
assert not resource.table_exists("non_existent_table")
test_df = pl.DataFrame({"id": [1, 2, 3]})
resource.drop_create_duck_db_table("test_table", test_df)
assert resource.table_exists("test_table")
os.unlink(tmp_file.name)
Integration Tests - Definitions Load
Verify all Dagster definitions load without errors:
from macro_agents.definitions import defs
class TestDagsterDefinitions:
def test_definitions_load(self):
assert defs is not None
assert len(defs.assets) > 0
assert len(defs.resources) > 0
def test_all_resources_are_configurable(self):
for resource_key, resource in defs.resources.items():
assert hasattr(resource, "model_config")
assert hasattr(type(resource), "model_fields")
Schedule and Sensor Tests
Test schedule configuration and timing:
from macro_agents.defs.replication import weekly_replication_schedule
class TestSchedules:
def test_weekly_replication_schedule_configuration(self):
assert weekly_replication_schedule.name == "weekly_replication_schedule"
assert weekly_replication_schedule.cron_schedule == "0 2 * * 0"
assert weekly_replication_schedule.execution_timezone == "America/New_York"
def test_schedule_timing_consistency(self):
replication_hour = 2
assert replication_hour <= 6, "Weekly replication should run early in the morning"
Mocking External Services
Use patch for external API calls and credentials:
from unittest.mock import Mock, patch
def test_sling_resource_setup_with_credentials(self):
test_creds = {
"type": "service_account",
"project_id": "test-project",
"private_key": "-----BEGIN PRIVATE KEY-----\ntest\n-----END PRIVATE KEY-----\n",
}
creds_json = json.dumps(test_creds)
mock_context = Mock()
mock_context.log = Mock()
with patch.dict(os.environ, {"SLING_GOOGLE_APPLICATION_CREDENTIALS": creds_json}):
with patch("macro_agents.defs.replication.sling.SlingConnectionResource") as mock_conn:
resource = SlingResourceWithCredentials()
resource.setup_for_execution(mock_context)
assert hasattr(resource, "_sling_resource")
Skip Tests When Environment Not Available
Use pytest.mark.skipif for optional integration tests:
@pytest.mark.skipif(
not os.getenv("MOTHERDUCK_TOKEN"),
reason="MOTHERDUCK_TOKEN not set"
)
def test_motherduck_connection_parameters(self):
token = os.getenv("MOTHERDUCK_TOKEN")
assert len(token) > 0
2. dbt Testing
Test Types
- Schema Tests - Defined in
schema.yml files (not_null, unique, accepted_values)
- Data Tests - Custom SQL in
tests/ folder
- dbt Project Validation - Python tests that run dbt commands
Schema Tests in schema.yml
version: 2
models:
- name: major_indicies_summary
description: "Performance analysis of major stock market indices"
columns:
- name: symbol
tests:
- not_null
- unique
- name: time_period
tests:
- not_null
- accepted_values:
values: ['12_weeks', '6_months', '1_year', '5_years']
- name: total_return_pct
tests:
- not_null
Custom Data Tests
Create SQL files in dbt_project/tests/ that return failing rows:
{{ config(severity='warn') }}
{% set models_to_test = [
'currency_analysis_return',
'major_indicies_analysis_return',
] %}
{% for model in models_to_test %}
SELECT
symbol,
month_date,
pct_change_q1_forward,
'zero_forward_return' as issue_type
FROM {{ ref(model) }}
WHERE ABS(pct_change_q1_forward) < 0.01
AND pct_change_q1_forward IS NOT NULL
{% if not loop.last %}
UNION ALL
{% endif %}
{% endfor %}
Data Completeness Tests
WITH weekly_counts AS (
SELECT
DATE_TRUNC('week', date) AS week_start,
COUNT(*) AS record_count
FROM {{ ref('stg_us_sectors') }}
WHERE date >= CURRENT_DATE - INTERVAL '12 weeks'
GROUP BY DATE_TRUNC('week', date)
)
SELECT week_start
FROM weekly_counts
WHERE record_count < 10
Python Tests for dbt Validation
Test dbt project from Python (runs in Dagster test suite):
import subprocess
from pathlib import Path
class TestDbtProjectValidation:
@pytest.fixture
def dbt_project_dir(self):
return Path(__file__).parent.parent.parent / "dbt_project"
def test_dbt_parse_succeeds(self, dbt_project_dir):
original_cwd = os.getcwd()
try:
os.chdir(dbt_project_dir)
result = subprocess.run(
["dbt", "parse", "--target", "local"],
capture_output=True,
text=True,
timeout=60,
)
if result.returncode != 0:
pytest.fail(f"dbt parse failed:\n{result.stderr}")
finally:
os.chdir(original_cwd)
def test_dbt_list_models_succeeds(self, dbt_project_dir):
original_cwd = os.getcwd()
try:
os.chdir(dbt_project_dir)
result = subprocess.run(
["dbt", "list", "--target", "local", "--resource-type", "model"],
capture_output=True,
text=True,
)
models = [l.strip() for l in result.stdout.split("\n") if l.strip()]
assert len(models) > 0, "No models found"
finally:
os.chdir(original_cwd)
3. FastAPI Backend Testing
Test Setup (conftest.py)
import pytest
from fastapi.testclient import TestClient
from api.main import app
@pytest.fixture
def client():
return TestClient(app)
@pytest.fixture
def temp_duckdb_file():
with tempfile.NamedTemporaryFile(suffix=".duckdb", delete=False) as tmp_file:
yield tmp_file.name
if os.path.exists(tmp_file.name):
os.unlink(tmp_file.name)
@pytest.fixture
def mock_motherduck_resource(temp_duckdb_file):
return MotherDuckResource(
md_token="test_token",
environment="dev",
local_path=temp_duckdb_file,
)
API Endpoint Tests
from fastapi.testclient import TestClient
from api.main import app
client = TestClient(app)
def test_get_market_summaries():
response = client.get("/api/markets/summary")
assert response.status_code in [200, 500]
if response.status_code == 200:
data = response.json()
assert "results" in data
assert "count" in data
assert isinstance(data["results"], list)
def test_get_market_summaries_with_category():
response = client.get("/api/markets/summary?category=sector")
assert response.status_code in [200, 500]
def test_get_market_summaries_invalid_limit():
response = client.get("/api/markets/summary?limit=0")
assert response.status_code == 400
response = client.get("/api/markets/summary?limit=1001")
assert response.status_code == 400
Testing Validation Errors
def test_invalid_limit_returns_400():
response = client.get("/api/markets/summary?limit=0")
assert response.status_code == 400
assert "detail" in response.json()
Service Layer Tests
def test_data_service_initialization():
service = DataService()
assert service.motherduck is not None
def test_get_market_summaries_empty():
service = DataService()
results = service.get_market_summaries()
assert isinstance(results, list)
4. React Frontend Testing
Test Setup (tests/setup.ts)
import { expect, afterEach } from 'vitest';
import { cleanup } from '@testing-library/react';
import * as matchers from '@testing-library/jest-dom/matchers';
expect.extend(matchers);
afterEach(() => {
cleanup();
});
Component Tests with Providers
Wrap components with required providers:
import { describe, it, expect, vi } from 'vitest';
import { render, screen } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ThemeProvider } from '../theme/ThemeContext';
function renderWithProviders(component: React.ReactElement) {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
},
});
return render(
<QueryClientProvider client={queryClient}>
<ThemeProvider>{component}</ThemeProvider>
</QueryClientProvider>
);
}
Mocking API Hooks
import * as apiHooks from '../hooks/useApi';
vi.mock('../hooks/useApi');
describe('Dashboard', () => {
it('should render loading state', () => {
vi.mocked(apiHooks.useLatestEconomyState).mockReturnValue({
data: undefined,
isLoading: true,
error: null,
} as ReturnType<typeof apiHooks.useLatestEconomyState>);
renderWithProviders(<Dashboard />);
expect(screen.getAllByText('Loading...').length).toBeGreaterThan(0);
});
it('should render dashboard with data', async () => {
const mockData = {
analysis_date: '2024-01-01',
model_name: 'test-model',
analysis_content: 'Test content',
};
vi.mocked(apiHooks.useLatestEconomyState).mockReturnValue({
data: mockData,
isLoading: false,
error: null,
} as ReturnType<typeof apiHooks.useLatestEconomyState>);
renderWithProviders(<Dashboard />);
await waitFor(() => {
expect(screen.getByText('Latest Economy State')).toBeInTheDocument();
});
});
});
Testing User Interactions
import { fireEvent } from '@testing-library/react';
it('should call onRangeChange when a button is clicked', () => {
const onRangeChange = vi.fn();
render(<TimeRangeSelector selectedRange="1Y" onRangeChange={onRangeChange} />);
fireEvent.click(screen.getByText('2Y'));
expect(onRangeChange).toHaveBeenCalledWith('2Y');
});
Testing with Theme Provider
import { ThemeProvider } from '../../theme/ThemeContext';
function renderWithTheme(component: React.ReactElement) {
return render(<ThemeProvider>{component}</ThemeProvider>);
}
describe('TimeSeriesChart', () => {
const mockData = [
{ date: '2024-01-01', value: 100 },
{ date: '2024-01-02', value: 105 },
];
it('should render chart with data', () => {
renderWithTheme(<TimeSeriesChart data={mockData} title="Test Chart" />);
expect(screen.getByText('Test Chart')).toBeInTheDocument();
});
it('should render chart without title', () => {
const { container } = renderWithTheme(<TimeSeriesChart data={mockData} />);
expect(container.querySelector('h3')).not.toBeInTheDocument();
});
});
Testing Async Effects and Cleanup
Test components with useEffect cleanup to ensure no memory leaks:
import { render, screen, waitFor, act } from '@testing-library/react';
describe('DataLoader (async useEffect)', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it('should not update state after unmount', async () => {
const consoleSpy = vi.spyOn(console, 'error');
const mockFetch = vi.fn().mockImplementation(
() => new Promise(resolve => setTimeout(() => resolve({ data: 'test' }), 1000))
);
vi.mocked(api.fetchData).mockImplementation(mockFetch);
const { unmount } = renderWithProviders(<DataLoader />);
unmount();
await act(async () => {
vi.advanceTimersByTime(1500);
});
expect(consoleSpy).not.toHaveBeenCalled();
});
it('should cancel previous request when dependencies change', async () => {
const abortSpy = vi.fn();
global.AbortController = vi.fn().mockImplementation(() => ({
signal: {},
abort: abortSpy,
}));
const { rerender } = renderWithProviders(<DataLoader id="1" />);
rerender(
<QueryClientProvider client={queryClient}>
<DataLoader id="2" />
</QueryClientProvider>
);
expect(abortSpy).toHaveBeenCalled();
});
it('should handle loading, success, and error states', async () => {
vi.mocked(api.fetchData).mockResolvedValueOnce({ items: [] });
renderWithProviders(<DataLoader />);
expect(screen.getByTestId('loading-spinner')).toBeInTheDocument();
await waitFor(() => {
expect(screen.queryByTestId('loading-spinner')).not.toBeInTheDocument();
expect(screen.getByText('No items found')).toBeInTheDocument();
});
});
});
Testing Custom Hooks with Async Operations
import { renderHook, waitFor, act } from '@testing-library/react';
describe('useAsyncData hook', () => {
it('should return data after fetch completes', async () => {
vi.mocked(api.getData).mockResolvedValueOnce({ result: 'test' });
const { result } = renderHook(() => useAsyncData('test-id'));
expect(result.current.isLoading).toBe(true);
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
expect(result.current.data).toEqual({ result: 'test' });
});
});
it('should handle errors gracefully', async () => {
vi.mocked(api.getData).mockRejectedValueOnce(new Error('Network error'));
const { result } = renderHook(() => useAsyncData('test-id'));
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
expect(result.current.error).toBe('Network error');
});
});
it('should cleanup on unmount', async () => {
const mockAbort = vi.fn();
global.AbortController = vi.fn().mockImplementation(() => ({
signal: { aborted: false },
abort: mockAbort,
}));
const { unmount } = renderHook(() => useAsyncData('test-id'));
unmount();
expect(mockAbort).toHaveBeenCalled();
});
});
5. Running Tests
All Tests (via Makefile)
make test
Dagster Tests
cd macro_agents
uv run pytest tests/ -v
uv run pytest tests/test_resources.py -v
uv run pytest tests/test_resources.py::TestMotherDuckResource -v
uv run pytest tests/ --cov=macro_agents --cov-report=html
dbt Tests
cd dbt_project
dbt test
dbt test --select major_indicies_summary
dbt test --select test_type:data
dbt test --select test_type:schema
API Tests
cd api
PYTHONPATH=.. uv run pytest tests/ -v
uv run pytest tests/test_markets.py -v
Frontend Tests
cd frontend
npm test
npm test -- --run
npm run test:ui
6. Best Practices
General
- Test one thing per test - Each test should verify a single behavior
- Use descriptive test names - Name should explain what is being tested
- Arrange-Act-Assert - Structure tests clearly
- Clean up resources - Use fixtures with cleanup for temp files/databases
Dagster
- Mock external services - Never call real APIs in unit tests
- Test resource initialization - Verify both dev and prod configurations
- Test definitions load - Catch import/config errors early
- Use
pytest.mark.skipif - Skip integration tests when credentials unavailable
dbt
- Add schema tests for every model - At minimum: not_null on key columns
- Use custom tests for business logic - Data completeness, value ranges
- Set severity levels - Use
severity='warn' for non-blocking issues
- Test incrementally - Run
dbt test --select model_name during development
FastAPI
- Test both success and error cases - Include validation errors
- Use TestClient - Simulates HTTP requests without running server
- Accept multiple status codes -
[200, 500] for endpoints that depend on DB state
- Test with fixtures - Use
conftest.py for shared test setup
React
- Mock API hooks - Don't make real API calls in tests
- Use providers - Wrap with QueryClientProvider, ThemeProvider
- Test loading/error states - Cover all UI states
- Prefer user-centric queries - Use
getByText, getByRole over querySelector