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
CI Python version¶
GitHub Actions selects the latest available Python 3.12 patch release for backend checks and packaged builds. Using the minor version keeps the runtime compatible with updated hosted runner images.