테스트 _ 노트북을 스크립트·pytest로
Jupyter 노트북에서 테스트를 작성하는 것은 번거롭습니다. .ipynb 파일은 JSON이라서 pytest가 직접 수집할 수 없고, 별도 플러그인이 필요합니다. marimo는 다릅니다. .py 파일이기 때문에 표준 Python 도구로 접근할 수 있습니다.
이 챕터에서는 marimo 노트북을 스크립트로 실행하는 방법, 분석 로직을 순수 함수로 분리해 pytest로 테스트하는 방법, marimo check를 테스트 흐름에 포함하는 방법을 다룹니다.
노트북을 스크립트로 실행하기
PART 05 Ch05에서 marimo 파일을 python 명령으로 직접 실행할 수 있다고 소개했습니다. marimo 파일 끝에는 일반적으로 아래 블록이 있습니다.
if __name__ == "__main__": app.run()
python notebook.py를 실행하면 이 블록이 동작해 marimo 앱 서버가 실행됩니다. marimo run notebook.py와 결과가 같습니다.
CI에서 단순히 임포트 오류나 문법 오류를 잡으려면 Python으로 직접 실행하거나 py_compile을 씁니다.
python -m py_compile notebook.pyecho "문법 오류 없음"
임포트 오류나 문법 오류를 파일 단위로 빠르게 확인하려면 py_compile이 충분합니다.
순수 함수 분리 원칙
노트북 안의 모든 로직을 pytest로 테스트하는 것은 현실적이지 않습니다. marimo UI 코드(mo.ui.slider, mo.md, 등)는 marimo 런타임 없이는 의미 없는 코드입니다. 테스트할 수 있는 부분과 그렇지 않은 부분을 나누는 것이 먼저입니다.
테스트하기 좋은 코드. 입력을 받아 출력을 반환하는 순수 함수입니다. 외부 상태(파일, 네트워크, UI 위젯 값)에 의존하지 않습니다.
테스트하기 어려운 코드. mo.ui.slider()처럼 위젯을 생성하는 코드, 파일 경로에 직접 의존하는 코드, 셀 간 상태 의존이 복잡한 코드입니다.
순수 함수를 별도 .py 모듈로 분리하는 방식을 권장합니다.
project/├── notebook.py ← marimo 노트북├── analysis.py ← 순수 함수 모듈└── tests/ └── test_analysis.py ← pytest 테스트
analysis.py에 노트북에서 쓸 함수를 작성합니다.
# analysis.pydef normalize(values: list[float]) -> list[float]: """최솟값-최댓값 정규화.""" if not values: return [] min_v = min(values) max_v = max(values) if max_v == min_v: return [0.0] * len(values) return [(v - min_v) / (max_v - min_v) for v in values]def filter_outliers(values: list[float], sigma: float = 2.0) -> list[float]: """평균 +/- sigma 표준편차 범위 밖 값을 제거.""" if len(values) < 2: return values mean = sum(values) / len(values) variance = sum((v - mean) ** 2 for v in values) / len(values) std = variance ** 0.5 return [v for v in values if abs(v - mean) <= sigma * std]
이 함수들을 노트북에서 임포트해 씁니다.
# notebook.py 안의 한 셀from analysis import normalize, filter_outliersclean = filter_outliers(raw_data)normalized = normalize(clean)normalized
pytest로 함수 테스트하기
분리한 함수는 평범한 pytest 테스트로 검증합니다.
# tests/test_analysis.pyfrom analysis import normalize, filter_outliersdef test_normalize_basic(): result = normalize([0.0, 5.0, 10.0]) assert result == [0.0, 0.5, 1.0]def test_normalize_empty(): assert normalize([]) == []def test_normalize_all_same(): result = normalize([3.0, 3.0, 3.0]) assert result == [0.0, 0.0, 0.0]def test_filter_outliers_removes_extreme(): # n=5에서 모집단 std 기준 최대 z-점수는 2.0이므로 sigma=1.5로 설정합니다. # 1.5 * std ≈ 58.5 < abs(100 - mean) ≈ 78 이므로 100.0이 제거됩니다. data = [1.0, 2.0, 3.0, 4.0, 100.0] result = filter_outliers(data, sigma=1.5) assert 100.0 not in resultdef test_filter_outliers_keeps_normal(): data = [1.0, 2.0, 3.0, 4.0, 5.0] result = filter_outliers(data, sigma=2.0) assert len(result) == len(data) # 정상 범위 값은 모두 보존
pytest tests/
============================= test session starts ==============================collected 5 itemstests/test_analysis.py ..... [100%]============================== 5 passed in 0.12s ===============================
marimo check를 테스트에 포함하기
함수 테스트 외에 노트북 파일 자체의 구조 검사를 테스트에 포함합니다. marimo check가 critical 오류를 보고하면 비정상 종료합니다. pytest에서 subprocess로 호출해 이 동작을 활용합니다.
# tests/test_notebook_structure.pyimport subprocessimport pathlibNOTEBOOK_DIR = pathlib.Path(__file__).parent.parentdef get_marimo_notebooks(): return list(NOTEBOOK_DIR.glob("*.py"))def test_marimo_check_passes(): notebooks = get_marimo_notebooks() assert notebooks, "테스트할 marimo 노트북 파일이 없습니다." for nb in notebooks: result = subprocess.run( ["marimo", "check", str(nb)], capture_output=True, text=True, ) assert result.returncode == 0, ( f"{nb.name} marimo check 실패:\n{result.stdout}\n{result.stderr}" )
이 테스트를 pytest로 실행하면 *.py 노트북에 MB002·MB003 같은 critical 오류가 있을 때 테스트가 실패합니다.
노트북 수가 많다면 glob 패턴을 구체적으로 지정합니다.
def get_marimo_notebooks(): return list(NOTEBOOK_DIR.glob("notebooks/*.py"))
테스트 전략 요약
전체를 테스트하려 하지 않습니다. 다음 범위만 커버해도 충분한 안전망이 생깁니다.
| 대상 | 방법 |
|---|---|
| 분석 로직(순수 함수) | analysis.py 분리 후 pytest |
| 노트북 구조(MB002·MB003) | marimo check subprocess 테스트 |
| 문법 오류 | python -m py_compile notebook.py |
정리
- marimo 파일은
.py이므로python -m py_compile로 문법을 검사하고python notebook.py로 직접 실행할 수 있습니다. - 분석 로직을 순수 함수로 분리해 별도 모듈에 두면 pytest로 독립적으로 테스트할 수 있습니다.
marimo check를subprocess.run으로 호출하는 pytest 테스트를 추가하면 MB002·MB003 구조 오류를 자동으로 감지합니다.- 노트북 전체를 테스트하려 하지 않습니다. 순수 함수 테스트와 구조 검사 두 가지가 실용적인 안전망을 만들어줍니다.