Skip to content

Commit 0094b7d

Browse files
committed
Add test format validation, error codes framework, and $divide tests
Signed-off-by: Yunxuan Shi <yunxuan@amazon.com>
1 parent 36efdab commit 0094b7d

15 files changed

Lines changed: 1304 additions & 18 deletions

File tree

CONTRIBUTING.md

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,19 @@ Thank you for your interest in contributing to the DocumentDB Functional Tests!
3131

3232
## Writing Tests
3333

34+
For comprehensive testing guidance, see our detailed documentation:
35+
36+
- **[Quick Reference](docs/testing/QUICK_REFERENCE.md)** - Fast lookup for all testing rules
37+
- **[Test Format Guide](docs/testing/TEST_FORMAT.md)** - Test structure, naming, assertions, and tags
38+
- **[Test Coverage Guide](docs/testing/TEST_COVERAGE.md)** - Coverage strategies and edge case testing
39+
- **[Folder Structure Guide](docs/testing/FOLDER_STRUCTURE.md)** - Where to put tests with decision tree
40+
3441
### Test File Organization
3542

3643
- Place tests in the appropriate directory based on the operation being tested
3744
- Use descriptive file names: `test_<feature>.py`
3845
- Group related tests in the same file
46+
- See [Folder Structure Guide](docs/testing/FOLDER_STRUCTURE.md) for detailed organization rules
3947

4048
### Test Structure
4149

@@ -57,15 +65,23 @@ def test_descriptive_name(collection):
5765
- Any special conditions or edge cases
5866
"""
5967
# Arrange - Insert test data
60-
collection.insert_one({"name": "Alice", "age": 30})
68+
collection.insert_one({"a": 1, "b": 2})
6169

62-
# Act - Execute the operation being tested
63-
result = collection.find({"name": "Alice"})
70+
# Execute the operation being tested, use runCommand format
71+
execute_command(collection, {"find": collection.name, filter: {"a": 1}})
6472

65-
# Assert - Verify expected behavior
66-
assert len(list(result)) == 1
73+
# Assert expected behavior, don't use plain assert for consistent failure log format
74+
# Assert whole output when possible, to catch all unexpected regression
75+
expected = [{"_id": 0, "a": 1, "b": 2}]
76+
assertSuccess(result, expected)
6777
```
6878

79+
### Test Case Guidelines
80+
81+
- Each test function defines one test case
82+
- One assertion per test function
83+
- Use execute_command for all MongoDB operations
84+
6985
### Naming Conventions
7086

7187
- **Test functions**: `test_<what_is_being_tested>`
@@ -131,10 +147,10 @@ The framework provides three main fixtures:
131147
collection.insert_one({"name": "Alice"})
132148

133149
# Act - Execute operation
134-
result = collection.find_one({"name": "Alice"})
150+
result = execute_command(collection, {"find": collection.name, filter: {"name": "Alice"}})
135151

136152
# Assert - Verify results
137-
assert result["name"] == "Alice"
153+
assertSuccess(result, {"name": "Alice"})
138154
# Collection automatically dropped after test
139155
```
140156

docs/testing/TEST_COVERAGE.md

Lines changed: 355 additions & 0 deletions
Large diffs are not rendered by default.

docs/testing/TEST_FORMAT.md

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
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

Comments
 (0)