Testing with Fapilog

This guide covers patterns for testing applications that use fapilog.

Capturing Stdout Output

The StdoutJsonSink uses os.writev() for high-performance output, which bypasses Python’s sys.stdout redirection. This makes it difficult to capture output in tests using contextlib.redirect_stdout().

Solution: Use capture_mode=True

Enable capture_mode when creating your logger to use buffered writes that can be captured:

import io
import sys
import asyncio
from fapilog import AsyncLoggerBuilder

async def test_logging_output():
    # Create a buffer to capture output
    buf = io.BytesIO()
    orig_stdout = sys.stdout
    sys.stdout = io.TextIOWrapper(buf, encoding="utf-8")

    try:
        # Enable capture_mode for testing
        logger = await (
            AsyncLoggerBuilder()
            .add_stdout(capture_mode=True)
            .build_async()
        )

        await logger.info("test message", data={"key": "value"})
        await logger.drain()

        # Flush and read captured output
        sys.stdout.flush()
        output = buf.getvalue().decode("utf-8")

        assert "test message" in output
        assert '"key": "value"' in output or '"key":"value"' in output
    finally:
        sys.stdout = orig_stdout

Sync Logger Example

from fapilog import LoggerBuilder

def test_sync_logging():
    buf = io.BytesIO()
    orig_stdout = sys.stdout
    sys.stdout = io.TextIOWrapper(buf, encoding="utf-8")

    try:
        logger = LoggerBuilder().add_stdout(capture_mode=True).build()
        logger.info("sync test")
        logger.drain()

        sys.stdout.flush()
        output = buf.getvalue().decode("utf-8")
        assert "sync test" in output
    finally:
        sys.stdout = orig_stdout

pytest Fixture

Create a reusable fixture for capturing fapilog output:

import io
import sys
import pytest
from fapilog import AsyncLoggerBuilder

@pytest.fixture
def captured_stdout():
    """Fixture that captures stdout for testing."""
    buf = io.BytesIO()
    orig = sys.stdout
    sys.stdout = io.TextIOWrapper(buf, encoding="utf-8")

    yield buf

    sys.stdout = orig

@pytest.fixture
async def test_logger(captured_stdout):
    """Fixture that provides a capture-enabled logger."""
    logger = await (
        AsyncLoggerBuilder()
        .add_stdout(capture_mode=True)
        .build_async()
    )
    yield logger
    await logger.drain()

@pytest.mark.asyncio
async def test_with_fixture(test_logger, captured_stdout):
    await test_logger.info("using fixtures")
    await test_logger.drain()

    sys.stdout.flush()
    output = captured_stdout.getvalue().decode("utf-8")
    assert "using fixtures" in output

When to Use capture_mode

Scenario

Use capture_mode?

Unit tests that verify log content

Yes

Integration tests checking log format

Yes

Production applications

No (default)

Benchmarks measuring logging performance

No

Note: capture_mode=True disables the os.writev() optimization, so it should only be used in tests, not production.

Testing with Pretty Output

The StdoutPrettySink (used with format="pretty") already uses sys.stdout.write() and doesn’t need capture_mode. You can capture its output directly:

logger = LoggerBuilder().add_stdout(format="pretty").build()
# Output can be captured without capture_mode

Alternative: File Sink for Testing

For tests that don’t need real-time output capture, consider using a temporary file sink:

import tempfile
from pathlib import Path
from fapilog import AsyncLoggerBuilder

async def test_with_file_sink():
    with tempfile.TemporaryDirectory() as tmpdir:
        logger = await (
            AsyncLoggerBuilder()
            .add_file(Path(tmpdir) / "test.log")
            .build_async()
        )

        await logger.info("file test")
        await logger.drain()

        log_content = (Path(tmpdir) / "test.log").read_text()
        assert "file test" in log_content