Jupyter Notebook Validator Operator - Testing Guide¶
Overview¶
This guide explains the three-tier testing strategy for the Jupyter Notebook Validator Operator. Each tier represents increasing complexity and execution time, allowing for comprehensive validation while maintaining fast CI/CD feedback loops.
Testing Philosophy¶
Our testing strategy is based on real-world MLOps workflows, progressing from simple data validation to complex model training scenarios. This approach ensures:
- Fast Feedback: Tier 1 tests run in seconds for quick validation
- Realistic Scenarios: Tier 2 tests represent typical data science workflows
- Production Readiness: Tier 3 tests validate production-like ML pipelines
- Comprehensive Coverage: All tiers combined cover the full spectrum of notebook complexity
Three-Tier Testing Strategy¶
📊 Comparison Matrix¶
| Aspect | Tier 1: Simple | Tier 2: Intermediate | Tier 3: Complex |
|---|---|---|---|
| Execution Time | <30 seconds | 1-5 minutes | 5-15 minutes |
| Resource Usage | <100Mi / <100m CPU | <500Mi / <500m CPU | <2Gi / <1000m CPU |
| Dependencies | None (pure Python) | CSV files, small datasets | Datasets, model artifacts |
| Use Case | Basic validation | Data analysis | Model training |
| CI/CD Stage | Every PR | Every PR | Nightly / On-demand |
| Determinism | Fully deterministic | Controlled (random seeds) | Controlled (random seeds) |
Tier 1: Simple Notebooks¶
Purpose: Validate basic operator functionality with fast execution
Example 1: Hello World¶
# Validates: Basic execution, assertions, output capture
result = 2 + 2
print(f"Result: {result}")
assert result == 4
What it tests: - ✅ Operator can execute basic Python code - ✅ Cell outputs are captured correctly - ✅ Assertions work as expected - ✅ Execution completes successfully
Example 2: Data Validation¶
import pandas as pd
# Create sample data
data = {'name': ['Alice', 'Bob'], 'age': [25, 30]}
df = pd.DataFrame(data)
# Validate
assert len(df) == 2
assert df['age'].mean() == 27.5
print("✓ Data validation passed")
What it tests: - ✅ Pandas library available - ✅ DataFrame operations work - ✅ Statistical computations correct - ✅ Validation logic executes
Example 3: Error Handling¶
# Test graceful error handling
try:
result = 10 / 0
except ZeroDivisionError as e:
print(f"✓ Caught expected error: {e}")
# Execution continues
print("Execution continues after handled error")
What it tests: - ✅ Operator handles exceptions gracefully - ✅ Execution continues after handled errors - ✅ Error messages captured in output
Running Tier 1 Tests¶
# Run all Tier 1 tests
make test-tier1
# Run specific test
kubectl apply -f - <<EOF
apiVersion: mlops.dev/v1alpha1
kind: NotebookValidationJob
metadata:
name: test-hello-world
spec:
notebook:
git:
url: https://github.com/your-org/jupyter-notebook-validator-operator.git
ref: main
path: tests/notebooks/tier1-simple/01-hello-world.ipynb
golden:
path: tests/notebooks/tier1-simple/golden/01-hello-world-golden.ipynb
EOF
Tier 2: Intermediate Notebooks¶
Purpose: Validate realistic data science workflows
Example 1: Data Analysis¶
from sklearn.datasets import load_iris
import pandas as pd
# Load dataset
iris = load_iris()
df = pd.DataFrame(iris.data, columns=iris.feature_names)
# Statistical analysis
summary = df.describe()
print(summary)
# Correlation analysis
correlation = df.corr()
print(correlation)
# Assertions
assert df.shape[0] == 150
assert correlation.shape == (4, 4)
What it tests: - ✅ Scikit-learn integration - ✅ Statistical computations - ✅ Data exploration workflows - ✅ Multi-cell execution
Example 2: Feature Engineering¶
import numpy as np
from sklearn.preprocessing import StandardScaler
np.random.seed(42) # Reproducibility
# Generate data
data = np.random.randn(1000, 3)
# Scale features
scaler = StandardScaler()
scaled_data = scaler.fit_transform(data)
# Validate scaling
assert scaled_data.mean() < 0.01 # Near zero mean
assert abs(scaled_data.std() - 1.0) < 0.01 # Unit variance
print("✓ Feature scaling validated")
What it tests: - ✅ Data preprocessing pipelines - ✅ Reproducibility with random seeds - ✅ Numerical validation - ✅ Moderate execution time handling
Running Tier 2 Tests¶
Tier 3: Complex Notebooks¶
Purpose: Validate production-like ML workflows
Example: Model Training Pipeline¶
from sklearn.datasets import load_breast_cancer
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.metrics import roc_auc_score
import joblib
np.random.seed(42)
# Load data
data = load_breast_cancer()
X_train, X_test, y_train, y_test = train_test_split(
data.data, data.target, test_size=0.2, random_state=42
)
# Train model
model = RandomForestClassifier(n_estimators=100, random_state=42)
model.fit(X_train, y_train)
# Cross-validation
cv_scores = cross_val_score(model, X_train, y_train, cv=5, scoring='roc_auc')
print(f"CV ROC-AUC: {cv_scores.mean():.4f}")
# Evaluate
y_pred_proba = model.predict_proba(X_test)[:, 1]
test_auc = roc_auc_score(y_test, y_pred_proba)
print(f"Test ROC-AUC: {test_auc:.4f}")
# Validate performance
assert cv_scores.mean() > 0.90
assert test_auc > 0.90
# Save model
joblib.dump(model, '/tmp/model.joblib')
print("✓ Model training complete")
What it tests: - ✅ Full ML training pipeline - ✅ Cross-validation workflows - ✅ Model persistence - ✅ Performance validation - ✅ Long-running computations - ✅ Resource management
Running Tier 3 Tests¶
# Run all Tier 3 tests (typically in nightly builds)
make test-tier3
# Run with explicit trigger
git commit -m "feat: new feature [test-tier3]"
Golden Notebook Comparison¶
Golden notebooks represent the expected output for each test notebook. The operator compares execution results against golden notebooks to detect regressions.
What Gets Compared¶
- Cell Outputs: Text output, print statements
- Execution Status: Success/failure of each cell
- Assertions: All assertions must pass
- Execution Order: Cells execute in correct sequence
What Gets Ignored¶
- Timestamps: Execution timestamps vary
- Memory Addresses: Object memory addresses differ
- Floating Point Precision: Minor numerical differences (<1e-6)
- Matplotlib Figures: Visual outputs (compared separately)
Updating Golden Notebooks¶
# After intentional changes, update golden notebooks
make update-golden-notebooks TIER=1
# Review changes
git diff tests/notebooks/tier1-simple/golden/
# Commit updated golden notebooks
git add tests/notebooks/*/golden/
git commit -m "test: update golden notebooks for new feature"
CI/CD Integration¶
GitHub Actions Workflow¶
name: Notebook Tests
on: [push, pull_request]
jobs:
tier1:
name: Tier 1 (Fast)
runs-on: ubuntu-latest
steps:
- name: Run Tier 1 tests
run: make test-tier1
timeout-minutes: 5
tier2:
name: Tier 2 (Intermediate)
runs-on: ubuntu-latest
needs: tier1
steps:
- name: Run Tier 2 tests
run: make test-tier2
timeout-minutes: 10
tier3:
name: Tier 3 (Complex)
runs-on: ubuntu-latest
if: github.event_name == 'schedule'
steps:
- name: Run Tier 3 tests
run: make test-tier3
timeout-minutes: 30
Test Execution Flow¶
┌─────────────────────────────────────────────────────────┐
│ Pull Request │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ Tier 1: Simple Tests (<30s) │
│ ✓ Hello World │
│ ✓ Data Validation │
│ ✓ Error Handling │
└─────────────────────────────────────────────────────────┘
↓ (if pass)
┌─────────────────────────────────────────────────────────┐
│ Tier 2: Intermediate Tests (1-5min) │
│ ✓ Data Analysis │
│ ✓ Feature Engineering │
└─────────────────────────────────────────────────────────┘
↓ (if pass)
┌─────────────────────────────────────────────────────────┐
│ Merge to Main │
└─────────────────────────────────────────────────────────┘
↓ (nightly)
┌─────────────────────────────────────────────────────────┐
│ Tier 3: Complex Tests (5-15min) │
│ ✓ Model Training │
│ ✓ Hyperparameter Tuning │
└─────────────────────────────────────────────────────────┘
Resource Requirements¶
Kubernetes Resource Quotas¶
# Tier 1: Simple notebooks
resources:
requests:
memory: "100Mi"
cpu: "100m"
limits:
memory: "500Mi"
cpu: "500m"
# Tier 2: Intermediate notebooks
resources:
requests:
memory: "500Mi"
cpu: "500m"
limits:
memory: "1Gi"
cpu: "1000m"
# Tier 3: Complex notebooks
resources:
requests:
memory: "1Gi"
cpu: "1000m"
limits:
memory: "2Gi"
cpu: "2000m"
Best Practices¶
1. Use Random Seeds¶
2. Add Validation Assertions¶
# Validate intermediate results
assert df.shape[0] > 0, "DataFrame is empty"
assert model_score > 0.8, f"Model score too low: {model_score}"
3. Print Progress¶
print("Loading data...")
# ... data loading code ...
print("✓ Data loaded successfully")
print("Training model...")
# ... training code ...
print("✓ Model training complete")
4. Handle Errors Gracefully¶
try:
# Risky operation
result = perform_operation()
except Exception as e:
print(f"⚠ Warning: {e}")
# Fallback or recovery
5. Clean Up Resources¶
import tempfile
import os
# Use temporary files
with tempfile.NamedTemporaryFile(delete=True) as tmp:
# Work with tmp.name
pass # Automatically cleaned up
Troubleshooting¶
Test Failures¶
Symptom: Tier 1 test fails
# Check operator logs
kubectl logs -n jupyter-validator-system deployment/jupyter-notebook-validator-operator
# Check validation job status
kubectl describe notebookvalidationjob test-hello-world
# Check validation pod logs
kubectl logs -l job-name=test-hello-world
Symptom: Golden notebook mismatch
# Compare outputs
make compare-outputs JOB=test-hello-world
# Update golden notebook if change is intentional
make update-golden NOTEBOOK=01-hello-world
Symptom: Timeout
# Increase timeout for specific test
kubectl patch notebookvalidationjob test-model-training \
--type merge \
-p '{"spec":{"timeoutSeconds":1800}}' # 30 minutes
Coverage Standards¶
Baseline (measured April 2026, commit b329281)¶
| Package | Tracked Lines | Coverage |
|---|---|---|
api/v1alpha1 |
1095 | 5.21% |
internal/controller |
4388 | 45.58% |
pkg |
2924 | 47.81% |
| Overall | 8407 | 41.09% |
Thresholds¶
| Gate | Current Target | Milestone |
|---|---|---|
| Project overall | 41% | v1.0.10 → 50%, v1.1.0 → 60%, stretch → 75% |
| Patch (new code) | −5% max | A PR must not drop coverage by more than 5 points |
These thresholds are enforced automatically on every PR by Codecov. The project gate is set at the current baseline so PRs are not blocked unfairly. Raise .codecov.yml target: as coverage improves milestone by milestone. The api/v1alpha1 package is the biggest gap (5.21%); adding generated-type validation tests there will have the largest impact on the overall number.
Running Coverage Locally¶
# Full coverage via make (sets up envtest automatically)
make test
go tool cover -func=cover.out | tail -1 # total line
# HTML report in browser
go tool cover -html=cover.out
Roadmap to 75%¶
- Fix
internal/controllerBeforeSuite flakiness so all 14 controller specs run reliably in CI - Add table-driven unit tests for the reconciler happy-path and error branches
- Bring
pkg/buildfrom ~35% to ~60% with additional build-strategy unit tests - Each new feature PR is expected to include tests that keep patch coverage ≥ 0%
Checking Job Status with kubectl¶
The NotebookValidationJob CRD includes printer columns for quick status checks:
# Standard output shows Phase, Notebook, Timeout, and Age
kubectl get notebookvalidationjobs
# NAME PHASE NOTEBOOK TIMEOUT AGE
# my-validation Succeeded notebooks/train.ipynb 30m 5m
# Short name also works
kubectl get nvj
# Wide output adds Git-URL and Message columns
kubectl get nvj -o wide
# NAME PHASE NOTEBOOK GIT-URL TIMEOUT AGE MESSAGE
# my-validation Succeeded notebooks/train.ipynb https://github.com/org/repo.git 30m 5m Validation completed successfully
Next Steps¶
- Review ADR 008: Notebook Testing Strategy and Complexity Levels
- Explore Test Notebooks: Browse
tests/notebooks/directory - Run Tests Locally:
make test-all - Contribute Test Cases: Add new test notebooks for edge cases
References¶
- ADR 008: Notebook Testing Strategy
- Papermill Documentation
- Scikit-learn Testing Guide
- Netflix Notebook Testing
Last Updated: 2025-11-07 Version: 1.0