Skip to content

Commit 8974907

Browse files
authored
Merge pull request #1394 from anik120/parameterized-integration-tests
LCORE-716: Refactor duplicate integration/endpoints tests to parameterized tests
2 parents 17ba188 + 2ff9ee6 commit 8974907

File tree

5 files changed

+540
-622
lines changed

5 files changed

+540
-622
lines changed

tests/integration/README.md

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ This directory contains integration tests for Lightspeed Core Stack. Integration
1010
- [Test Constants](#test-constants)
1111
- [Writing Integration Tests](#writing-integration-tests)
1212
- [Running Tests](#running-tests)
13+
- [Data-Driven (Parameterized) Tests](#data-driven-parameterized-tests)
1314
- [Best Practices](#best-practices)
1415

1516
## Getting Started
@@ -252,6 +253,107 @@ uv run make test-integration
252253
uv run pytest tests/integration/ -v --tb=short
253254
```
254255

256+
## Data-Driven (Parameterized) Tests
257+
258+
### Overview
259+
260+
Data-driven tests use `@pytest.mark.parametrize` to run the same test logic with different inputs. This eliminates duplicate code and makes test coverage more visible.
261+
262+
**Benefits:**
263+
- Reduce code duplication
264+
- Add new test cases by simply adding to the data table
265+
- See all test scenarios at a glance
266+
- Consistent structure across similar tests
267+
268+
### When to Use
269+
270+
Use parameterized tests when you have:
271+
- **Multiple similar tests** that differ only in input data and expected output
272+
- **Validation tests** with multiple valid/invalid scenarios
273+
- **Error handling tests** with different error conditions
274+
275+
### Pattern
276+
277+
```python
278+
# Define test cases as a list
279+
TEST_CASES = [
280+
pytest.param(
281+
{
282+
"input": "value1",
283+
"expected_result": "result1",
284+
},
285+
id="descriptive_test_name_1",
286+
),
287+
pytest.param(
288+
{
289+
"input": "value2",
290+
"expected_result": "result2",
291+
},
292+
id="descriptive_test_name_2",
293+
),
294+
]
295+
296+
@pytest.mark.asyncio
297+
@pytest.mark.parametrize("test_case", TEST_CASES)
298+
async def test_example_data_driven(
299+
test_case: dict,
300+
# ... fixtures
301+
) -> None:
302+
"""Data-driven test for example functionality.
303+
304+
Tests multiple scenarios:
305+
- Scenario 1 description
306+
- Scenario 2 description
307+
308+
Parameters:
309+
test_case: Dictionary containing test parameters
310+
# ... other fixtures
311+
"""
312+
input_value = test_case["input"]
313+
expected = test_case["expected_result"]
314+
315+
result = await function_under_test(input_value)
316+
317+
assert result == expected
318+
```
319+
320+
### Best Practices for Parameterized Tests
321+
322+
1. **Use descriptive `id` values** - They appear in test output
323+
```python
324+
pytest.param(..., id="attachment_unknown_type_returns_422") # Good
325+
pytest.param(..., id="test1") # Bad
326+
```
327+
328+
2. **Group related test data** - Keep test cases together at module level
329+
```python
330+
ATTACHMENT_TEST_CASES = [...] # Define near the test that uses it
331+
```
332+
333+
3. **Document all scenarios** - List scenarios in docstring
334+
```python
335+
"""Data-driven test for attachments.
336+
337+
Tests:
338+
- Single attachment
339+
- Empty payload
340+
- Invalid type (422 error)
341+
"""
342+
```
343+
344+
4. **Keep test logic simple** - Use if/else only for success vs. error paths
345+
```python
346+
if expected_status == 200:
347+
# Success assertions
348+
else:
349+
# Error assertions
350+
```
351+
352+
5. **Use consistent dict keys** - Standardize parameter names
353+
```python
354+
{"expected_status": 200, "expected_error": None} # Consistent
355+
```
356+
255357
## Best Practices
256358

257359
### 1. Use Common Fixtures

tests/integration/endpoints/test_model_list.py

Lines changed: 53 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -90,57 +90,62 @@ def mock_llama_stack_client_failing_fixture(
9090
yield mock_client
9191

9292

93-
@pytest.mark.asyncio
94-
async def test_models_list(
95-
test_config: AppConfig,
96-
mock_llama_stack_client: AsyncMockType,
97-
test_request: Request,
98-
test_auth: AuthTuple,
99-
) -> None:
100-
"""Test that models endpoint returns successful response.
101-
102-
This integration test verifies:
103-
- Model list handler
104-
105-
Parameters:
106-
----------
107-
test_config: Test configuration
108-
mock_llama_stack_client: Mocked Llama Stack client
109-
test_request: FastAPI request
110-
test_auth: noop authentication tuple
111-
"""
112-
_ = test_config
113-
_ = mock_llama_stack_client
114-
115-
response = await models_endpoint_handler(
116-
request=test_request,
117-
auth=test_auth,
118-
model_type=ModelFilter(model_type=None),
119-
)
120-
121-
# Verify response structure
122-
assert response is not None
123-
assert len(response.models) == 2
124-
assert response.models[0]["identifier"] == "test-provider/test-model-1"
125-
assert response.models[0]["api_model_type"] == "llm"
126-
assert response.models[1]["identifier"] == "test-provider/test-model-2"
127-
assert response.models[1]["api_model_type"] == "embedding"
93+
MODEL_FILTER_TEST_CASES = [
94+
pytest.param(
95+
{
96+
"filter_type": None,
97+
"expected_count": 2,
98+
"expected_models": [
99+
{"identifier": "test-provider/test-model-1", "api_model_type": "llm"},
100+
{
101+
"identifier": "test-provider/test-model-2",
102+
"api_model_type": "embedding",
103+
},
104+
],
105+
},
106+
id="no_filter_returns_all_models",
107+
),
108+
pytest.param(
109+
{
110+
"filter_type": "llm",
111+
"expected_count": 1,
112+
"expected_models": [
113+
{"identifier": "test-provider/test-model-1", "api_model_type": "llm"},
114+
],
115+
},
116+
id="filter_llm_returns_llm_model",
117+
),
118+
pytest.param(
119+
{
120+
"filter_type": "foobar",
121+
"expected_count": 0,
122+
"expected_models": [],
123+
},
124+
id="filter_unknown_type_returns_empty",
125+
),
126+
]
128127

129128

130129
@pytest.mark.asyncio
131-
async def test_models_list_filter_model_type_llm(
130+
@pytest.mark.parametrize("test_case", MODEL_FILTER_TEST_CASES)
131+
async def test_models_list_with_filter(
132+
test_case: dict,
132133
test_config: AppConfig,
133134
mock_llama_stack_client: AsyncMockType,
134135
test_request: Request,
135136
test_auth: AuthTuple,
136137
) -> None:
137-
"""Test that models endpoint returns successful response.
138+
"""Tests for models endpoint filtering.
138139
139-
This integration test verifies:
140-
- Model list handler
140+
Tests different model_type filter scenarios:
141+
- No filter (returns all models)
142+
- Filter by llm type
143+
- Filter by unknown type (returns empty)
141144
142145
Parameters:
143146
----------
147+
test_case: Dictionary containing test parameters (filter_type,
148+
expected_count, expected_models)
144149
test_config: Test configuration
145150
mock_llama_stack_client: Mocked Llama Stack client
146151
test_request: FastAPI request
@@ -149,83 +154,24 @@ async def test_models_list_filter_model_type_llm(
149154
_ = test_config
150155
_ = mock_llama_stack_client
151156

152-
response = await models_endpoint_handler(
153-
request=test_request, auth=test_auth, model_type=ModelFilter(model_type="llm")
154-
)
155-
156-
# Verify response structure
157-
assert response is not None
158-
assert len(response.models) == 1
159-
assert response.models[0]["identifier"] == "test-provider/test-model-1"
160-
assert response.models[0]["api_model_type"] == "llm"
161-
162-
163-
@pytest.mark.asyncio
164-
async def test_models_list_filter_model_type_embedding(
165-
test_config: AppConfig,
166-
mock_llama_stack_client: AsyncMockType,
167-
test_request: Request,
168-
test_auth: AuthTuple,
169-
) -> None:
170-
"""Test that models endpoint returns successful response.
171-
172-
This integration test verifies:
173-
- Model list handler
174-
175-
Parameters:
176-
----------
177-
test_config: Test configuration
178-
mock_llama_stack_client: Mocked Llama Stack client
179-
test_request: FastAPI request
180-
test_auth: noop authentication tuple
181-
"""
182-
_ = test_config
183-
_ = mock_llama_stack_client
157+
filter_type = test_case["filter_type"]
158+
expected_count = test_case["expected_count"]
159+
expected_models = test_case["expected_models"]
184160

185161
response = await models_endpoint_handler(
186162
request=test_request,
187163
auth=test_auth,
188-
model_type=ModelFilter(model_type="embedding"),
164+
model_type=ModelFilter(model_type=filter_type),
189165
)
190166

191167
# Verify response structure
192168
assert response is not None
193-
assert len(response.models) == 1
194-
assert response.models[0]["identifier"] == "test-provider/test-model-2"
195-
assert response.models[0]["api_model_type"] == "embedding"
196-
197-
198-
@pytest.mark.asyncio
199-
async def test_models_list_filter_model_type_unknown(
200-
test_config: AppConfig,
201-
mock_llama_stack_client: AsyncMockType,
202-
test_request: Request,
203-
test_auth: AuthTuple,
204-
) -> None:
205-
"""Test that models endpoint returns successful response.
169+
assert len(response.models) == expected_count
206170

207-
This integration test verifies:
208-
- Model list handler
209-
210-
Parameters:
211-
----------
212-
test_config: Test configuration
213-
mock_llama_stack_client: Mocked Llama Stack client
214-
test_request: FastAPI request
215-
test_auth: noop authentication tuple
216-
"""
217-
_ = test_config
218-
_ = mock_llama_stack_client
219-
220-
response = await models_endpoint_handler(
221-
request=test_request,
222-
auth=test_auth,
223-
model_type=ModelFilter(model_type="foobar"),
224-
)
225-
226-
# Verify response structure
227-
assert response is not None
228-
assert len(response.models) == 0
171+
# Verify each expected model
172+
for i, expected_model in enumerate(expected_models):
173+
assert response.models[i]["identifier"] == expected_model["identifier"]
174+
assert response.models[i]["api_model_type"] == expected_model["api_model_type"]
229175

230176

231177
@pytest.mark.asyncio

0 commit comments

Comments
 (0)