Tutorial: Testing Strategies¶
This tutorial teaches you to write comprehensive test suites for Mistaber encodings. You will learn test organization, fixture patterns, parameterized testing, and CI integration to ensure your encodings are correct and maintainable.
Duration: 1.5 hours Difficulty: Intermediate Prerequisites: Tutorial 01: Your First Encoding
Minimum Test Requirements¶
Every encoding must meet these thresholds:
| Test Type | Minimum Count | Purpose |
|---|---|---|
| Positive tests | 5 | Verify rules fire correctly |
| Negative tests | 3 | Verify rules don't fire incorrectly |
| Edge cases | 2 | Test boundary conditions |
| Multi-world tests | If applicable | Verify inheritance and overrides |
| Machloket tests | If applicable | Test both positions |
Test File Organization¶
Directory Structure¶
tests/
corpus/
yd_87/
__init__.py
conftest.py # Shared fixtures for YD 87
test_beheima_chalav.py # YD 87:1 tests
test_of_chalav.py # YD 87:3 (poultry) tests
test_chaya_chalav.py # YD 87:3 (wild animal) tests
test_fish_dairy.py # YD 87:3 (fish) machloket tests
yd_89/
__init__.py
conftest.py # Shared fixtures for YD 89
test_waiting_time.py # YD 89:1 tests
worlds/
__init__.py
test_inheritance.py # World inheritance tests
test_override.py # Override mechanism tests
test_machloket.py # Machloket detection tests
integration/
__init__.py
test_full_queries.py # End-to-end query tests
Naming Conventions¶
| Pattern | Example | Contents |
|---|---|---|
test_[topic].py |
test_beheima_chalav.py |
Tests for a specific halachic topic |
test_[siman]_[seif].py |
test_87_1.py |
Tests for a specific seif |
conftest.py |
conftest.py |
Shared fixtures for the directory |
The conftest.py File¶
Shared fixtures go in conftest.py for automatic discovery by pytest.
Example: YD 87 Fixtures¶
# tests/corpus/yd_87/conftest.py
"""Shared fixtures for Yoreh Deah 87 (Basar Bechalav) tests."""
import pytest
from pathlib import Path
from mistaber.engine import HsrsEngine
@pytest.fixture(scope="module")
def engine():
"""Shared engine instance for all tests in module."""
return HsrsEngine(Path("mistaber/ontology"))
@pytest.fixture
def beheima_chalav_mixture():
"""Standard beheima (beef) + chalav (milk) mixture."""
return """
mixture(m1).
food(beef). food_type(beef, beheima).
food(milk). food_type(milk, chalav).
contains(m1, beef). contains(m1, milk).
"""
@pytest.fixture
def of_chalav_mixture():
"""Standard of (poultry) + chalav mixture."""
return """
mixture(m1).
food(chicken). food_type(chicken, of).
food(milk). food_type(milk, chalav).
contains(m1, chicken). contains(m1, milk).
"""
@pytest.fixture
def chaya_chalav_mixture():
"""Standard chaya (wild animal) + chalav mixture."""
return """
mixture(m1).
food(deer). food_type(deer, chaya).
food(milk). food_type(milk, chalav).
contains(m1, deer). contains(m1, milk).
"""
@pytest.fixture
def dag_chalav_mixture():
"""Standard dag (fish) + chalav mixture."""
return """
mixture(m1).
food(salmon). food_type(salmon, dag).
food(cream). food_type(cream, chalav).
contains(m1, salmon). contains(m1, cream).
"""
@pytest.fixture
def parve_mixture():
"""Parve-only mixture (no basar bechalav concerns)."""
return """
mixture(m1).
food(carrot). food_type(carrot, parve).
food(rice). food_type(rice, parve).
contains(m1, carrot). contains(m1, rice).
"""
@pytest.fixture
def empty_scenario():
"""Empty scenario with no declarations."""
return ""
Writing Positive Tests¶
Positive tests verify that rules fire when they should.
Pattern: Arrange-Act-Assert¶
def test_rule_fires_correctly(self, beheima_chalav_mixture):
"""Beheima+chalav eating is forbidden d'oraita."""
# Arrange: Set up the scenario
scenario = beheima_chalav_mixture
# Act: Run the query
result = query(scenario, world="mechaber")
# Assert: Verify expected outcomes
assert result.holds("issur(achiila, m1, d_oraita)")
assert result.holds("issur(bishul, m1, d_oraita)")
assert result.holds("issur(hanaah, m1, d_oraita)")
Complete Positive Test Set¶
class TestBeheimaChalaviIssur:
"""Positive tests for beheima + chalav prohibition (YD 87:1)."""
# Test 1: Eating prohibition
def test_issur_achiila(self, beheima_chalav_mixture):
"""Eating beheima+chalav is forbidden d'oraita."""
result = query(beheima_chalav_mixture, world="mechaber")
assert result.holds("issur(achiila, m1, d_oraita)")
# Test 2: Cooking prohibition
def test_issur_bishul(self, beheima_chalav_mixture):
"""Cooking beheima+chalav is forbidden d'oraita."""
result = query(beheima_chalav_mixture, world="mechaber")
assert result.holds("issur(bishul, m1, d_oraita)")
# Test 3: Benefit prohibition
def test_issur_hanaah(self, beheima_chalav_mixture):
"""Benefit from beheima+chalav is forbidden d'oraita."""
result = query(beheima_chalav_mixture, world="mechaber")
assert result.holds("issur(hanaah, m1, d_oraita)")
# Test 4: Madrega classification
def test_madrega_doraita(self, beheima_chalav_mixture):
"""Beheima+chalav is classified as d'oraita."""
result = query(beheima_chalav_mixture, world="mechaber")
assert result.holds("basar_chalav_madrega(m1, d_oraita)")
# Test 5: Mixture detection
def test_mixture_detected(self, beheima_chalav_mixture):
"""Beheima+chalav mixture is correctly detected."""
result = query(beheima_chalav_mixture, world="mechaber")
assert result.holds("is_beheima_chalav_mixture(m1)")
Writing Negative Tests¶
Negative tests verify that rules do NOT fire when they shouldn't.
Pattern: Verify Absence¶
def test_rule_does_not_fire(self, parve_mixture):
"""Parve foods do not trigger basar bechalav."""
# Arrange
scenario = parve_mixture
# Act
result = query(scenario, world="mechaber")
# Assert: Rules should NOT fire
assert not result.holds("issur(achiila, m1, _)")
assert not result.holds("is_beheima_chalav_mixture(m1)")
Complete Negative Test Set¶
class TestNoIssurim:
"""Negative tests: Scenarios that should NOT trigger issur."""
# Test 1: Parve only
def test_parve_no_issur(self, parve_mixture):
"""Parve-only mixture has no basar bechalav issur."""
result = query(parve_mixture, world="mechaber")
assert not result.holds("issur(achiila, m1, _)")
assert not result.holds("is_beheima_chalav_mixture(m1)")
assert not result.holds("is_of_chalav_mixture(m1)")
# Test 2: Single food item
def test_single_food_no_mixture(self):
"""Single food item is not a basar bechalav mixture."""
scenario = """
food(beef). food_type(beef, beheima).
"""
result = query(scenario, world="mechaber")
assert not result.holds("mixture(_)")
assert not result.holds("issur(achiila, _, _)")
# Test 3: Meat only (no dairy)
def test_meat_only_no_issur(self):
"""Meat without dairy has no basar bechalav issur."""
scenario = """
mixture(m1).
food(beef). food_type(beef, beheima).
food(chicken). food_type(chicken, of).
contains(m1, beef). contains(m1, chicken).
"""
result = query(scenario, world="mechaber")
assert not result.holds("is_beheima_chalav_mixture(m1)")
assert not result.holds("issur(achiila, m1, d_oraita)")
Writing Edge Case Tests¶
Edge cases test boundary conditions and unusual scenarios.
Identifying Edge Cases¶
| Category | Example |
|---|---|
| Empty inputs | No foods declared |
| Single values | Only one food item |
| Boundary values | Exactly 60:1 ratio for bitul |
| Unusual combinations | Multiple meat types |
| Missing data | Mixture without contains |
Complete Edge Case Set¶
class TestEdgeCases:
"""Edge case tests for boundary conditions."""
# Edge 1: Mixture declared but empty
def test_empty_mixture(self):
"""Mixture with no contents is not basar bechalav."""
scenario = """
mixture(m1).
% No contains/2 declarations
"""
result = query(scenario, world="mechaber")
assert not result.holds("is_beheima_chalav_mixture(m1)")
# Edge 2: Multiple meat types
def test_multiple_meat_types(self):
"""Mixture with multiple meat types still triggers."""
scenario = """
mixture(m1).
food(beef). food_type(beef, beheima).
food(chicken). food_type(chicken, of).
food(milk). food_type(milk, chalav).
contains(m1, beef). contains(m1, chicken). contains(m1, milk).
"""
result = query(scenario, world="mechaber")
# Both mixture types detected
assert result.holds("is_beheima_chalav_mixture(m1)")
assert result.holds("is_of_chalav_mixture(m1)")
# Edge 3: Food type not declared
def test_missing_food_type(self):
"""Food without type does not trigger rules."""
scenario = """
mixture(m1).
food(mystery). % No food_type
food(milk). food_type(milk, chalav).
contains(m1, mystery). contains(m1, milk).
"""
result = query(scenario, world="mechaber")
assert not result.holds("is_beheima_chalav_mixture(m1)")
# Edge 4: Boundary ratio for bitul
def test_exact_60_ratio_bitul(self):
"""Exactly 60:1 ratio satisfies bitul requirement."""
scenario = """
mixture(m1).
issur_component(m1, i1).
heter_component(m1, h1).
ratio(h1, i1, 60). % Exactly 60
"""
result = query(scenario, world="mechaber")
assert result.holds("batel(m1)")
Multi-World Testing¶
Multi-world tests verify inheritance and override behavior.
Testing Inheritance¶
class TestWorldInheritance:
"""Tests for world inheritance behavior."""
@pytest.fixture
def standard_scenario(self):
return """
mixture(m1).
food(beef). food_type(beef, beheima).
food(milk). food_type(milk, chalav).
contains(m1, beef). contains(m1, milk).
"""
def test_sefardi_yo_inherits_from_mechaber(self, standard_scenario):
"""sefardi_yo inherits mechaber's rulings."""
mechaber = query(standard_scenario, world="mechaber")
sefardi = query(standard_scenario, world="sefardi_yo")
# Same issur in both
assert mechaber.holds("issur(achiila, m1, d_oraita)")
assert sefardi.holds("issur(achiila, m1, d_oraita)")
def test_ashk_mb_inherits_from_rema(self, standard_scenario):
"""ashk_mb inherits rema's rulings."""
rema = query(standard_scenario, world="rema")
ashk_mb = query(standard_scenario, world="ashk_mb")
# Same issur in both
assert rema.holds("issur(achiila, m1, d_oraita)")
assert ashk_mb.holds("issur(achiila, m1, d_oraita)")
def test_all_worlds_agree_on_beheima(self, standard_scenario):
"""All worlds agree beheima+chalav is d'oraita."""
worlds = ["mechaber", "rema", "gra", "sefardi_yo", "ashk_mb", "ashk_ah"]
for world in worlds:
result = query(standard_scenario, world=world)
assert result.holds("issur(achiila, m1, d_oraita)"), \
f"{world} should have issur d'oraita"
Testing Overrides¶
class TestOverrideBehavior:
"""Tests for override mechanism in machloket."""
@pytest.fixture
def fish_dairy(self):
return """
mixture(m1).
food(salmon). food_type(salmon, dag).
food(cream). food_type(cream, chalav).
contains(m1, salmon). contains(m1, cream).
"""
def test_override_blocks_inheritance(self, fish_dairy):
"""Rema's override blocks mechaber's sakana."""
mechaber = query(fish_dairy, world="mechaber")
rema = query(fish_dairy, world="rema")
assert mechaber.holds("sakana(m1)")
assert not rema.holds("sakana(m1)")
def test_override_propagates_to_children(self, fish_dairy):
"""Override propagates to child worlds."""
rema = query(fish_dairy, world="rema")
ashk_mb = query(fish_dairy, world="ashk_mb")
# Both should NOT have sakana
assert rema.holds("no_sakana(m1)")
assert ashk_mb.holds("no_sakana(m1)")
# ashk_mb should NOT inherit mechaber's sakana
assert not ashk_mb.holds("sakana(m1)")
Parameterized Testing¶
Use @pytest.mark.parametrize for testing multiple cases efficiently.
Parameterized World Tests¶
@pytest.mark.parametrize("world,expected_sakana", [
("mechaber", True),
("sefardi_yo", True),
("rema", False),
("ashk_mb", False),
("ashk_ah", False),
])
def test_fish_dairy_by_world(dag_chalav_mixture, world, expected_sakana):
"""Fish+dairy sakana status varies by world."""
result = query(dag_chalav_mixture, world=world)
if expected_sakana:
assert result.holds("sakana(m1)"), f"{world} should have sakana"
else:
assert result.holds("no_sakana(m1)") or not result.holds("sakana(m1)"), \
f"{world} should NOT have sakana"
Parameterized Scenario Tests¶
@pytest.mark.parametrize("meat_type,expected_madrega", [
("beheima", "d_oraita"),
("chaya", "d_rabanan"),
("of", "d_rabanan"),
])
def test_madrega_by_meat_type(meat_type, expected_madrega):
"""Madrega varies by meat type."""
scenario = f"""
mixture(m1).
food(meat). food_type(meat, {meat_type}).
food(milk). food_type(milk, chalav).
contains(m1, meat). contains(m1, milk).
"""
result = query(scenario, world="mechaber")
assert result.holds(f"basar_chalav_madrega(m1, {expected_madrega})")
Test Markers¶
Use markers to categorize tests for selective running.
Defining Markers¶
# pytest.ini
[pytest]
markers =
multiworld: tests that verify behavior across multiple worlds
machloket: tests for disputed rulings
doraita: tests for Torah-level prohibitions
drabanan: tests for rabbinic prohibitions
slow: tests that take longer to run
integration: end-to-end integration tests
Using Markers¶
@pytest.mark.multiworld
def test_inheritance_all_worlds():
"""Test requires checking multiple worlds."""
pass
@pytest.mark.machloket
def test_fish_dairy_dispute():
"""Test for machloket between Mechaber and Rema."""
pass
@pytest.mark.doraita
@pytest.mark.multiworld
def test_beheima_chalav_all_worlds():
"""D'oraita test across all worlds."""
pass
Running Marked Tests¶
# Run only multi-world tests
pytest -m multiworld
# Run machloket tests
pytest -m machloket
# Run d'oraita tests (not slow)
pytest -m "doraita and not slow"
CI Integration¶
GitHub Actions Workflow¶
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install dependencies
run: |
pip install -e ".[dev]"
pip install pytest pytest-cov
- name: Run tests
run: pytest tests/ --cov=mistaber --cov-report=xml
- name: Check coverage threshold
run: coverage report --fail-under=80
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
files: coverage.xml
Pre-Commit Hook¶
# .pre-commit-config.yaml
repos:
- repo: local
hooks:
- id: pytest-check
name: pytest
entry: pytest tests/ -x -q --tb=short
language: system
pass_filenames: false
always_run: true
Test Checklist¶
Before submitting your encoding:
- [ ] At least 5 positive test cases
- [ ] At least 3 negative test cases
- [ ] At least 2 edge cases
- [ ] Multi-world tests for inheritance (if applicable)
- [ ] Machloket tests for both positions (if applicable)
- [ ] All tests pass locally (
pytest tests/) - [ ] Tests organized in correct directory
- [ ] Fixtures shared in conftest.py
- [ ] Test names are descriptive
- [ ] Markers applied where appropriate
Common Testing Mistakes¶
Mistake 1: Testing Implementation, Not Behavior¶
# WRONG - tests internal predicates
def test_internal_helper():
result = query(scenario, world="mechaber")
assert result.holds("_helper_predicate(x)")
# CORRECT - tests observable behavior
def test_observable_ruling():
result = query(scenario, world="mechaber")
assert result.holds("issur(achiila, m1, d_oraita)")
Mistake 2: Insufficient World Coverage¶
# WRONG - only tests one world
def test_ruling():
result = query(scenario, world="mechaber")
assert result.holds("ruling(x)")
# CORRECT - tests inheritance chain
def test_ruling_with_inheritance():
mechaber = query(scenario, world="mechaber")
sefardi = query(scenario, world="sefardi_yo")
assert mechaber.holds("ruling(x)")
assert sefardi.holds("ruling(x)") # Verify inheritance
Mistake 3: Brittle Assertions¶
# WRONG - depends on exact atom format
def test_brittle():
result = query(scenario, world="mechaber")
assert "issur(achiila,m1,d_oraita)" in result.raw_output
# CORRECT - uses semantic assertion
def test_robust():
result = query(scenario, world="mechaber")
assert result.holds("issur(achiila, m1, d_oraita)")
Summary¶
Effective testing requires:
- Organized structure: Tests in logical directories with shared fixtures
- Complete coverage: Positive, negative, edge, and multi-world tests
- Parameterization: Efficient testing of multiple cases
- CI integration: Automated testing on every commit
- Clear assertions: Test behavior, not implementation
Next Steps¶
Continue with:
- Tutorial 05: Review Process - Navigate reviews
Quick Reference: Test Patterns¶
# === Positive test ===
def test_rule_fires(fixture):
result = query(fixture, world="world")
assert result.holds("expected_predicate(args)")
# === Negative test ===
def test_rule_does_not_fire(fixture):
result = query(fixture, world="world")
assert not result.holds("unexpected_predicate(_)")
# === Parameterized test ===
@pytest.mark.parametrize("input,expected", [...])
def test_parameterized(input, expected):
result = query(input, world="world")
assert result.holds(expected)
# === Multi-world test ===
def test_inheritance():
parent = query(scenario, world="parent")
child = query(scenario, world="child")
assert parent.holds("predicate(x)")
assert child.holds("predicate(x)")