Skip to content

Commit 253e289

Browse files
test: processor_mixin
1 parent c1520c8 commit 253e289

File tree

4 files changed

+307
-3
lines changed

4 files changed

+307
-3
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,12 @@ windows:
132132
.venv\Scripts\jupyter lab
133133
```
134134

135+
## Tests
136+
137+
```sh
138+
uv run pytest
139+
```
140+
135141
## Scrapping
136142

137143
Voir le [README du sous-dossier](src/scrapping/README.md).

pyproject.toml

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ dev = [
2424
test = [
2525
"pytest>=8.3.5,<9.0.0",
2626
"pytest-mock>=3.15.1,<4.0.0",
27+
"pytest-cov>=7.0.0,<8.0.0",
2728
]
2829
scrap = [
2930
"scrapy>=2.13.3,<3.0.0",
@@ -67,5 +68,13 @@ scrap = [
6768
line-ending = "auto"
6869

6970
[tool.pytest.ini_options]
70-
addopts = ["--import-mode=importlib"]
71-
pythonpath = ["."]
71+
addopts = ["--import-mode=importlib", "--cov=src", "--cov-report=term-missing", "--no-cov-on-fail", "--cov-branch"]
72+
pythonpath = ["src"]
73+
74+
75+
[tool.coverage.run]
76+
branch = true
77+
data_file = ".coverage/default_output"
78+
omit = [
79+
"src/tests/*",
80+
]

src/tests/test_processor_mixin.py

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
from pathlib import Path
2+
from typing import Any
3+
4+
import pytest
5+
from pytest_mock import MockerFixture
6+
7+
from src.utils.processor_mixin import ProcessorMixin
8+
9+
10+
class DummyProcessorMixin(ProcessorMixin):
11+
save_contents: list[str] = []
12+
13+
@classmethod
14+
def write_dummy_input_file(cls):
15+
with open(cls.input_file, "w", encoding="utf-8") as f:
16+
f.write("dummy input content")
17+
18+
@classmethod
19+
def write_dummy_output_file(cls):
20+
with open(cls.output_file, "w", encoding="utf-8") as f:
21+
f.write("dummy output content")
22+
23+
@classmethod
24+
def pre_process(cls, content: Any | None, **kwargs) -> Any | None:
25+
return "pre_process " + content
26+
27+
@classmethod
28+
def post_process(cls, content: Any | None, **kwargs) -> Any | None:
29+
return content + " post_process"
30+
31+
@classmethod
32+
def fetch_from_api(cls, **kwargs):
33+
return "dummy api content"
34+
35+
@classmethod
36+
def fetch_from_file(cls, path: Path, **kwargs):
37+
with open(path, "r", encoding="utf-8") as f:
38+
return f.read()
39+
40+
@classmethod
41+
def save(cls, content: Any, path: Path) -> None:
42+
DummyProcessorMixin.save_contents.append(content)
43+
with open(path, "w", encoding="utf-8") as f:
44+
f.write(content)
45+
46+
47+
class NoContentProcessorMixin(ProcessorMixin):
48+
@classmethod
49+
def fetch_from_file(cls, path: Path, **kwargs):
50+
return None
51+
52+
@classmethod
53+
def write_dummy_output_file(cls):
54+
with open(cls.output_file, "w", encoding="utf-8") as f:
55+
f.write("dummy output content")
56+
57+
58+
class TestFetch:
59+
"""Test ProcessorMixin.fetch method.
60+
61+
Author: Nicolas Grosjean
62+
"""
63+
64+
@pytest.mark.parametrize("input_file_not_none", [True, False])
65+
def test_output_file_exists(self, tmp_path: Path, input_file_not_none: bool):
66+
DummyProcessorMixin.input_file = tmp_path / "input.txt" if input_file_not_none else None
67+
DummyProcessorMixin.output_file = tmp_path / "output.txt"
68+
DummyProcessorMixin.save_contents = []
69+
DummyProcessorMixin.write_dummy_output_file()
70+
actual = DummyProcessorMixin.fetch()
71+
assert actual == "dummy output content post_process"
72+
assert DummyProcessorMixin.save_contents == []
73+
74+
def test_input_and_output_file_exists(self, tmp_path: Path):
75+
DummyProcessorMixin.input_file = tmp_path / "input.txt"
76+
DummyProcessorMixin.output_file = tmp_path / "output.txt"
77+
DummyProcessorMixin.save_contents = []
78+
DummyProcessorMixin.write_dummy_input_file()
79+
DummyProcessorMixin.write_dummy_output_file()
80+
actual = DummyProcessorMixin.fetch()
81+
assert actual == "dummy output content post_process"
82+
assert DummyProcessorMixin.save_contents == []
83+
84+
@pytest.mark.parametrize("output_file_not_none", [True, False])
85+
def test_input_file_exists(self, tmp_path: Path, output_file_not_none: bool):
86+
DummyProcessorMixin.input_file = tmp_path / "input.txt"
87+
DummyProcessorMixin.output_file = (
88+
tmp_path / "output.txt" if output_file_not_none else None
89+
)
90+
DummyProcessorMixin.save_contents = []
91+
DummyProcessorMixin.write_dummy_input_file()
92+
actual = DummyProcessorMixin.fetch()
93+
assert actual == "pre_process dummy input content post_process"
94+
expected_save_contents = []
95+
if output_file_not_none:
96+
expected_save_contents.append("pre_process dummy input content")
97+
assert DummyProcessorMixin.save_contents == expected_save_contents
98+
99+
@pytest.mark.parametrize(
100+
"input_file_not_none, output_file_not_none",
101+
[(True, True), (True, False), (False, True), (False, False)],
102+
)
103+
def test_no_file_exists(
104+
self, tmp_path: Path, input_file_not_none: bool, output_file_not_none: bool
105+
):
106+
DummyProcessorMixin.input_file = tmp_path / "input.txt" if input_file_not_none else None
107+
DummyProcessorMixin.output_file = (
108+
tmp_path / "output.txt" if output_file_not_none else None
109+
)
110+
DummyProcessorMixin.save_contents = []
111+
actual = DummyProcessorMixin.fetch()
112+
assert actual == "pre_process dummy api content post_process"
113+
expected_save_contents = []
114+
if input_file_not_none:
115+
expected_save_contents.append("dummy api content")
116+
if output_file_not_none:
117+
expected_save_contents.append("pre_process dummy api content")
118+
assert DummyProcessorMixin.save_contents == expected_save_contents
119+
120+
@pytest.mark.parametrize(
121+
"input_file_not_none, output_file_not_none, input_file_exists, output_file_exists",
122+
[
123+
(True, True, True, True),
124+
(True, True, True, False),
125+
(True, True, False, True),
126+
(True, True, False, False),
127+
(True, False, True, False),
128+
(True, False, False, False),
129+
(False, True, False, True),
130+
(False, True, False, False),
131+
(False, False, False, False),
132+
],
133+
)
134+
def test_input_and_output_file_exists_reload_pipeline(
135+
self,
136+
tmp_path: Path,
137+
input_file_not_none: bool,
138+
output_file_not_none: bool,
139+
input_file_exists: bool,
140+
output_file_exists: bool,
141+
):
142+
DummyProcessorMixin.input_file = tmp_path / "input.txt" if input_file_not_none else None
143+
DummyProcessorMixin.output_file = (
144+
tmp_path / "output.txt" if output_file_not_none else None
145+
)
146+
DummyProcessorMixin.save_contents = []
147+
if input_file_exists:
148+
DummyProcessorMixin.write_dummy_input_file()
149+
if output_file_exists:
150+
DummyProcessorMixin.write_dummy_output_file()
151+
actual = DummyProcessorMixin.fetch(reload_pipeline=True)
152+
assert actual == "pre_process dummy api content post_process"
153+
expected_save_contents = []
154+
if input_file_not_none:
155+
expected_save_contents.append("dummy api content")
156+
if output_file_not_none:
157+
expected_save_contents.append("pre_process dummy api content")
158+
assert DummyProcessorMixin.save_contents == expected_save_contents
159+
160+
def test_stored_files_not_compatibles(self, tmp_path: Path, mocker: MockerFixture):
161+
def _raise_exception(*args):
162+
raise Exception("This is a test exception")
163+
164+
mocker.patch.object(
165+
DummyProcessorMixin,
166+
"fetch_from_file",
167+
side_effect=_raise_exception,
168+
)
169+
DummyProcessorMixin.input_file = tmp_path / "input.txt"
170+
DummyProcessorMixin.output_file = tmp_path / "output.txt"
171+
DummyProcessorMixin.save_contents = []
172+
DummyProcessorMixin.write_dummy_input_file()
173+
DummyProcessorMixin.write_dummy_output_file()
174+
actual = DummyProcessorMixin.fetch()
175+
assert actual == "pre_process dummy api content post_process"
176+
assert DummyProcessorMixin.save_contents == [
177+
"dummy api content",
178+
"pre_process dummy api content",
179+
]
180+
181+
@pytest.mark.parametrize(
182+
"method_with_exception_raised",
183+
[
184+
"fetch_from_api",
185+
"pre_process",
186+
"post_process",
187+
"save",
188+
],
189+
)
190+
def test_exception(
191+
self, tmp_path: Path, mocker: MockerFixture, method_with_exception_raised: str
192+
):
193+
def _raise_exception(*args):
194+
raise Exception("This is a test exception")
195+
196+
mocker.patch.object(
197+
DummyProcessorMixin,
198+
method_with_exception_raised,
199+
side_effect=_raise_exception,
200+
)
201+
DummyProcessorMixin.input_file = tmp_path / "input.txt"
202+
DummyProcessorMixin.output_file = tmp_path / "output.txt"
203+
DummyProcessorMixin.save_contents = []
204+
with pytest.raises(Exception, match="This is a test exception"):
205+
DummyProcessorMixin.fetch()
206+
207+
208+
class TestRun:
209+
"""Test ProcessorMixin.run method.
210+
211+
Author: Nicolas Grosjean
212+
"""
213+
214+
@pytest.mark.parametrize(
215+
"input_file_not_none, output_file_not_none",
216+
[(True, True), (True, False), (False, True), (False, False)],
217+
)
218+
def test_no_file_exists(
219+
self, tmp_path: Path, input_file_not_none: bool, output_file_not_none: bool
220+
):
221+
DummyProcessorMixin.input_file = tmp_path / "input.txt" if input_file_not_none else None
222+
DummyProcessorMixin.output_file = (
223+
tmp_path / "output.txt" if output_file_not_none else None
224+
)
225+
DummyProcessorMixin.save_contents = []
226+
DummyProcessorMixin.run()
227+
expected_save_contents = []
228+
if input_file_not_none:
229+
expected_save_contents.append("dummy api content")
230+
if output_file_not_none:
231+
expected_save_contents.append("pre_process dummy api content")
232+
assert DummyProcessorMixin.save_contents == expected_save_contents
233+
234+
def test_no_content(self, tmp_path: Path, caplog: pytest.LogCaptureFixture):
235+
NoContentProcessorMixin.output_file = tmp_path / "output.txt"
236+
NoContentProcessorMixin.write_dummy_output_file()
237+
NoContentProcessorMixin.run()
238+
assert caplog.records[-1].message == "NoContentProcessorMixin: have no content"

0 commit comments

Comments
 (0)