Skip to main content

Testing

A well-tested configuration stack requires that:

  1. Unit tests do not read files from disk.
  2. Code under test receives the configuration programmatically.
  3. Hot-reload behavior is exercised through controlled triggers.

Unit tests — inline configuration via an in-memory source

Instead of reading app-config.yaml, build the configuration in the test itself. The name of the in-memory source differs across bindings:

BindingClass
dagstack-config (Python)InMemorySource
@dagstack/config (TypeScript)InMemorySource
go.dagstack.dev/config (Go)DictSource (constructor NewDictSource)
tests/test_database_pool.py
import pytest
from dagstack.config import Config, InMemorySource
from app.database import DatabaseConfig, DatabasePool


def test_pool_uses_configured_size():
config = Config.load_from([
InMemorySource({
"database": {
"host": "localhost",
"port": 5432,
"name": "test",
"user": "app",
"password": "test-pw",
"pool_size": 42,
},
}),
])
pool = DatabasePool(config.get_section("database", DatabaseConfig))

assert pool.size == 42


def test_pool_rejects_invalid_host():
config = Config.load_from([
InMemorySource({
"database": {
"host": "0.0.0.0", # rejected by the validator
"name": "test",
"user": "app",
"password": "pw",
},
}),
])
with pytest.raises(ValueError, match="host must be a concrete"):
config.get_section("database", DatabaseConfig)

File-based tests in a temporary directory — YamlFileSource in tmpdir

When you need to exercise the YAML parsing itself (nested defaults, env interpolation), write the YAML into a temporary directory:

import pytest
from pathlib import Path
from dagstack.config import Config


@pytest.fixture
def app_config_yaml(tmp_path: Path, monkeypatch) -> Path:
# Env variables for interpolation:
monkeypatch.setenv("DB_PASSWORD", "test-pw")

yaml_path = tmp_path / "app-config.yaml"
yaml_path.write_text("""
database:
host: "${DB_HOST:-localhost}"
password: "${DB_PASSWORD}"
name: "test_db"
user: "app"
pool_size: 5
""")
return yaml_path


def test_env_interpolation(app_config_yaml):
config = Config.load(str(app_config_yaml))
assert config.get_string("database.password") == "test-pw"
assert config.get_string("database.host") == "localhost"

Hot reload — Phase 2

:::caution Not implemented in v0.1 / v0.2 Hot reload and change subscriptions are a planned Phase 2 API. In the current bindings (dagstack-config v0.2.0, config-go v0.1.0, @dagstack/config v0.1.0) the registration methods (on_change / on_section_change / reload in Python, OnChange / OnSectionChange / Reload in Go) exist in the API, but the callback is never invoked: reload() is a no-op, on_change returns an inactive Subscription. A mutable source (MutableDictSource) and atomic rollback on an invalid reload are part of the same Phase 2 scope.

For tests in v0.1 / v0.2, use immutable configs: build a separate Config for each scenario through load_from / loadFrom / LoadFrom. :::

Integration tests with DAGSTACK_ENV

For end-to-end scenarios where layer order matters, set DAGSTACK_ENV:

def test_production_overrides_base(tmp_path, monkeypatch):
(tmp_path / "app-config.yaml").write_text("""
database:
pool_size: 20
host: "localhost"
name: "test"
user: "app"
password: "pw"
""")
(tmp_path / "app-config.production.yaml").write_text("""
database:
pool_size: 100
""")

monkeypatch.setenv("DAGSTACK_ENV", "production")
monkeypatch.chdir(tmp_path)
config = Config.load("app-config.yaml")
assert config.get_int("database.pool_size") == 100

Running in CI

.gitea/workflows/config-tests.yml
name: Config tests

on: [push, pull_request]

jobs:
test:
runs-on: dagstack-runner
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: '3.12' }
- run: pip install -e '.[test]'
- run: pytest tests/ -v

See also