Writing Python Flow Tests
Guidelines for writing new end-to-end Python flow tests in tests/pytests/.
Framework
Tests use the RLTest framework. The typical pattern is:
- Create an index with
env.expect('FT.CREATE', ...).ok()
- Load data with
conn.execute_command('HSET', ...)
- Assert query results with
env.cmd(...) or env.expect(...)
Finding where to add tests
- Search existing test files in
tests/pytests/ for related functionality using Grep
(function names, command names, feature names).
- Determine whether an existing test can be extended or a new test is needed.
- Look at nearby tests in the same file for style and patterns specific to that file.
Test function signature
- Accept
env as a parameter when the test works with the default environment (dialect 2 on CI):def testMyFeature(env):
- Only create a custom
Env() when you need specific settings that differ from the default, such as:
protocol=3 (to access res['warning'] dicts)
DEFAULT_DIALECT 1 (to test legacy behavior)
- Other non-default
moduleArgs
- Always document why a custom
Env() is needed if it's not obvious.
Cluster considerations
- Add
@skip(cluster=True) to tests that don't exercise cluster-specific behavior. This avoids redundant test runs.
- If a test does need to run in cluster mode, use
{hash_tag} key prefixes (e.g., {doc}:1) to ensure keys land on the same shard.
Index creation
- Use
env.expect('FT.CREATE', ...).ok() for creating indexes — not conn.execute_command('FT.CREATE', ...).
- Reserve
conn (getConnectionByEnv(env)) for key-write commands like HSET, DEL, etc.
Waiting for index
- Data inserted before
FT.CREATE: Background indexing is activated. Call waitForIndex(env, 'idx') after FT.CREATE to wait for the backfill to complete before querying.
- Data inserted after
FT.CREATE: Each HSET/JSON.SET is immediately acknowledged by the index — no waitForIndex needed.
- Do not call
waitForIndex right after FT.CREATE with no pre-existing data — there is nothing to wait for.
Assertions
- Compare the full result when the response is deterministic and small:
# Good — full result comparison
res = env.cmd('FT.SEARCH', 'idx', '@t:{al*}', 'NOCONTENT')
env.assertEqual(res, [1, 'doc1'])
# Good — empty result
res = env.cmd('FT.SEARCH', 'idx', '@t:{a*}', 'NOCONTENT')
env.assertEqual(res, [0])
- Add
message=res when the assertion checks only part of the result (e.g., assertGreaterEqual), so failures show the actual response:env.assertGreaterEqual(res[0], 9, message=res)
- Use
env.expect(...).error().contains('...') for error-path tests.
- Use
env.assertContains(...) for checking substrings in responses (e.g., warning messages, explain output).
Determinism
A test must pass, or fail, for the same reason on every machine. Anything host-speed
dependent — a wall-clock TIMEOUT, a sleep, a race with background indexing or GC — should
prefer a deterministic hook instead; grep _FT.DEBUG in tests/pytests/ for what exists,
and check the one you pick reaches the code path under test.
Deprecated commands
- Do not use
FT.ADD — use HSET via conn.execute_command('HSET', ...) instead.
FT.ADD does not work in cluster mode and is deprecated.
Test structure
- Include a docstring explaining what code path or behavior the test exercises.
- Keep tests focused — one test per code path or behavior.
- Restore global config changes (e.g.,
MAXPREFIXEXPANSIONS) at the end of the test — tests in a file share one server.
1---2name: write-flow-tests3description: Guidelines for writing Python flow tests (end-to-end behavioral tests). Use this when writing new Python tests in tests/pytests/, and as the review criteria when reviewing changes to them.4---56# Writing Python Flow Tests78Guidelines for writing new end-to-end Python flow tests in `tests/pytests/`.910## Framework1112Tests use the `RLTest` framework. The typical pattern is:131. Create an index with `env.expect('FT.CREATE', ...).ok()`142. Load data with `conn.execute_command('HSET', ...)`153. Assert query results with `env.cmd(...)` or `env.expect(...)`1617## Finding where to add tests1819- Search existing test files in `tests/pytests/` for related functionality using Grep20 (function names, command names, feature names).21- Determine whether an existing test can be extended or a new test is needed.22- Look at nearby tests in the same file for style and patterns specific to that file.2324## Test function signature2526- Accept `env` as a parameter when the test works with the default environment (dialect 2 on CI):27 ```python28 def testMyFeature(env):29 ```30- Only create a custom `Env()` when you need specific settings that differ from the default, such as:31 - `protocol=3` (to access `res['warning']` dicts)32 - `DEFAULT_DIALECT 1` (to test legacy behavior)33 - Other non-default `moduleArgs`34- Always document *why* a custom `Env()` is needed if it's not obvious.3536## Cluster considerations3738- Add `@skip(cluster=True)` to tests that don't exercise cluster-specific behavior. This avoids redundant test runs.39- If a test does need to run in cluster mode, use `{hash_tag}` key prefixes (e.g., `{doc}:1`) to ensure keys land on the same shard.4041## Index creation4243- Use `env.expect('FT.CREATE', ...).ok()` for creating indexes — not `conn.execute_command('FT.CREATE', ...)`.44- Reserve `conn` (`getConnectionByEnv(env)`) for key-write commands like `HSET`, `DEL`, etc.4546## Waiting for index4748- **Data inserted before `FT.CREATE`**: Background indexing is activated. Call `waitForIndex(env, 'idx')` after `FT.CREATE` to wait for the backfill to complete before querying.49- **Data inserted after `FT.CREATE`**: Each `HSET`/`JSON.SET` is immediately acknowledged by the index — no `waitForIndex` needed.50- Do not call `waitForIndex` right after `FT.CREATE` with no pre-existing data — there is nothing to wait for.5152## Assertions5354- **Compare the full result** when the response is deterministic and small:55 ```python56 # Good — full result comparison57 res = env.cmd('FT.SEARCH', 'idx', '@t:{al*}', 'NOCONTENT')58 env.assertEqual(res, [1, 'doc1'])5960 # Good — empty result61 res = env.cmd('FT.SEARCH', 'idx', '@t:{a*}', 'NOCONTENT')62 env.assertEqual(res, [0])63 ```64- **Add `message=res`** when the assertion checks only part of the result (e.g., `assertGreaterEqual`), so failures show the actual response:65 ```python66 env.assertGreaterEqual(res[0], 9, message=res)67 ```68- Use `env.expect(...).error().contains('...')` for error-path tests.69- Use `env.assertContains(...)` for checking substrings in responses (e.g., warning messages, explain output).7071## Determinism7273A test must pass, or fail, for the same reason on every machine. Anything host-speed74dependent — a wall-clock `TIMEOUT`, a sleep, a race with background indexing or GC — should75prefer a deterministic hook instead; grep `_FT.DEBUG` in `tests/pytests/` for what exists,76and check the one you pick reaches the code path under test.7778## Deprecated commands7980- Do not use `FT.ADD` — use `HSET` via `conn.execute_command('HSET', ...)` instead.81- `FT.ADD` does not work in cluster mode and is deprecated.8283## Test structure8485- Include a docstring explaining what code path or behavior the test exercises.86- Keep tests focused — one test per code path or behavior.87- Restore global config changes (e.g., `MAXPREFIXEXPANSIONS`) at the end of the test — tests in a file share one server.