Skip to main content

Testing marimo Notebooks

marimo notebooks are pure Python, making them fully testable with standard Python testing frameworks. marimo provides first-class pytest integration with reactive test execution.

Quick Start

Create a notebook with test functions:
import marimo as mo

app = mo.App()

@app.cell
def test_addition():
    assert 1 + 1 == 2
    return

@app.cell  
def test_string_operations():
    assert "hello".upper() == "HELLO"
    return

if __name__ == "__main__":
    app.run()
Run tests interactively in the editor or via CLI:
marimo edit test_notebook.py
# Tests run automatically and display results

# Or run via pytest
pytest test_notebook.py

Reactive Test Execution

marimo can automatically run pytest on cells containing test functions:
app = mo.App(
    runtime={
        "reactive_tests": True  # Enable automatic test execution
    }
)
When enabled:
  • Cells with only test functions/classes are automatically tested
  • Test results appear inline in the notebook
  • Tests re-run when dependencies change

Writing Tests

Basic Test Functions

import marimo as mo

app = mo.App()

@app.cell
def test_calculation():
    """Test a simple calculation."""
    result = 2 + 2
    assert result == 4
    return

@app.cell
def test_with_fixture():
    """Tests can use pytest fixtures."""
    import pytest
    
    @pytest.fixture
    def sample_data():
        return [1, 2, 3, 4, 5]
    
    def test_sum(sample_data):
        assert sum(sample_data) == 15
    
    return

Testing Functions Defined in Notebooks

Use @app.function to define testable functions:
@app.function
def inc(x):
    """Increment a number."""
    return x + 1

@app.cell
def test_inc():
    assert inc(3) == 4
    return

@app.cell
def test_inc_negative():
    assert inc(-1) == 0
    return

Parametrized Tests

import pytest
import marimo as mo

app = mo.App()

@app.function
def multiply(a, b):
    return a * b

@app.cell
def test_multiply():
    @pytest.mark.parametrize("a,b,expected", [
        (2, 3, 6),
        (5, 4, 20),
        (0, 10, 0),
        (-2, 3, -6),
    ])
    def test_multiply_cases(a, b, expected):
        assert multiply(a, b) == expected
    return

Test Classes

import marimo as mo

app = mo.App()

@app.cell
def test_calculator():
    class TestCalculator:
        def test_add(self):
            assert 1 + 1 == 2
        
        def test_subtract(self):
            assert 5 - 3 == 2
        
        def test_multiply(self):
            assert 3 * 4 == 12
    return

Running Tests

In the marimo Editor

With reactive_tests: True, test cells automatically execute and display results:
=========== Overview ===========
Passed Tests:
✓ test_notebook.py::test_addition
✓ test_notebook.py::test_string_operations

Summary:
Total: 2, Passed: 2, Failed: 0, Errors: 0, Skipped: 0

Using pytest CLI

Run tests using pytest directly:
# Run all tests in a notebook
pytest test_notebook.py

# Run with verbose output
pytest -v test_notebook.py

# Run specific test
pytest test_notebook.py::test_addition

# Run with coverage
pytest --cov=. test_notebook.py

Programmatic Test Execution

From marimo/_runtime/pytest.py, you can run tests programmatically:
from marimo._runtime.pytest import run_pytest

# Run tests in a notebook
result = run_pytest(
    defs=set(globals().keys()),
    lcls=globals(),
    notebook_path="test_notebook.py"
)

print(result.summary)
# Total: 5, Passed: 4, Failed: 1, Errors: 0, Skipped: 0

print(result.output)  # Full pytest output

Test Fixtures

Cell-Level Fixtures

Define fixtures in the same cell as tests:
import pytest
import marimo as mo

@app.cell
def test_with_data():
    @pytest.fixture
    def sample_dataframe():
        import pandas as pd
        return pd.DataFrame({
            'a': [1, 2, 3],
            'b': [4, 5, 6]
        })
    
    def test_dataframe_shape(sample_dataframe):
        assert sample_dataframe.shape == (3, 2)
    
    def test_dataframe_columns(sample_dataframe):
        assert list(sample_dataframe.columns) == ['a', 'b']
    
    return

Shared Fixtures

Define fixtures in a separate cell for reuse:
@app.cell
def fixtures():
    import pytest
    
    @pytest.fixture
    def database_connection():
        # Setup
        conn = create_connection()
        yield conn
        # Teardown
        conn.close()
    
    return database_connection,

@app.cell
def test_database(database_connection):
    result = database_connection.query("SELECT 1")
    assert result == 1
    return

Integration Testing

Testing Data Pipelines

import marimo as mo
import pandas as pd

app = mo.App()

@app.function
def load_data(filepath):
    return pd.read_csv(filepath)

@app.function  
def transform_data(df):
    return df.dropna().reset_index(drop=True)

@app.function
def calculate_metrics(df):
    return {
        'mean': df['value'].mean(),
        'std': df['value'].std()
    }

@app.cell
def test_pipeline():
    # Create test data
    test_df = pd.DataFrame({
        'value': [1, 2, None, 4, 5]
    })
    test_df.to_csv('/tmp/test_data.csv', index=False)
    
    # Test pipeline
    raw = load_data('/tmp/test_data.csv')
    assert len(raw) == 5
    
    cleaned = transform_data(raw)
    assert len(cleaned) == 4  # NaN removed
    
    metrics = calculate_metrics(cleaned)
    assert metrics['mean'] == 3.0
    
    return

Testing UI Components

import marimo as mo

app = mo.App()

@app.cell
def test_ui_elements():
    # Test slider creation
    slider = mo.ui.slider(0, 100, value=50)
    assert slider.value == 50
    
    # Test text input
    text = mo.ui.text(value="hello")
    assert text.value == "hello"
    
    # Test dropdown
    dropdown = mo.ui.dropdown(
        options=['a', 'b', 'c'],
        value='b'
    )
    assert dropdown.value == 'b'
    
    return

Testing Best Practices

Keep test cells independent and focused:
# ✓ Good: One test per cell or related tests together
@app.cell
def test_addition():
    assert add(2, 3) == 5
    return

# ❌ Avoid: Mixing tests with application logic
@app.cell
def compute_and_test():
    result = expensive_computation()
    assert result > 0  # Test mixed with computation
    return result
Follow pytest conventions for test naming:
def test_user_authentication_with_valid_credentials():
    # Clear what's being tested
    pass

def test_data_loading_handles_missing_file():
    # Clear error condition being tested  
    pass
Cover boundary conditions and error cases:
import pytest

def test_division_by_zero():
    with pytest.raises(ZeroDivisionError):
        result = 1 / 0

def test_empty_list_input():
    assert process_list([]) == []

def test_negative_input_validation():
    with pytest.raises(ValueError):
        validate_positive(-1)
Avoid duplication with pytest fixtures:
@pytest.fixture
def temp_database():
    # Setup
    db = create_test_database()
    populate_test_data(db)
    yield db
    # Teardown
    db.drop_all_tables()
    db.close()

Running Doctests

marimo supports running doctests in notebook cells:
import marimo as mo

app = mo.App()

@app.function
def add(a, b):
    """
    Add two numbers.
    
    >>> add(2, 3)
    5
    >>> add(-1, 1)
    0
    >>> add(0, 0)
    0
    """
    return a + b

@app.cell
def run_doctests():
    import doctest
    results = doctest.testmod()
    return mo.md(f"Tests: {results.attempted}, Failures: {results.failed}")

Test Configuration

Configure pytest behavior in pyproject.toml:
[tool.pytest.ini_options]
testpaths = ["tests", "notebooks"]
python_files = "test_*.py"
python_functions = "test_*"
addopts = """
    -v
    --strict-markers
    --disable-warnings
    --color=yes
"""

CI/CD Integration

Run notebook tests in continuous integration:
# .github/workflows/test.yml
name: Test Notebooks

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-python@v4
        with:
          python-version: '3.11'
      
      - name: Install dependencies
        run: |
          pip install marimo pytest
          pip install -r requirements.txt
      
      - name: Run tests
        run: |
          pytest notebooks/

Example: Complete Test Suite

From examples/testing/test_with_pytest.py:
import marimo as mo

app = mo.App()

@app.function
def inc(x):
    """Increment a number by 1."""
    return x + 1

@app.cell
def test_answer():
    # This test intentionally fails to demonstrate failure reporting
    assert inc(3) == 5, "This test fails"
    return

@app.cell
def test_sanity():
    # This test passes
    assert inc(3) == 4, "This test passes"
    return

if __name__ == "__main__":
    app.run()

Debugging Failed Tests

When tests fail, marimo provides detailed tracebacks:
import marimo as mo
import pytest

@app.cell
def test_with_debug():
    # Use pytest.set_trace() for interactive debugging
    value = compute_something()
    
    # Add informative assertion messages
    assert value > 0, f"Expected positive value, got {value}"
    
    # Use pytest.approx for floating point
    assert value == pytest.approx(3.14, rel=1e-2)
    return

Build docs developers (and LLMs) love