Skip to content

Commit 5d4664a

Browse files
committed
Standardize mock_response_factory usage in nutrition tests
1 parent 27e9a55 commit 5d4664a

11 files changed

+232
-41
lines changed

standardize_mock_responses.py

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Script to standardize mock_response usage in test files.
4+
5+
This script finds test files that use direct mock_response manipulation and
6+
converts them to use mock_response_factory instead.
7+
"""
8+
9+
# Standard library imports
10+
import os
11+
from pathlib import Path
12+
import re
13+
import sys
14+
from typing import List
15+
from typing import Tuple
16+
17+
18+
def find_pattern_in_file(file_path: str, pattern: str) -> List[str]:
19+
"""Find all matches of pattern in the given file."""
20+
with open(file_path, "r") as f:
21+
content = f.read()
22+
return re.findall(pattern, content, re.MULTILINE)
23+
24+
25+
def transform_file(file_path: str, dry_run: bool = False) -> bool:
26+
"""Transform a file to use mock_response_factory."""
27+
with open(file_path, "r") as f:
28+
content = f.read()
29+
30+
# Backup original content
31+
original_content = content
32+
33+
# Pattern 1: def test_*(mock_response) -> def test_*(mock_response_factory)
34+
# Find all test function definitions
35+
test_pattern = r"(def\s+test_\w+\([^)]*)(,\s*mock_response)(\s*[,)].*)"
36+
37+
def param_replacement(match):
38+
before = match.group(1)
39+
param = match.group(2).replace("mock_response", "mock_response_factory")
40+
after = match.group(3)
41+
return f"{before}{param}{after}"
42+
43+
content = re.sub(test_pattern, param_replacement, content)
44+
45+
# Pattern 2: mock_response.json.return_value = {...}
46+
pattern2 = r"([ \t]*)mock_response\.json\.return_value\s*=\s*({[^;]*}|\[[^;]*\])"
47+
48+
def replacement2(match):
49+
indent = match.group(1)
50+
data = match.group(2).strip()
51+
# Ensure data has balanced braces
52+
open_braces = data.count("{") - data.count("}")
53+
open_brackets = data.count("[") - data.count("]")
54+
55+
if open_braces != 0 or open_brackets != 0:
56+
# Skip this match as it has unbalanced braces or brackets
57+
return match.group(0)
58+
59+
return f"{indent}mock_response = mock_response_factory(\n{indent} 200, \n{indent} {data}\n{indent})"
60+
61+
content = re.sub(pattern2, replacement2, content)
62+
63+
# Pattern 3: mock_response.status_code = 204
64+
pattern3 = r"([ \t]*)mock_response\.status_code\s*=\s*(\d+)"
65+
66+
def replacement3(match):
67+
indent = match.group(1)
68+
status_code = match.group(2)
69+
return f"{indent}mock_response = mock_response_factory({status_code})"
70+
71+
content = re.sub(pattern3, replacement3, content)
72+
73+
# Pattern 4: We're disabling this pattern for now as it was causing issues
74+
# The goal was to change order of assignments (response assignment should come before oauth assignment)
75+
# but it was causing syntax errors
76+
"""
77+
pattern4 = r'([ \t]*)(.*?)\.oauth(?:\.|\w+\.)*request\.return_value\s*=\s*mock_response\n([ \t]*)mock_response\s*=\s*mock_response_factory'
78+
79+
def replacement4(match):
80+
indent1 = match.group(1)
81+
obj = match.group(2)
82+
indent2 = match.group(3)
83+
return f"{indent2}mock_response = mock_response_factory\n{indent1}{obj}.oauth.request.return_value = mock_response"
84+
85+
content = re.sub(pattern4, replacement4, content)
86+
"""
87+
88+
# Pattern 5: Fix cases where test was updated to use mock_response_factory as parameter
89+
# but still uses mock_response in the body
90+
pattern5 = r"def\s+test_\w+\([^)]*mock_response_factory[^)]*\).*?(?=\n\s*def|\Z)"
91+
92+
def fix_mock_response_usage(match):
93+
test_func = match.group(0)
94+
# If the function uses mock_response_factory but also has direct mock_response usage
95+
if (
96+
"mock_response.json.return_value =" in test_func
97+
or "mock_response.status_code =" in test_func
98+
):
99+
# Add a mock_response declaration at the beginning of the function body
100+
# Find the first indented line after the function def
101+
lines = test_func.split("\n")
102+
for i, line in enumerate(lines):
103+
if i > 0 and line.strip() and not line.strip().startswith("#"):
104+
indent = re.match(r"(\s*)", line).group(1)
105+
# Insert the mock_response assignment after docstring (if any)
106+
for j in range(i, len(lines)):
107+
if not lines[j].strip().startswith('"""') and not lines[
108+
j
109+
].strip().startswith("'''"):
110+
lines.insert(j, f"{indent}mock_response = mock_response_factory(200)")
111+
break
112+
break
113+
return "\n".join(lines)
114+
return test_func
115+
116+
content = re.sub(pattern5, fix_mock_response_usage, content, flags=re.DOTALL)
117+
118+
# If no changes were made, return False
119+
if content == original_content:
120+
return False
121+
122+
# Write the changes back to the file
123+
if not dry_run:
124+
with open(file_path, "w") as f:
125+
f.write(content)
126+
127+
return True
128+
129+
130+
def find_files_to_transform() -> List[Tuple[str, bool]]:
131+
"""Find all test files that need to be transformed."""
132+
test_dir = Path("tests")
133+
result = []
134+
135+
for root, _, files in os.walk(test_dir):
136+
for file in files:
137+
if file.endswith(".py") and file.startswith("test_"):
138+
file_path = os.path.join(root, file)
139+
140+
# Check if file uses mock_response directly
141+
uses_mock_response = bool(
142+
find_pattern_in_file(
143+
file_path, r"def\s+test_\w+\([^)]*,\s*mock_response\s*[,)]"
144+
)
145+
)
146+
147+
# Check if file directly manipulates mock_response
148+
manipulates_mock_response = bool(
149+
find_pattern_in_file(
150+
file_path, r"mock_response\.(json\.return_value|status_code)\s*="
151+
)
152+
)
153+
154+
# Check if file uses mock_response_factory
155+
uses_factory = bool(
156+
find_pattern_in_file(file_path, r"mock_response\s*=\s*mock_response_factory")
157+
)
158+
159+
# Determine if file needs transformation
160+
needs_transform = (
161+
uses_mock_response or manipulates_mock_response
162+
) and not uses_factory
163+
164+
if needs_transform:
165+
result.append((file_path, needs_transform))
166+
167+
return result
168+
169+
170+
def main():
171+
"""Main entry point."""
172+
dry_run = "--dry-run" in sys.argv
173+
files = find_files_to_transform()
174+
175+
print(f"Found {len(files)} files that need to be transformed.")
176+
if dry_run:
177+
print("Running in dry-run mode. No files will be modified.")
178+
179+
for file_path, _ in files:
180+
print(f"Transforming {file_path}...", end="")
181+
transformed = transform_file(file_path, dry_run)
182+
print(" TRANSFORMED" if transformed else " SKIPPED (no changes)")
183+
184+
print("\nDone!")
185+
print(f"Transformed {len(files)} files.")
186+
187+
if dry_run:
188+
print("\nRun without --dry-run to apply changes.")
189+
190+
191+
if __name__ == "__main__":
192+
main()

tests/fitbit_client/resources/nutrition/test_add_favorite_foods.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33
"""Tests for the add_favorite_foods endpoint."""
44

55

6-
def test_add_favorite_foods_success(nutrition_resource, mock_response):
6+
def test_add_favorite_foods_success(nutrition_resource, mock_response_factory):
77
"""Test successful addition of a food to favorites"""
88
food_id = 12345
9-
mock_response.json.return_value = {"success": True}
9+
mock_response = mock_response_factory(200, {"success": True})
1010
nutrition_resource.oauth.request.return_value = mock_response
1111
result = nutrition_resource.add_favorite_foods(food_id=food_id)
1212
assert result == mock_response.json.return_value

tests/fitbit_client/resources/nutrition/test_create_food_goal.py

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22

33
"""Tests for the create_food_goal endpoint."""
44

5-
# Third party imports
6-
75
# Third party imports
86
from pytest import raises
97

@@ -12,9 +10,9 @@
1210
from fitbit_client.resources._constants import FoodPlanIntensity
1311

1412

15-
def test_create_food_goal_with_calories_success(nutrition_resource, mock_response):
13+
def test_create_food_goal_with_calories_success(nutrition_resource, mock_response_factory):
1614
"""Test successful creation of a food goal using calories"""
17-
mock_response.json.return_value = {"goals": {"calories": 2000}}
15+
mock_response = mock_response_factory(200, {"goals": {"calories": 2000}})
1816
nutrition_resource.oauth.request.return_value = mock_response
1917
result = nutrition_resource.create_food_goal(calories=2000)
2018
assert result == mock_response.json.return_value
@@ -28,12 +26,11 @@ def test_create_food_goal_with_calories_success(nutrition_resource, mock_respons
2826
)
2927

3028

31-
def test_create_food_goal_with_intensity_success(nutrition_resource, mock_response):
29+
def test_create_food_goal_with_intensity_success(nutrition_resource, mock_response_factory):
3230
"""Test successful creation of a food goal using intensity"""
33-
mock_response.json.return_value = {
34-
"foodPlan": {"intensity": "EASIER"},
35-
"goals": {"calories": 2200},
36-
}
31+
mock_response = mock_response_factory(
32+
200, {"foodPlan": {"intensity": "EASIER"}, "goals": {"calories": 2200}}
33+
)
3734
nutrition_resource.oauth.request.return_value = mock_response
3835
result = nutrition_resource.create_food_goal(
3936
intensity=FoodPlanIntensity.EASIER, personalized=True

tests/fitbit_client/resources/nutrition/test_create_food_goal_intensity.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,13 @@
22

33
"""Tests for the create_food_goal_intensity endpoint."""
44

5-
# Local imports
6-
75
# Local imports
86
from fitbit_client.resources._constants import FoodPlanIntensity
97

108

11-
def test_create_food_goal_intensity_without_personalized(nutrition_resource, mock_response):
9+
def test_create_food_goal_intensity_without_personalized(nutrition_resource, mock_response_factory):
1210
"""Test creating food goal with intensity but without personalized flag (lines 217-220)"""
13-
mock_response.json.return_value = {"foodPlan": {"intensity": "EASIER"}}
11+
mock_response = mock_response_factory(200, {"foodPlan": {"intensity": "EASIER"}})
1412
nutrition_resource.oauth.request.return_value = mock_response
1513
result = nutrition_resource.create_food_goal(intensity=FoodPlanIntensity.EASIER)
1614
assert result == mock_response.json.return_value

tests/fitbit_client/resources/nutrition/test_create_food_log_custom_minimal.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,13 @@
22

33
"""Tests for the create_food_log_custom_minimal endpoint."""
44

5-
# Local imports
6-
75
# Local imports
86
from fitbit_client.resources._constants import MealType
97

108

11-
def test_create_food_log_custom_minimal(nutrition_resource, mock_response):
9+
def test_create_food_log_custom_minimal(nutrition_resource, mock_response_factory):
1210
"""Test creating custom food log with minimal parameters (no brand or nutritional values)"""
13-
mock_response.json.return_value = {"foodLog": {"logId": 12345}}
11+
mock_response = mock_response_factory(200, {"foodLog": {"logId": 12345}})
1412
nutrition_resource.oauth.request.return_value = mock_response
1513
result = nutrition_resource.create_food_log(
1614
date="2025-02-08",

tests/fitbit_client/resources/nutrition/test_create_meal.py

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,19 @@
33
"""Tests for the create_meal endpoint."""
44

55

6-
def test_create_meal_success(nutrition_resource, mock_response):
6+
def test_create_meal_success(nutrition_resource, mock_response_factory):
77
"""Test successful creation of a meal"""
8-
mock_response.json.return_value = {
9-
"meal": {
10-
"id": 12345,
11-
"name": "Test Meal",
12-
"description": "Test meal description",
13-
"mealFoods": [{"foodId": 67890, "amount": 100.0, "unitId": 147}],
14-
}
15-
}
8+
mock_response = mock_response_factory(
9+
200,
10+
{
11+
"meal": {
12+
"id": 12345,
13+
"name": "Test Meal",
14+
"description": "Test meal description",
15+
"mealFoods": [{"foodId": 67890, "amount": 100.0, "unitId": 147}],
16+
}
17+
},
18+
)
1619
nutrition_resource.oauth.request.return_value = mock_response
1720
foods = [{"food_id": 67890, "amount": 100.0, "unit_id": 147}]
1821
result = nutrition_resource.create_meal(

tests/fitbit_client/resources/nutrition/test_create_water_goal.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
"""Tests for the create_water_goal endpoint."""
44

55

6-
def test_create_water_goal_success(nutrition_resource, mock_response):
6+
def test_create_water_goal_success(nutrition_resource, mock_response_factory):
77
"""Test successful creation of a water goal"""
8-
mock_response.json.return_value = {"goal": {"goal": 2000.0, "startDate": "2025-02-08"}}
8+
mock_response = mock_response_factory(
9+
200, {"goal": {"goal": 2000.0, "startDate": "2025-02-08"}}
10+
)
911
nutrition_resource.oauth.request.return_value = mock_response
1012
result = nutrition_resource.create_water_goal(target=2000.0)
1113
assert result == mock_response.json.return_value

tests/fitbit_client/resources/nutrition/test_create_water_log.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22

33
"""Tests for the create_water_log endpoint."""
44

5-
# Third party imports
6-
75
# Third party imports
86
from pytest import raises
97

@@ -12,9 +10,9 @@
1210
from fitbit_client.resources._constants import WaterUnit
1311

1412

15-
def test_create_water_log_success(nutrition_resource, mock_response):
13+
def test_create_water_log_success(nutrition_resource, mock_response_factory):
1614
"""Test successful creation of a water log entry"""
17-
mock_response.json.return_value = {"waterLog": {"logId": 12345, "amount": 500.0}}
15+
mock_response = mock_response_factory(200, {"waterLog": {"logId": 12345, "amount": 500.0}})
1816
nutrition_resource.oauth.request.return_value = mock_response
1917
result = nutrition_resource.create_water_log(
2018
amount=500.0, date="2025-02-08", unit=WaterUnit.MILLILITERS
@@ -36,8 +34,9 @@ def test_create_water_log_invalid_date(nutrition_resource):
3634
nutrition_resource.create_water_log(amount=500.0, date="invalid-date")
3735

3836

39-
def test_create_water_log_allows_today(nutrition_resource, mock_response):
37+
def test_create_water_log_allows_today(nutrition_resource, mock_response_factory):
4038
"""Test that 'today' is accepted as a valid date"""
39+
mock_response = mock_response_factory(200)
4140
mock_response.json.return_value = {"waterLog": {"logId": 12345}}
4241
mock_response.headers = {"content-type": "application/json"}
4342
nutrition_resource.oauth.request.return_value = mock_response

tests/fitbit_client/resources/nutrition/test_custom_user_id.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from fitbit_client.resources._constants import MealType
99

1010

11-
def test_custom_user_id(nutrition_resource, mock_response):
11+
def test_custom_user_id(nutrition_resource, mock_response_factory):
1212
"""Test that endpoints correctly handle custom user IDs"""
1313
custom_user_id = "123ABC"
1414
test_cases = [
@@ -35,7 +35,7 @@ def test_custom_user_id(nutrition_resource, mock_response):
3535
f"https://api.fitbit.com/1/user/{custom_user_id}/foods/log/water/date/2025-02-08.json",
3636
),
3737
]
38-
mock_response.json.return_value = {"success": True}
38+
mock_response = mock_response_factory(200, {"success": True})
3939
nutrition_resource.oauth.request.return_value = mock_response
4040
for method, params, expected_url in test_cases:
4141
result = method(**params)

tests/fitbit_client/resources/nutrition/test_delete_favorite_foods.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
"""Tests for the delete_favorite_foods endpoint."""
44

55

6-
def test_delete_favorite_food_success(nutrition_resource, mock_response):
6+
def test_delete_favorite_food_success(nutrition_resource, mock_response_factory):
77
"""Test successful deletion of a favorite food"""
8-
mock_response.status_code = 204
8+
mock_response = mock_response_factory(204)
99
nutrition_resource.oauth.request.return_value = mock_response
1010
result = nutrition_resource.delete_favorite_food(food_id=12345)
1111
assert result is None

0 commit comments

Comments
 (0)