|
| 1 | +# Test Format Guide |
| 2 | + |
| 3 | +## Test Structure |
| 4 | + |
| 5 | +Every API test follows: Setup → Execute → Assert. |
| 6 | + |
| 7 | +```python |
| 8 | +def test_descriptive_name(collection): |
| 9 | + """Clear description of what this test validates.""" |
| 10 | + # Setup (insert documents if needed) |
| 11 | + collection.insert_many([{"_id": 0, "a": 1}, {"_id": 1, "a": 2}]) |
| 12 | + |
| 13 | + # Execute — always use runCommand format |
| 14 | + result = execute_command(collection, { |
| 15 | + "find": collection.name, |
| 16 | + "filter": {"a": 1} |
| 17 | + }) |
| 18 | + |
| 19 | + # Assert — use framework assertion helpers |
| 20 | + assertSuccess(result, [{"_id": 0, "a": 1}]) |
| 21 | +``` |
| 22 | + |
| 23 | +## Naming |
| 24 | + |
| 25 | +**Files:** `test_<feature_area>.py` — files in feature subfolders must include the feature name. |
| 26 | +``` |
| 27 | +✅ /tests/aggregate/unwind/test_unwind_path.py |
| 28 | +❌ /tests/aggregate/unwind/test_path.py |
| 29 | +``` |
| 30 | + |
| 31 | +**Functions:** `test_<what_is_being_tested>` — descriptive, self-documenting. |
| 32 | +``` |
| 33 | +✅ test_find_with_gt_operator, test_unwind_preserves_null_arrays |
| 34 | +❌ test_1, test_query, test_edge_case |
| 35 | +``` |
| 36 | + |
| 37 | +## Assertions |
| 38 | + |
| 39 | +Use helpers from `framework.assertions`, not plain `assert`: |
| 40 | + |
| 41 | +```python |
| 42 | +# assertResult — parametrized tests mixing success and error cases |
| 43 | +assertResult(result, expected=5) # checks cursor.firstBatch == [{"result": 5}] |
| 44 | +assertResult(result, error_code=16555) # checks error code only |
| 45 | + |
| 46 | +# assertSuccess — raw command output |
| 47 | +assertSuccess(result, [{"_id": 0, "a": 1}]) |
| 48 | +assertSuccess(result, expected, ignore_order=True) |
| 49 | + |
| 50 | +# assertFailureCode — error cases (only check code, not message) |
| 51 | +assertFailureCode(result, 14) |
| 52 | +``` |
| 53 | + |
| 54 | +**One assertion per test function.** Split multiple assertions into separate tests. |
| 55 | + |
| 56 | +## Fixtures |
| 57 | + |
| 58 | +- `collection` — most common. Auto cleanup after test. Insert documents in test body. |
| 59 | +- `database_client` — when you need multiple collections or database-level ops. Auto dropped after test. |
| 60 | +- `engine_client` — raw client access. |
| 61 | + |
| 62 | +## Execute Command |
| 63 | + |
| 64 | +Always use `execute_command()` with runCommand format to get test result, not driver methods. Setups can use methods. |
| 65 | + |
| 66 | +```python |
| 67 | +# ✅ runCommand format |
| 68 | +result = execute_command(collection, {"find": collection.name, "filter": {"a": 1}}) |
| 69 | + |
| 70 | +# ❌ Driver methods |
| 71 | +result = collection.find({"a": 1}) |
| 72 | +``` |
| 73 | + |
| 74 | +## Helper Functions |
| 75 | + |
| 76 | +Avoid deep helper function chains. One layer of abstraction on top of `execute_command()` is acceptable, don't add more abstraction layers unless justified. |
| 77 | + |
| 78 | +```python |
| 79 | +# ✅ Good: execute_expression wraps execute_command with aggregate pipeline boilerplate |
| 80 | +result = execute_expression(collection, {"$add": [1, 2]}) |
| 81 | + |
| 82 | +# ❌ Bad: trivial wrappers that just save a few characters add indirection for no clarity gain |
| 83 | +# result = execute_operator(collection, "$add", [1, 2]) |
| 84 | +``` |
| 85 | + |
| 86 | +Keep helpers in `utils/` at each test level. Helpers should reduce meaningful boilerplate (e.g., building an aggregate pipeline), not just shorten a single line. |
| 87 | + |
| 88 | +Minimize helper scope — one helper should do one thing. If a helper has many if/else branches handling different cases, split it into separate helpers at a lower folder level. |
| 89 | + |
| 90 | +## Parametrized Tests |
| 91 | + |
| 92 | +Use `@pytest.mark.parametrize` with dataclasses for operators with many test cases: |
| 93 | + |
| 94 | +```python |
| 95 | +@dataclass(frozen=True) |
| 96 | +class DivideTest(BaseTestCase): |
| 97 | + dividend: Any = None |
| 98 | + divisor: Any = None |
| 99 | + |
| 100 | +DIVIDE_TESTS: list[DivideTest] = [ |
| 101 | + DivideTest("int32", dividend=10, divisor=2, expected=5.0, msg="Should divide int32 values"), |
| 102 | + DivideTest("null_divisor", dividend=10, divisor=None, expected=None, msg="Should return null when divisor is null"), |
| 103 | + DivideTest("string_err", dividend=10, divisor="string", error_code=TYPE_MISMATCH_ERROR, msg="Should reject string"), |
| 104 | +] |
| 105 | + |
| 106 | +@pytest.mark.parametrize("test", DIVIDE_TESTS, ids=lambda t: t.id) |
| 107 | +def test_divide(collection, test): |
| 108 | + """Test $divide operator.""" |
| 109 | + result = execute_expression(collection, {"$divide": [test.dividend, test.divisor]}) |
| 110 | + assertResult(result, expected=test.expected, error_code=test.error_code, msg=test.msg) |
| 111 | +``` |
| 112 | + |
| 113 | +- `BaseTestCase` (from `framework.test_case`) provides `id`, `expected`, `error_code`, `msg` — extend it per operator |
| 114 | +- Shared helpers/dataclasses live in `utils/` at each level |
| 115 | +- `msg` is **required** — describes expected behavior, not input |
| 116 | +- Use constants from `framework.test_constants` (`INT32_MAX`, `FLOAT_NAN`, etc.) and `framework.error_codes` (`TYPE_MISMATCH_ERROR`, etc.) |
| 117 | + |
| 118 | +## Validation |
| 119 | + |
| 120 | +A pytest hook auto-validates during collection: |
| 121 | +- Files must match `test_*.py` (except `__init__.py`) |
| 122 | +- Test functions must have docstrings |
| 123 | +- Must use assertion helpers, not plain `assert` |
| 124 | +- One assertion per test function |
| 125 | +- Must use `execute_command()` or helpers from utils |
0 commit comments