Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,7 @@
!/app/assets/builds/.keep

db/production*

# Ignore VCR cassettes (recorded HTTP interactions)
test/fixtures/vcr_cassettes/*
!test/fixtures/vcr_cassettes/.gitkeep
6 changes: 6 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ group :development, :test do
gem "mocha"
end

# Gems used exclusively in the test suite
group :test do
gem "vcr", "~> 6.2"
gem "webmock", "~> 3.23"
end

group :development do
gem "web-console"
end
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,8 @@ User.find_by!(email_address: "me@example.com").update!(admin: false)
## Authors

- [@bradly](https://www.github.com/bradly)

## Running the test suite

See `TESTING.md` for details on recording and replaying VCR cassettes when
testing the recipe parser.
70 changes: 70 additions & 0 deletions TESTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Testing the Recipe Parsing Logic

This project relies on **VCR** to record and replay real HTTP responses in the
test suite. Using VCR keeps the tests **fast**, **repeatable**, and **offline
friendly** while still exercising the full `RecipeExtractor` stack.

## Directory layout

- `test/fixtures/vcr_cassettes/` – YAML cassettes (recorded HTTP
interactions). The entire directory is excluded from Git via `.gitignore` –
only a `.gitkeep` placeholder is tracked so the folder exists on fresh
clones.

**Why are cassettes ignored?** Personal recipe URLs may expose private
browsing history or inadvertently redistribute copyrighted HTML. Keeping
them local avoids licensing headaches while still giving every developer a
convenient harness.

## Recording a new cassette

1. **Pick a recipe URL** that you want to add as a regression test.
2. Run the following rake task:

```bash
# Uses the URL itself (parameter-ized) for the cassette name
bin/rails parsing:record_vcr[https://www.allrecipes.com/recipe/24074/alysias-basic-meat-lasagna]

# …or provide a custom cassette name
bin/rails parsing:record_vcr[https://www.bbcgoodfood.com/recipes/lemon-drizzle,citrus_cake]
```

The task will:

- Boot Rails
- Configure VCR in **record-all** mode
- Fetch the URL through `RecipeExtractor`
- Write the cassette to `test/fixtures/vcr_cassettes/<name>.yml`

## Adding an assertion

Open (or create) a test file under `test/parsing/` and wrap your extraction
code in `VCR.use_cassette`:

```ruby
VCR.use_cassette('citrus_cake') do
extractor = RecipeExtractor.new('https://www.bbcgoodfood.com/recipes/lemon-drizzle')
data = extractor.data

assert_equal 'Lemon drizzle cake', data[:name]
end
```

When the cassette is missing the test will be **skipped** (helpful for CI
environments without network access). Run the rake task locally to create it
and the test will automatically start running.

## Test run cheatsheet

```bash
# Run the full suite
bin/rails test

# Run only the parsing tests
bin/rails test test/parsing

# Re-record a single cassette
bin/rails parsing:record_vcr[<url>,<cassette_name>]
```

Happy parsing! 🥣
54 changes: 54 additions & 0 deletions lib/tasks/vcr.rake
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# frozen_string_literal: true

# Rake tasks for working with VCR cassettes used by the recipe parsing tests.

namespace :parsing do
desc <<~DESC
Record a VCR cassette for the given recipe URL.

Examples:
# Generate a cassette using an automatic, URL-based filename
bin/rails parsing:record_vcr[https://example.com/recipe]

# Specify a custom cassette name (without file extension)
bin/rails parsing:record_vcr[https://example.com/recipe,my_custom_name]
DESC
task :record_vcr, %i[url cassette_name] => :environment do |_t, args|
url = args[:url]

unless url.present?
warn 'ERROR: URL argument is required. Example: bin/rails parsing:record_vcr[https://example.com/recipe]'
exit 1
end

cassette_name = args[:cassette_name].presence || url.parameterize.presence || 'recipe'

cassette_dir = Rails.root.join('test', 'fixtures', 'vcr_cassettes')
FileUtils.mkdir_p(cassette_dir)

require 'vcr'
require 'webmock'

VCR.configure do |config|
config.cassette_library_dir = cassette_dir
config.hook_into :webmock
config.ignore_localhost = true
config.default_cassette_options = { record: :all }
end

puts "Recording VCR cassette '#{cassette_name}.yml' for #{url}..."

VCR.use_cassette(cassette_name) do
extractor = RecipeExtractor.new(url)
begin
extractor.data
puts 'OK: fetch complete.'
rescue StandardError => e
warn "ERROR: Failed to fetch recipe - #{e.class}: #{e.message}"
raise
end
end

puts "Cassette saved to #{cassette_dir.join(cassette_name + '.yml')}"
end
end
1 change: 1 addition & 0 deletions test/fixtures/vcr_cassettes/.gitkeep
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

28 changes: 28 additions & 0 deletions test/parsing/recipe_extractor_vcr_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
require 'test_helper'

# Test harness that verifies RecipeExtractor can parse a real-world recipe
# page using a VCR cassette. The cassette file itself is *not* committed
# to the repo; instead we skip the assertion unless a cassette has already
# been recorded locally.

class RecipeExtractorVCRTest < ActiveSupport::TestCase
CASSETTE_NAME = 'example_recipe'
TEST_URL = 'https://www.allrecipes.com/recipe/24074/alysias-basic-meat-lasagna'

test 'extract recipe data via recorded HTTP interaction' do
cassette_file = Rails.root.join('test', 'fixtures', 'vcr_cassettes', "#{CASSETTE_NAME}.yml")

# Skip in CI or first-run scenarios where the cassette does not yet exist.
skip "VCR cassette not present (run `bin/rails parsing:record_vcr[#{TEST_URL}]` to create it)" unless File.exist?(cassette_file)

VCR.use_cassette(CASSETTE_NAME) do
extractor = RecipeExtractor.new(TEST_URL)
data = extractor.data

# Minimal smoke tests – adjust / extend as needed when new cassettes are added.
assert data[:name].present?, 'recipe name should be present'
assert data[:ingredients].present?, 'ingredients should be present'
assert data[:instruction_sections].present?, 'instruction sections should be present'
end
end
end
24 changes: 24 additions & 0 deletions test/test_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,30 @@
require "rails/test_help"
require 'minitest/autorun'
require 'mocha/minitest'
# HTTP interaction recording / replay
require 'webmock/minitest'
require 'vcr'

# Configure VCR to store cassettes under `test/fixtures/vcr_cassettes`
VCR.configure do |config|
# Store cassettes in our dedicated fixtures directory
config.cassette_library_dir = Rails.root.join('test', 'fixtures', 'vcr_cassettes')

# Hook VCR into WebMock so that HTTP calls are intercepted
config.hook_into :webmock

# Allow localhost connections (capybara, rails server, etc.)
config.ignore_localhost = true

# Filter environment specific data (e.g. authorization headers)
config.filter_sensitive_data('<AUTH_TOKEN>') { ENV['AUTH_TOKEN'] }

# When a cassette is missing, record a new one in :new_episodes mode by default.
config.default_cassette_options = { record: :new_episodes }

# Enable more readable names for generated cassette files
config.configure_rspec_metadata! if config.respond_to?(:configure_rspec_metadata!)
end

module ActiveSupport
class TestCase
Expand Down