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