Testing

The Replane SDK provides an in-memory client specifically designed for testing. This allows you to test your application without connecting to a real Replane server.

In-Memory Client

The InMemoryReplaneClient provides the same interface as the real clients but stores all configs in memory:

from replane.testing import InMemoryReplaneClient

# Create with initial configs
replane = InMemoryReplaneClient({
    "feature_enabled": True,
    "rate_limit": 100,
    "api-version": "v2",
})

# Use like the real client
assert replane.configs["feature_enabled"] is True
assert replane.configs["rate_limit"] == 100

Using create_test_client

For convenience, use the create_test_client helper:

from replane.testing import create_test_client

replane = create_test_client({
    "feature_flags": {"dark-mode": True, "new-ui": False},
    "limits": {"max_items": 50, "max_users": 10},
})

Pytest Fixtures

Create a fixture to share test configs across tests:

import pytest
from replane.testing import create_test_client

@pytest.fixture
def replane():
    return create_test_client({
        "feature_enabled": True,
        "rate_limit": 100,
    })

def test_feature_flag(replane):
    assert replane.configs["feature_enabled"] is True

def test_rate_limit(replane):
    assert replane.configs["rate_limit"] == 100

Testing with Overrides

Test override behavior by setting up configs with override rules:

from replane.testing import InMemoryReplaneClient

def test_plan_based_rate_limits():
    replane = InMemoryReplaneClient()
    replane.set_config(
        "rate_limit",
        value=100,  # Base value for free users
        overrides=[
            {
                "name": "premium-users",
                "conditions": [
                    {"operator": "in", "property": "plan", "expected": ["pro", "enterprise"]}
                ],
                "value": 1000,
            }
        ],
    )

    # Free user gets base value
    free_client = replane.with_context({"plan": "free"})
    assert free_client.configs["rate_limit"] == 100

    # Premium users get override value
    pro_client = replane.with_context({"plan": "pro"})
    assert pro_client.configs["rate_limit"] == 1000
    
    enterprise_client = replane.with_context({"plan": "enterprise"})
    assert enterprise_client.configs["rate_limit"] == 1000

Testing Multiple Conditions

def test_multiple_conditions():
    replane = InMemoryReplaneClient()
    replane.set_config(
        "feature",
        value=False,
        overrides=[
            {
                "name": "premium-us-users",
                "conditions": [
                    {"operator": "equals", "property": "plan", "expected": "premium"},
                    {"operator": "equals", "property": "region", "expected": "us"},
                ],
                "value": True,
            }
        ],
    )

    # Both conditions must match
    assert replane.with_context({"plan": "premium", "region": "us"}).configs["feature"] is True
    assert replane.with_context({"plan": "premium", "region": "eu"}).configs["feature"] is False
    assert replane.with_context({"plan": "free", "region": "us"}).configs["feature"] is False

Dynamic Config Updates

The in-memory client supports updating configs during tests:

def test_config_updates():
    replane = InMemoryReplaneClient({"feature": False})

    # Initially disabled
    assert replane.configs["feature"] is False

    # Update the config
    replane.set("feature", True)

    # Now enabled
    assert replane.configs["feature"] is True

Testing Subscriptions

Test that your code reacts to config changes:

def test_subscription():
    replane = InMemoryReplaneClient({"value": 1})
    changes = []

    def on_change(name, config):
        changes.append((name, config.value))

    replane.subscribe(on_change)

    replane.set("value", 2)
    replane.set("value", 3)

    assert changes == [("value", 2), ("value", 3)]

Testing Error Handling

Test how your code handles missing configs:

import pytest
from replane.testing import InMemoryReplaneClient

def test_missing_config():
    replane = InMemoryReplaneClient()

    with pytest.raises(KeyError):
        _ = replane.configs["nonexistent"]

def test_missing_with_default():
    replane = InMemoryReplaneClient()

    # Should return default, not raise
    value = replane.configs.get("nonexistent", "fallback")
    assert value == "fallback"

Default Context in Tests

Set a default context for all test operations:

def test_with_default_context():
    replane = InMemoryReplaneClient(
        {"feature": False},
        context={"environment": "test"},
    )

    replane.set_config(
        "feature",
        value=False,
        overrides=[
            {
                "name": "test-env",
                "conditions": [
                    {"operator": "equals", "property": "environment", "expected": "test"}
                ],
                "value": True,
            }
        ],
    )

    # Default context is used
    assert replane.configs["feature"] is True

Dependency Injection Pattern

For better testability, inject the Replane client into your code:

# your_module.py
class FeatureService:
    def __init__(self, replane):
        self.replane = replane

    def is_feature_enabled(self, user_id: str) -> bool:
        user_client = self.replane.with_context({"user_id": user_id})
        return user_client.configs["new_feature"]

# test_your_module.py
from replane.testing import create_test_client
from your_module import FeatureService

def test_feature_service():
    replane = create_test_client({"new_feature": True})
    service = FeatureService(replane)

    assert service.is_feature_enabled("user-123") is True

Context Manager Support

The in-memory client supports context managers for cleanup:

def test_with_context_manager():
    with InMemoryReplaneClient({"key": "value"}) as replane:
        assert replane.configs["key"] == "value"
    # Client is closed after the block