Testing Guide
This guide explains how to write tests for your viewtext use cases using pytest.
Testing with TOML Configurations
There are two main approaches for testing viewtext with TOML configurations:
Approach 1: Temporary TOML Files
This approach is recommended for isolated unit tests. Create temporary TOML files within your test functions:
import os
import tempfile
import pytest
from viewtext.loader import LayoutLoader
from viewtext.engine import LayoutEngine
from viewtext.registry import BaseFieldRegistry
def test_my_use_case():
# 1. Create TOML content as a string
config_content = """
[layouts.my_layout]
name = "My Use Case Layout"
[[layouts.my_layout.lines]]
field = "temperature"
index = 0
formatter = "number"
[layouts.my_layout.lines.formatter_params]
decimals = 1
suffix = "°C"
"""
# 2. Write to temporary file
with tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as tmp:
tmp.write(config_content)
tmp_path = tmp.name
try:
# 3. Load and test
loader = LayoutLoader(config_path=tmp_path)
layout = loader.get_layout("my_layout")
# Create registry and engine
registry = BaseFieldRegistry()
engine = LayoutEngine(field_registry=registry)
# Create test context
context = {"temperature": 23.456}
# Build and assert
result = engine.build_line_str(layout, context)
assert result == ["23.5°C"]
finally:
# 4. Cleanup
os.unlink(tmp_path)
Approach 2: Existing TOML Files
This approach is better for integration tests that use actual configuration files:
from viewtext.loader import LayoutLoader
from viewtext.engine import LayoutEngine
from viewtext.registry import BaseFieldRegistry
def test_with_existing_toml():
# Point to actual TOML file in your project
loader = LayoutLoader(config_path="examples/layouts.toml")
layout = loader.get_layout("demo")
registry = BaseFieldRegistry()
engine = LayoutEngine(field_registry=registry)
context = {
"demo1": "Line 1",
"demo2": "Line 2",
"demo3": "Line 3",
"demo4": "Line 4"
}
result = engine.build_line_str(layout, context)
assert len(result) == 4
assert result[0] == "Line 1"
Complete Test Example
Here’s a complete example testing a weather display use case:
class TestWeatherDisplay:
def test_temperature_and_humidity_display(self):
config_content = """
[fields.temp]
context_key = "temperature"
[fields.humidity]
context_key = "humidity"
[formatters.temp_fmt]
type = "number"
decimals = 1
suffix = "°C"
[formatters.humidity_fmt]
type = "number"
decimals = 0
suffix = "%"
[layouts.weather]
name = "Weather Display"
[[layouts.weather.lines]]
field = "temp"
index = 0
formatter = "temp_fmt"
[[layouts.weather.lines]]
field = "humidity"
index = 1
formatter = "humidity_fmt"
"""
with tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as tmp:
tmp.write(config_content)
tmp_path = tmp.name
try:
loader = LayoutLoader(config_path=tmp_path)
layout = loader.get_layout("weather")
registry = BaseFieldRegistry()
engine = LayoutEngine(field_registry=registry)
context = {"temperature": 23.456, "humidity": 65.7}
result = engine.build_line_str(layout, context)
assert result[0] == "23.5°C"
assert result[1] == "66%"
finally:
os.unlink(tmp_path)
Key Testing Patterns
When testing viewtext applications, consider these common patterns:
Layout Validation
Test that layouts load correctly from TOML:
def test_layout_loads_correctly():
loader = LayoutLoader(config_path="my_config.toml")
config = loader.load()
assert "my_layout" in config.layouts
assert config.layouts["my_layout"].name == "My Layout"
assert len(config.layouts["my_layout"].lines) == 3
Field Mapping
Test that fields resolve correctly from context:
def test_field_resolution():
registry = BaseFieldRegistry()
def temp_getter(ctx):
return ctx["temperature"]
registry.register("temp", temp_getter)
engine = LayoutEngine(field_registry=registry)
layout_config = {
"lines": [{"field": "temp", "index": 0}]
}
context = {"temperature": 25}
result = engine.build_line_str(layout_config, context)
assert result == ["25"]
Formatter Application
Test that formatters work correctly with parameters from TOML:
def test_formatter_with_params():
registry = BaseFieldRegistry()
def price_getter(ctx):
return ctx["price"]
registry.register("price", price_getter)
engine = LayoutEngine(field_registry=registry)
layout_config = {
"lines": [{
"field": "price",
"index": 0,
"formatter": "price",
"formatter_params": {"symbol": "$", "decimals": 2}
}]
}
context = {"price": 123.45}
result = engine.build_line_str(layout_config, context)
assert result == ["$123.45"]
Template Formatter Testing
Test template formatters that combine multiple fields:
def test_template_formatter():
config_content = """
[fields.first_name]
context_key = "first_name"
[fields.last_name]
context_key = "last_name"
[fields.age]
context_key = "age"
[layouts.profile]
name = "User Profile"
[[layouts.profile.lines]]
field = "full_name"
index = 0
formatter = "template"
[layouts.profile.lines.formatter_params]
template = "{first_name} {last_name}"
[[layouts.profile.lines]]
field = "info"
index = 1
formatter = "template"
[layouts.profile.lines.formatter_params]
template = "Age: {age}"
"""
with tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as tmp:
tmp.write(config_content)
tmp_path = tmp.name
try:
loader = LayoutLoader(config_path=tmp_path)
layout = loader.get_layout("profile")
registry = BaseFieldRegistry()
engine = LayoutEngine(field_registry=registry)
context = {
"first_name": "John",
"last_name": "Doe",
"age": 30
}
result = engine.build_line_str(layout, context)
assert result[0] == "John Doe"
assert result[1] == "Age: 30"
finally:
os.unlink(tmp_path)
def test_template_with_missing_field():
"""Test that template formatter handles missing fields gracefully."""
config_content = """
[layouts.test]
name = "Test Layout"
[[layouts.test.lines]]
field = "greeting"
index = 0
formatter = "template"
[layouts.test.lines.formatter_params]
template = "Hello, {name}!"
"""
with tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as tmp:
tmp.write(config_content)
tmp_path = tmp.name
try:
loader = LayoutLoader(config_path=tmp_path)
layout = loader.get_layout("test")
registry = BaseFieldRegistry()
engine = LayoutEngine(field_registry=registry)
# Context missing the 'name' field
context = {}
result = engine.build_line_str(layout, context)
# Should handle missing field gracefully
assert "Hello" in result[0]
finally:
os.unlink(tmp_path)
def test_complex_template():
"""Test template with multiple fields and formatting."""
config_content = """
[fields.temp]
context_key = "temperature"
[fields.humidity]
context_key = "humidity"
[fields.location]
context_key = "location"
[layouts.weather_report]
name = "Weather Report"
[[layouts.weather_report.lines]]
field = "summary"
index = 0
formatter = "template"
[layouts.weather_report.lines.formatter_params]
template = "{location}: {temp}°C, {humidity}% humidity"
"""
with tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as tmp:
tmp.write(config_content)
tmp_path = tmp.name
try:
loader = LayoutLoader(config_path=tmp_path)
layout = loader.get_layout("weather_report")
registry = BaseFieldRegistry()
engine = LayoutEngine(field_registry=registry)
context = {
"location": "Berlin",
"temperature": 22,
"humidity": 65
}
result = engine.build_line_str(layout, context)
assert result[0] == "Berlin: 22°C, 65% humidity"
finally:
os.unlink(tmp_path)
Edge Cases
Test edge cases like missing fields, invalid formatters, and empty contexts:
def test_missing_field_returns_empty():
registry = BaseFieldRegistry()
engine = LayoutEngine(field_registry=registry)
layout_config = {
"lines": [{"field": "nonexistent", "index": 0}]
}
context = {}
result = engine.build_line_str(layout_config, context)
assert result == [""]
def test_unknown_formatter_falls_back_to_text():
registry = BaseFieldRegistry()
def value_getter(ctx):
return ctx["value"]
registry.register("value", value_getter)
engine = LayoutEngine(field_registry=registry)
layout_config = {
"lines": [{
"field": "value",
"index": 0,
"formatter": "unknown_formatter"
}]
}
context = {"value": "test"}
result = engine.build_line_str(layout_config, context)
assert result == ["test"]
Integration Tests
Test the full flow from LayoutLoader → LayoutEngine → output:
def test_full_integration():
config_content = """
[layouts.integration_test]
name = "Integration Test"
[[layouts.integration_test.lines]]
field = "value1"
index = 0
formatter = "text"
[layouts.integration_test.lines.formatter_params]
prefix = "Value: "
[[layouts.integration_test.lines]]
field = "value2"
index = 1
formatter = "number"
[layouts.integration_test.lines.formatter_params]
thousands_sep = ","
"""
with tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as tmp:
tmp.write(config_content)
tmp_path = tmp.name
try:
loader = LayoutLoader(config_path=tmp_path)
layout = loader.get_layout("integration_test")
registry = BaseFieldRegistry()
engine = LayoutEngine(field_registry=registry)
context = {"value1": "test", "value2": 1234567}
result = engine.build_line_str(layout, context)
assert result[0] == "Value: test"
assert result[1] == "1,234,567"
finally:
os.unlink(tmp_path)
Running Tests
To run all tests:
pytest
To run a specific test file:
pytest tests/test_my_use_case.py
To run a single test function:
pytest tests/test_my_use_case.py::TestMyUseCase::test_temperature_display
To run tests with coverage:
pytest --cov=viewtext --cov-report=term
Best Practices
Use descriptive test names that explain what is being tested
Test one thing per test function to make failures easy to diagnose
Use temporary files for unit tests to avoid dependencies on external files
Use existing TOML files for integration tests to test real-world configurations
Clean up temporary files in the
finallyblock to avoid leaving artifactsTest both success and failure cases including edge cases and error conditions
Use pytest fixtures for common setup code that’s shared across multiple tests
Pytest Fixtures Example
import pytest
@pytest.fixture
def temp_config_file():
"""Fixture that creates and cleans up a temporary config file."""
config_content = """
[layouts.test]
name = "Test Layout"
[[layouts.test.lines]]
field = "value"
index = 0
"""
with tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as tmp:
tmp.write(config_content)
tmp_path = tmp.name
yield tmp_path
os.unlink(tmp_path)
def test_with_fixture(temp_config_file):
loader = LayoutLoader(config_path=temp_config_file)
layout = loader.get_layout("test")
assert layout["name"] == "Test Layout"