Skip to content

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

docker compose -f docker-compose.test.yml 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.

  1. Cold start / first-boot (Phase A) — empty data dir generates config.toml and exits cleanly (no NameError).
  2. Configured start — WebUI up; qBit + Arr connected.
  3. Live: Settings.AutoPauseResume — WebUI save changes pause/resume behavior without a full process restart.
  4. 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 when SearchMissing flips).
  5. Live: FreeSpace — WebUI save; policy loop reflects the new threshold.
  6. Torrent path (optional fixtures) — detect / failed or recheck category handling if you can add a torrent.
  7. RadarrArr spawn — after the per-type hierarchy, manager builds RadarrArr (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:

  1. qBittorrent — open http://localhost:18080. Username is admin; the temporary password is printed in docker logs qbitrr-test-qbittorrent. Set a persistent password in the WebUI, set default save path to /downloads, and create category radarr-movies (save path /downloads/radarr-movies is fine).
  2. Radarr — open http://localhost:17878, complete the wizard (root folder /movies). Add a qBittorrent download client pointing at host qbittorrent, port 8080, with category radarr-movies. Copy the API key from Settings → General.
  3. qBitrr config — copy the generated config.toml into .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:

  1. Toggle Settings.AutoPauseResume — expect reloadType: live and WebUI.log Live settings changed (no worker restart): Settings.AutoPauseResume (same worker PIDs).
  2. Toggle Radarr-Movies.EntrySearch.SearchMissing — expect Applying live Arr config refresh for: Radarr-Movies and Radarr-Movies.log Applied in-place config refresh.
  3. Set Settings.FreeSpace / FreeSpaceFolder — expect live settings notice; policy loop reads effective FreeSpace on subsequent iterations.

Tear down

docker compose -f docker-compose.test.yml down
# Optional: wipe local state
# rm -rf .compose-test

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 configuration
  • invalid_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

import logging
logging.basicConfig(level=logging.DEBUG)

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