| name | sm-engine-test |
| description | Source Monitor engine test patterns and helpers. Use when writing or modifying tests for the Source Monitor engine, debugging test failures, setting up test isolation, or working with VCR/WebMock in this project. |
| allowed-tools | Read, Write, Edit, Bash, Glob, Grep |
Source Monitor Engine Tests
Quick Start
require "test_helper"
module SourceMonitor
class MyFeatureTest < ActiveSupport::TestCase
test "does something" do
source = create_source!(name: "Test", feed_url: "https://example.com/feed.xml")
assert source.persisted?
end
end
end
Critical: Parallel Worker Caveat
Single test files MUST use PARALLEL_WORKERS=1 due to a PG fork segfault bug (Ruby 3.4+ / pg 1.6+ / fork-based parallelism).
PARALLEL_WORKERS=1 bin/rails test test/models/source_monitor/source_test.rb
bin/rails test
COVERAGE=1 PARALLEL_WORKERS=1 bin/rails test
Test Helpers
All helpers are available in every test via ActiveSupport::TestCase.
| Helper | Purpose | Defined In |
|---|
create_source!(attrs) | Factory for Source records | test/test_helper.rb |
with_queue_adapter(adapter) | Temporarily swap ActiveJob adapter | test/test_helper.rb |
with_inline_jobs { } | Execute jobs inline in a block | test/test_prof.rb |
setup_once { } | TestProf before_all for expensive setup | test/test_prof.rb |
clean_source_monitor_tables! | Delete all engine records (FK-safe order) | test/test_helper.rb |
SourceMonitor.reset_configuration! | Reset config to defaults (runs in setup) | automatic |
Configuration Reset
Every test automatically calls SourceMonitor.reset_configuration! in setup. If you modify config in a test, it will be reset before the next test.
test "custom config behavior" do
SourceMonitor.configure do |config|
config.fetching.min_interval_minutes = 10
end
assert_equal 10, SourceMonitor.config.fetching.min_interval_minutes
end
For tests that explicitly need teardown:
setup do
SourceMonitor.reset_configuration!
end
teardown do
SourceMonitor.reset_configuration!
end
Factory Helper: create_source!
Creates a SourceMonitor::Source record, bypassing validations for speed.
source = create_source!
source = create_source!(
name: "Custom Source",
feed_url: "https://custom.example.com/feed.xml",
active: false,
adaptive_fetching_enabled: false,
fetch_interval_minutes: 120
)
Important: create_source! uses save!(validate: false) so it skips model validations. This is intentional for test speed but means you can create records that would fail validation.
WebMock Setup
WebMock is configured globally to block all external HTTP requests except localhost.
stub_request(:get, "https://example.com/feed.xml")
.to_return(
status: 200,
body: File.read(file_fixture("feeds/rss_sample.xml")),
headers: { "Content-Type" => "application/rss+xml" }
)
stub_request(:get, url)
.with(headers: { "If-None-Match" => '"etag123"' })
.to_return(status: 304, headers: { "ETag" => '"etag123"' })
stub_request(:get, url).to_raise(Faraday::TimeoutError.new("execution expired"))
stub_request(:get, url).to_raise(Faraday::ConnectionFailed.new("connection refused"))
VCR Cassettes
VCR is configured to hook into WebMock. Cassettes are stored in test/vcr_cassettes/.
test "fetches feed" do
source = create_source!(feed_url: "https://www.ruby-lang.org/en/feeds/news.rss")
VCR.use_cassette("source_monitor/fetching/rss_success") do
result = FeedFetcher.new(source: source, jitter: ->(_) { 0 }).call
assert_equal :fetched, result.status
end
end
Naming convention: source_monitor/<module>/<descriptor> (e.g., source_monitor/fetching/rss_success).
Test Isolation
Scope Queries to Specific Records
Parallel tests share the database. Always scope assertions to records you created:
assert_equal 3, SourceMonitor::Item.where(source: source).count
assert_equal 3, SourceMonitor::Item.count
Use Unique Feed URLs
create_source! generates random feed URLs by default. When you need a specific URL, make it unique:
source = create_source!(feed_url: "https://example.com/feed-#{SecureRandom.hex(4)}.xml")
Clean Tables When Needed
For tests that need a blank-slate database:
setup do
clean_source_monitor_tables!
end
The cleanup order respects foreign keys: LogEntry > ScrapeLog > FetchLog > HealthCheckLog > ItemContent > Item > Source.
Job Testing
test "enqueues fetch job" do
source = create_source!
assert_enqueued_with(job: SourceMonitor::FetchSourceJob, args: [source]) do
source.enqueue_fetch!
end
end
test "performs fetch end-to-end" do
with_inline_jobs do
end
end
test "with async adapter" do
with_queue_adapter(:async) do
end
end
Test Types
| Type | Base Class | Location |
|---|
| Model | ActiveSupport::TestCase | test/models/source_monitor/ |
| Controller | ActionDispatch::IntegrationTest | test/controllers/source_monitor/ |
| Library | ActiveSupport::TestCase | test/lib/source_monitor/ |
| Generator | Rails::Generators::TestCase | test/lib/generators/ |
Common Patterns
Testing with Notifications
test "emits fetch finish event" do
payloads = []
ActiveSupport::Notifications.subscribed(
->(_name, _start, _finish, _id, payload) { payloads << payload },
"source_monitor.fetch.finish"
) do
FeedFetcher.new(source: source, jitter: ->(_) { 0 }).call
end
assert payloads.last[:success]
end
Testing with Time Travel
test "schedules next fetch" do
travel_to Time.zone.parse("2024-01-01 10:00:00 UTC")
assert_equal Time.current + 45.minutes, source.next_fetch_at
ensure
travel_back
end
Private Method Testing (via send)
test "jitter_offset returns zero for zero interval" do
fetcher = FeedFetcher.new(source: source)
assert_equal 0, fetcher.send(:jitter_offset, 0)
end
Running Tests
bin/rails test
PARALLEL_WORKERS=1 bin/rails test test/models/source_monitor/source_test.rb
PARALLEL_WORKERS=1 bin/rails test test/models/source_monitor/source_test.rb --verbose
PARALLEL_WORKERS=1 bin/rails test test/models/source_monitor/source_test.rb -n "test_is_valid_with_minimal_attributes"
COVERAGE=1 PARALLEL_WORKERS=1 bin/rails test
SAMPLE=10 bin/rails test
File Fixtures
Test fixtures (feed XML files, etc.) are in test/fixtures/:
body = File.read(file_fixture("feeds/rss_sample.xml"))
Testing Checklist
References