Testing¶
qBitrr testing strategies and guidelines. Automated coverage exists for config reload policy, package layout, WebUI routes, and frontend units; live smoke against real qBit + Arr remains the integration checklist.
Current Testing Approach¶
Automated tests¶
Python (unittest):
python -m unittest discover -s tests -v
# Or a single module:
python -m unittest tests.test_config_reload_policy -v
Notable modules under tests/: config first-boot, live-reload characterization, config_reload_policy, Arr factory/startup, WebUI routes/reload, package layout.
WebUI (Vitest):
cd webui && npm test
# Coverage report (text + HTML under webui/coverage/; no thresholds enforced):
cd webui && npm run test:coverage
Manual / live smoke¶
Use manual testing against real services for end-to-end confidence:
Requirements: - qBittorrent instance (v4.3+ or v5.0+) - At least one Arr instance (Radarr, Sonarr, or Lidarr) - Test torrents with various states - Test media files for FFprobe validation
Test Environment Setup:
Use a dedicated config directory so qBitrr loads your test config:
# Option 1: Run from a directory that has your test config as config.toml
mkdir -p test-env/.config
cp config.example.toml test-env/.config/config.toml
# Edit test-env/.config/config.toml with test service URLs
cd test-env && qbitrr
# Option 2: Override the config/data path with an environment variable
cp config.example.toml /path/to/test-config/config.toml
# Edit /path/to/test-config/config.toml
export QBITRR_OVERRIDES_DATA_PATH=/path/to/test-config
qbitrr
There is no --config or --foreground CLI flag; qBitrr runs in the foreground by default when started from the command line.
Testing Checklist¶
When making changes, test these scenarios:
Core Functionality¶
- qBitrr starts successfully
- Connects to qBittorrent
- Connects to all configured Arr instances
- WebUI accessible at configured port
- Logs written to correct location
Torrent Processing¶
- Detects new torrents added by Arr
- Tracks torrent download progress
- Detects torrent completion
- Triggers import to Arr
- Updates torrent state in database
Health Monitoring¶
- Detects stalled torrents
- Marks torrents with ETA > MaxETA as stalled
- Handles failed trackers
- FFprobe validation (if enabled)
- Blacklists failed torrents
Seeding Management¶
- Continues seeding after import
- Tracks seed ratio and time
- Deletes torrents when seed goals met
- Respects tracker-specific rules (if configured)
Search Features¶
- Auto-search for missing content (if enabled)
- Re-search after blacklisting (if enabled)
- Search cooldown works correctly
- Search history recorded in database
Configuration¶
- Config file changes detected
- Environment variables override TOML
- Invalid config generates helpful errors
- Config validation works (e.g. on save in WebUI or at startup)
WebUI¶
- Dashboard loads correctly
- Processes page shows all Arr instances
- Logs page displays recent logs
- Arr-specific pages show torrents
- API endpoints return correct data
- API authentication works (if token set)
Docker Testing¶
# Build test image
docker build -t qbitrr:test .
# Run with test config
docker run -d \
--name qbitrr-test \
-p 6969:6969 \
-v $(pwd)/test-config.toml:/config/config.toml \
-v /path/to/downloads:/downloads \
qbitrr:test
# Check logs
docker logs -f qbitrr-test
# Clean up
docker stop qbitrr-test
docker rm qbitrr-test
Live smoke (compose)¶
Use the test-only stack in docker-compose.test.yml (linuxserver qBittorrent + Radarr + qBitrr built from this branch). Data lands under .compose-test/ (gitignored). Do not use this compose for production.
Host ports (to avoid clashing with local Arr/qBit installs):
| Service | Host | In-compose |
|---|---|---|
| qBittorrent WebUI | http://localhost:18080 | qbittorrent:8080 |
| Radarr | http://localhost:17878 | radarr:7878 |
| qBitrr WebUI | http://localhost:16969 | qbitrr:6969 |
Build¶
Checklist (finite; record pass/fail in the PR)¶
Record results in the PR description (or review notes). Do not add a permanent *_TEST*.md in the repo root.
- Cold start / first-boot (Phase A) — empty data dir generates
config.tomland exits cleanly (noNameError). - Configured start — WebUI up; qBit + Arr connected.
- Live:
Settings.AutoPauseResume— WebUI save changes pause/resume behavior without a full process restart. - Live: Arr LIVE key — e.g.
EntrySearch.SearchMissing; LIVE workers sync via_sync_loop_settings_from_config→_apply_arr_live_attrs_from_config(no full Arr respawn; supervisor may start/stop the search worker whenSearchMissingflips). - Live: FreeSpace — WebUI save; policy loop reflects the new threshold.
- Torrent path (optional fixtures) — detect / failed or recheck category handling if you can add a torrent.
RadarrArrspawn — after the per-type hierarchy, manager buildsRadarrArr(and Sonarr/Lidarr if configured).
Phase A — first-boot (empty config)¶
mkdir -p .compose-test/qbitrr-firstboot
docker compose -f docker-compose.test.yml run --rm --no-deps \
-v "$(pwd)/.compose-test/qbitrr-firstboot:/config" \
qbitrr
# Expect: exit code 0, message that config.toml was generated, file present under
# .compose-test/qbitrr-firstboot/config.toml, no NameError in logs.
Local equivalent (no Docker):
# Uses the same contract as tests/test_config_first_boot.py
python -m unittest tests.test_config_first_boot -v
Bring up qBit + Radarr¶
mkdir -p .compose-test/{qbittorrent,radarr,qbitrr,downloads,media}
docker compose -f docker-compose.test.yml up -d qbittorrent radarr
Bootstrap notes:
- qBittorrent — open http://localhost:18080. Username is
admin; the temporary password is printed indocker logs qbitrr-test-qbittorrent. Set a persistent password in the WebUI, set default save path to/downloads, and create categoryradarr-movies(save path/downloads/radarr-moviesis fine). - Radarr — open http://localhost:17878, complete the wizard (root folder
/movies). Add a qBittorrent download client pointing at hostqbittorrent, port8080, with categoryradarr-movies. Copy the API key from Settings → General. - qBitrr config — copy the generated
config.tomlinto.compose-test/qbitrr/(or run Phase A into that directory), then set at least:
[Settings]
CompletedDownloadFolder = "/downloads"
FreeSpaceFolder = "/downloads"
FreeSpace = "-1"
AutoPauseResume = true
[qBit]
Host = "qbittorrent"
Port = 8080
UserName = "admin"
Password = "<persistent qBit password>"
[Radarr-Movies]
Managed = true
URI = "http://radarr:7878"
APIKey = "<radarr api key>"
Category = "radarr-movies"
[Radarr-Movies.EntrySearch]
SearchMissing = false
Disable or leave unmanaged any other Arr sections that still say CHANGE_ME.
Generated configs often set WebUI.AuthDisabled = false and a random WebUI.Token. Use that token for API calls:
TOKEN=$(rg -oP '(?m)^Token = "\K[^"]+' .compose-test/qbitrr/config.toml)
curl -s -H "Authorization: Bearer $TOKEN" http://localhost:16969/api/processes
docker compose -f docker-compose.test.yml up -d qbitrr
docker logs -f qbitrr-test-qbitrr
# WebUI: http://localhost:16969/ui → /static/index.html
Confirming RadarrArr¶
With a configured stack:
# Factory wiring (works without live Arr credentials)
docker compose -f docker-compose.test.yml exec qbitrr \
python -c "from qBitrr.arss.factory import arr_class_for_section as f; print(f('Radarr-Movies').__name__)"
# Expect: RadarrArr
# Live spawn: Arr worker log should show the instance starting
docker logs qbitrr-test-qbitrr 2>&1 | grep -E 'Starting Arr instance: Radarr-Movies|Starting Radarr-Movies monitor|qBitrr\.Radarr-Movies'
Unit coverage of the same factory mapping: tests/test_arss_startup.py.
Live-reload checks (items 3–5)¶
Use the WebUI or POST /api/config with {"changes":{...}} and a Bearer token:
- Toggle Settings.AutoPauseResume — expect
reloadType: liveand WebUI.logLive settings changed (no worker restart): Settings.AutoPauseResume(same worker PIDs). - Toggle Radarr-Movies.EntrySearch.SearchMissing — expect
Applying live Arr config refresh for: Radarr-Moviesand Radarr-Movies.logApplied in-place config refresh. - Set Settings.FreeSpace / FreeSpaceFolder — expect live settings notice; policy loop reads effective FreeSpace on subsequent iterations.
Tear down¶
If Docker / images are unavailable¶
Run Phase A via tests/test_config_first_boot.py, run live-reload characterization tests under tests/, and exercise the checklist against any existing local qBit + Arr instances. Note in the PR which checklist rows could not be live-smoked.
Expanding automated coverage¶
Unit and characterization tests already live under tests/ (stdlib unittest) and webui/ (Vitest). Prefer adding focused tests next to the module under change rather than large E2E harnesses.
End-to-End / live smoke¶
Prefer the manual finite checklist under Live smoke (compose) with docker-compose.test.yml. Automated browser E2E is not required for the confidence-hardening smoke.
Example unittest pattern¶
# tests/test_config_reload_policy.py
import unittest
from qBitrr.config_reload_policy import classify_config_changes
class TestFailedCategoryRequiresFullRestart(unittest.TestCase):
def test_failed_category_is_full_restart(self):
plan = classify_config_changes({"Settings.FailedCategory": "failed-new"})
self.assertTrue(plan.needs_full_restart)
Performance Tests (optional)¶
# tests/performance/test_event_loop.py
def test_event_loop_with_many_torrents():
"""Ensure event loop completes in reasonable time with 100 torrents."""
torrents = generate_test_torrents(count=100)
start = time.time()
manager.process_torrents(torrents)
duration = time.time() - start
assert duration < 10.0, f"Event loop took {duration}s (expected < 10s)"
Test Data¶
Sample Configurations¶
Located in tests/fixtures/:
valid_config.toml- Valid configurationinvalid_config.toml- Invalid configuration (for error testing)minimal_config.toml- Minimal required fields
Mock Data¶
# tests/fixtures/torrents.py
SAMPLE_TORRENTS = {
'downloading': {
'hash': 'abc123',
'name': 'Test Movie 2024',
'progress': 0.5,
'eta': 1800,
'state': 'downloading'
},
'completed': {
'hash': 'def456',
'name': 'Another Movie 2024',
'progress': 1.0,
'eta': 0,
'state': 'uploading'
},
'stalled': {
'hash': 'ghi789',
'name': 'Stalled Movie',
'progress': 0.1,
'eta': 7200,
'state': 'stalledDL'
}
}
Debugging Tests¶
Enable Debug Logging¶
Run Single Test¶
# Python — one module or test method
python -m unittest tests.test_config_reload_policy -v
python -m unittest tests.test_arss_startup.TestArrFactory.test_arr_class_for_section -v
# WebUI Vitest
cd webui && npm test
Manual Test Scenarios¶
Scenario 1: Failed Download¶
Setup: 1. Add movie to Radarr 2. Radarr grabs torrent with no seeders
Expected Behavior: 1. qBitrr detects torrent 2. ETA exceeds MaximumETA after StallTimeout 3. Torrent marked as stalled 4. Torrent blacklisted in Radarr 5. New search triggered (if AutoReSearch enabled)
Scenario 2: Successful Import¶
Setup: 1. Add movie to Radarr 2. Radarr grabs popular torrent
Expected Behavior: 1. qBitrr tracks download progress 2. Download completes 3. FFprobe validates file (if enabled) 4. Import triggered in Radarr 5. Torrent continues seeding 6. Deleted when seed goals met
Scenario 3: Configuration Change¶
Setup: 1. qBitrr running 2. Edit config.toml (e.g. change LoopSleepTimer)
Expected Behavior: 1. qBitrr detects config change 2. Reloads configuration 3. Event loops restart with new interval 4. No data loss in database
Related Documentation¶
- Development Guide - Complete development setup
- Contributing - Contribution guidelines
- Code Style - Code formatting rules