You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
test: add unit tests for pipeline, analyzer, bootstrap, and collectors
Adds offline, fully-mocked unit tests covering the ingestion pipeline, AI analyzer, bootstrap, Pearson VUE and training-provider collectors, plus settings, email sender, and init_db. Suite grew from 198 to 341 passing tests. Updates docs/details/testing.md with the new test suite layout and current results.
Copy file name to clipboardExpand all lines: docs/details/testing.md
+41-14Lines changed: 41 additions & 14 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -11,7 +11,8 @@ This project uses **pytest** for unit and integration testing.
11
11
3.[Install Development Dependencies](#-install-development-dependencies)
12
12
4.[Running the Test Suite](#-running-the-test-suite)
13
13
5.[Understanding the Results](#-understanding-the-results)
14
-
6.[Troubleshooting](#-troubleshooting)
14
+
6.[Test Suite Layout](#-test-suite-layout)
15
+
7.[Troubleshooting](#-troubleshooting)
15
16
16
17
---
17
18
@@ -71,7 +72,7 @@ A template is provided as `.env.example`.
71
72
72
73
This includes any required database connection strings, email credentials, API keys, and (optionally) Reddit API credentials.
73
74
74
-
> **📝 Note:**Reddit-related tests require valid Reddit API credentials. If these values are left blank, the Reddit test(s) are expected to fail.
75
+
> **📝 Note:**The test suite runs fully **offline** with mocked dependencies (see [Test Suite Layout](#-test-suite-layout)) — no credentials are required for `pytest`. The values above are still needed to run the application itself (e.g. the startup bootstrap and live pipelines).
75
76
76
77
---
77
78
@@ -119,8 +120,7 @@ pytest -v
119
120
A typical test run will produce output similar to:
120
121
121
122
```text
122
-
107 passed
123
-
15 skipped
123
+
341 passed, 15 skipped, 1 warning in 6.29s
124
124
```
125
125
126
126
### ✅ Passed
@@ -138,22 +138,49 @@ For example:
138
138
- RSS validation tests skip sources that are implemented as website scrapers.
139
139
- Website scraping tests skip sources that are implemented using RSS feeds.
140
140
141
+
Currently 15 tests skip: 11 RSS-source checks and 4 website-source checks in `test_collectors.py`.
142
+
141
143
This confirms that the correct collector is configured for each source rather than indicating a problem.
142
144
143
145
The deduplication suite also includes URL canonicalization checks that verify tracking parameters are removed and host matching is parsed safely rather than relying on substring checks.
144
146
145
147
### ❌ Failed
146
148
147
-
A failed test indicates that the implementation does not currently match the expected behaviour or that an optional external dependency has not been configured.
149
+
A failed test indicates that the implementation does not currently match the expected behaviour.
148
150
149
-
The project supports collecting voucher information from Reddit, which requires Reddit API credentials. These credentials are intentionally **not** included in the repository.
151
+
The suite runs entirely **offline** — no live database, network requests, or third-party APIs (AI providers, email/Resend, Reddit) are touched. Every external dependency is mocked (for example `polite_get`, `AsyncGroq`, `genai.Client`, and `resend.Emails.send`), and database access is simulated with fake async sessions that route on the SQL statement text.
150
152
151
-
If Reddit API credentials are not configured in your `.env` file, the Reddit-related test will fail. This is expected behaviour and does not indicate an issue with the rest of the application.
153
+
This means no extra credentials or services are required to run the tests, and a failure points to a genuine regression in the code (or a broken test) rather than a missing configuration value.
152
154
153
-
To run the complete test suite successfully, populate the Reddit configuration values in `.env` with valid API credentials obtained from Reddit's developer portal.
155
+
---
154
156
155
-
Without Reddit credentials, all other tests should still pass successfully.
157
+
## 🗂️ Test Suite Layout
158
+
159
+
The suite is organised by module — each file targets one service, provider, or component. All tests are async-friendly (pytest-asyncio) and hermetic.
160
+
161
+
| Test file (tests) | Module under test | Highlights |
|`test_notification_outbox.py` (8) |`voucherbot/services/email/notifications.py`| Idempotency keys, outbox staging, delivery + `is_notified` update, retry/`FAILED` at max attempts, skip-when-unconfigured |
180
+
181
+
**Conventions:** HTTP providers patch `polite_get` with an `AsyncMock` returning a hand-built `httpx.Response`; AI providers patch the client factories and rate-budget internals; email patches `resend.Emails.send`. Modules reading a global `settings` object get it patched with a `SimpleNamespace(...)` helper, while `test_settings.py` builds fresh `Settings` instances with `_env_file=None` and an autouse fixture clearing the environment. DB-bound functions use fake async sessions that route on `str(statement)` so unexpected SQL fails loudly.
156
182
183
+
---
157
184
158
185
## 🧪 Testing the AI Voucher Parser End-to-End
159
186
@@ -168,7 +195,7 @@ IS_TEST=true
168
195
IS_PROD=false
169
196
```
170
197
171
-
-`IS_TEST=true` — seeds a `website:local_test` source pointing at `http://localhost:35926/` (see `voucherbot/database/bootstrap.py:928-945`)
198
+
-`IS_TEST=true` — seeds a `website:local_test` source pointing at `http://localhost:35926/` (see `voucherbot/database/bootstrap.py:967-985`)
172
199
-`IS_PROD=false` — the app creates tables and runs bootstrap on startup
173
200
174
201
### 2. Start the Local Test Server
@@ -192,7 +219,7 @@ The server listens on `http://localhost:35926/`.
192
219
193
220
### 3. How the Scraper Works
194
221
195
-
The test source is defined at **`voucherbot/database/bootstrap.py:928-945`**:
222
+
The test source is defined at **`voucherbot/database/bootstrap.py:967-985`** (`_test_source`):
196
223
197
224
```python
198
225
"config": {
@@ -206,7 +233,7 @@ The test source is defined at **`voucherbot/database/bootstrap.py:928-945`**:
206
233
}
207
234
```
208
235
209
-
The `WebsiteCollector` (`voucherbot/providers/website/collector.py:37-41`) reads these selectors and scrapes the page using BeautifulSoup.
236
+
The `WebsiteCollector` (`voucherbot/providers/website/collector.py:28-41`) reads these selectors and scrapes the page using BeautifulSoup.
210
237
211
238
The `index.html` at `D:\components\index.html` contains `.item` divs with `<h2>` titles — this structure matches the default selectors. **To test different content, edit the HTML or the selectors.**
212
239
@@ -260,8 +287,8 @@ To change how the test page is parsed, edit:
0 commit comments