Files
2025-11-29 18:28:07 +08:00

861 lines
20 KiB
Markdown

---
description: Generate comprehensive tests for Sinatra routes, middleware, and helpers using RSpec or Minitest
---
# Sinatra Test Command
Generates comprehensive test suites for Sinatra applications including route tests, middleware tests, helper tests, and integration tests using RSpec or Minitest.
## Arguments
- **$1: test-type** (optional) - Type of tests to generate: `routes`, `middleware`, `helpers`, or `all` (default: `all`)
- **$2: framework** (optional) - Testing framework: `rspec` or `minitest` (default: `rspec`)
## Usage Examples
```bash
# Generate all tests using RSpec
/sinatra-test
# Generate only route tests with RSpec
/sinatra-test routes
# Generate all tests using Minitest
/sinatra-test all minitest
# Generate middleware tests with Minitest
/sinatra-test middleware minitest
# Generate helper tests with RSpec
/sinatra-test helpers rspec
```
## Workflow
### Step 1: Analyze Application Structure
**Discovery Phase:**
1. Identify application type (classic vs modular)
2. Locate controller files
3. Extract route definitions
4. Find middleware stack
5. Identify helper methods
6. Check existing test structure
7. Detect testing framework if already configured
**Files to Analyze:**
```ruby
# Controllers
app/controllers/**/*.rb
app.rb (classic style)
# Middleware
config.ru
config/**/*.rb
# Helpers
app/helpers/**/*.rb
helpers/ directory
# Existing tests
spec/**/*_spec.rb
test/**/*_test.rb
```
**Route Extraction:**
```ruby
# Parse routes from controller files
# Identify: HTTP method, path, parameters, conditions
# Example routes to extract:
get '/users' do
# Handler
end
get '/users/:id', :id => /\d+/ do
# Handler with constraint
end
post '/users', :provides => [:json] do
# Handler with content negotiation
end
```
### Step 2: Generate Test Structure (RSpec)
**Create spec_helper.rb if missing:**
```ruby
# spec/spec_helper.rb
ENV['RACK_ENV'] = 'test'
require 'simplecov'
SimpleCov.start do
add_filter '/spec/'
add_filter '/config/'
end
require_relative '../config/environment'
require 'rack/test'
require 'rspec'
require 'json'
# Database setup (if applicable)
if defined?(Sequel)
require 'database_cleaner/sequel'
RSpec.configure do |config|
config.before(:suite) do
DatabaseCleaner.strategy = :transaction
DatabaseCleaner.clean_with(:truncation)
end
config.around(:each) do |example|
DatabaseCleaner.cleaning do
example.run
end
end
end
end
RSpec.configure do |config|
config.include Rack::Test::Methods
config.expect_with :rspec do |expectations|
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
end
config.mock_with :rspec do |mocks|
mocks.verify_partial_doubles = true
end
config.shared_context_metadata_behavior = :apply_to_host_groups
config.filter_run_when_matching :focus
config.example_status_persistence_file_path = 'spec/examples.txt'
config.disable_monkey_patching!
config.warnings = true
config.order = :random
Kernel.srand config.seed
end
# Helper methods for all specs
module SpecHelpers
def json_response
JSON.parse(last_response.body)
end
def auth_header(token)
{ 'HTTP_AUTHORIZATION' => "Bearer #{token}" }
end
end
RSpec.configure do |config|
config.include SpecHelpers
end
```
**Create support files:**
```ruby
# spec/support/factory_helper.rb (if using factories)
require 'factory_bot'
RSpec.configure do |config|
config.include FactoryBot::Syntax::Methods
end
# spec/support/shared_examples.rb
RSpec.shared_examples 'authenticated endpoint' do
it 'returns 401 without authentication' do
send(http_method, path)
expect(last_response.status).to eq(401)
end
end
RSpec.shared_examples 'json endpoint' do
it 'returns JSON content type' do
send(http_method, path, valid_params)
expect(last_response.content_type).to include('application/json')
end
end
```
### Step 3: Generate Route Tests
**For each route, generate comprehensive tests:**
```ruby
# spec/controllers/users_controller_spec.rb
require_relative '../spec_helper'
RSpec.describe UsersController do
def app
UsersController
end
describe 'GET /users' do
context 'with no users' do
it 'returns empty array' do
get '/users'
expect(last_response).to be_ok
expect(json_response).to eq([])
end
end
context 'with existing users' do
let!(:users) { create_list(:user, 3) }
it 'returns all users' do
get '/users'
expect(last_response).to be_ok
expect(json_response.length).to eq(3)
end
it 'includes user attributes' do
get '/users'
user_data = json_response.first
expect(user_data).to have_key('id')
expect(user_data).to have_key('name')
expect(user_data).to have_key('email')
end
end
context 'with pagination' do
let!(:users) { create_list(:user, 25) }
it 'respects page parameter' do
get '/users?page=2&per_page=10'
expect(json_response.length).to eq(10)
end
it 'includes pagination metadata' do
get '/users?page=1&per_page=10'
expect(json_response['meta']).to include(
'total' => 25,
'page' => 1,
'per_page' => 10
)
end
end
context 'with filtering' do
let!(:active_user) { create(:user, active: true) }
let!(:inactive_user) { create(:user, active: false) }
it 'filters by active status' do
get '/users?active=true'
expect(json_response.length).to eq(1)
expect(json_response.first['id']).to eq(active_user.id)
end
end
end
describe 'GET /users/:id' do
let(:user) { create(:user) }
context 'when user exists' do
it 'returns user details' do
get "/users/#{user.id}"
expect(last_response).to be_ok
expect(json_response['id']).to eq(user.id)
end
it 'includes all user attributes' do
get "/users/#{user.id}"
expect(json_response).to include(
'id' => user.id,
'name' => user.name,
'email' => user.email
)
end
end
context 'when user does not exist' do
it 'returns 404' do
get '/users/99999'
expect(last_response.status).to eq(404)
end
it 'returns error message' do
get '/users/99999'
expect(json_response).to include('error')
end
end
context 'with invalid id format' do
it 'returns 404' do
get '/users/invalid'
expect(last_response.status).to eq(404)
end
end
end
describe 'POST /users' do
let(:valid_attributes) do
{
name: 'John Doe',
email: 'john@example.com',
password: 'SecurePass123'
}
end
context 'with valid attributes' do
it 'creates a new user' do
expect {
post '/users', valid_attributes.to_json,
'CONTENT_TYPE' => 'application/json'
}.to change(User, :count).by(1)
end
it 'returns 201 status' do
post '/users', valid_attributes.to_json,
'CONTENT_TYPE' => 'application/json'
expect(last_response.status).to eq(201)
end
it 'returns created user' do
post '/users', valid_attributes.to_json,
'CONTENT_TYPE' => 'application/json'
expect(json_response).to include(
'name' => 'John Doe',
'email' => 'john@example.com'
)
end
it 'does not return password' do
post '/users', valid_attributes.to_json,
'CONTENT_TYPE' => 'application/json'
expect(json_response).not_to have_key('password')
end
end
context 'with invalid attributes' do
it 'returns 422 status' do
post '/users', { name: '' }.to_json,
'CONTENT_TYPE' => 'application/json'
expect(last_response.status).to eq(422)
end
it 'returns validation errors' do
post '/users', { name: '' }.to_json,
'CONTENT_TYPE' => 'application/json'
expect(json_response).to have_key('errors')
end
it 'does not create user' do
expect {
post '/users', { name: '' }.to_json,
'CONTENT_TYPE' => 'application/json'
}.not_to change(User, :count)
end
end
context 'with duplicate email' do
let!(:existing_user) { create(:user, email: 'john@example.com') }
it 'returns 422 status' do
post '/users', valid_attributes.to_json,
'CONTENT_TYPE' => 'application/json'
expect(last_response.status).to eq(422)
end
it 'returns uniqueness error' do
post '/users', valid_attributes.to_json,
'CONTENT_TYPE' => 'application/json'
expect(json_response['errors']).to include('email')
end
end
end
describe 'PUT /users/:id' do
let(:user) { create(:user) }
let(:update_attributes) { { name: 'Updated Name' } }
context 'when user exists' do
it 'updates user attributes' do
put "/users/#{user.id}", update_attributes.to_json,
'CONTENT_TYPE' => 'application/json'
user.reload
expect(user.name).to eq('Updated Name')
end
it 'returns 200 status' do
put "/users/#{user.id}", update_attributes.to_json,
'CONTENT_TYPE' => 'application/json'
expect(last_response).to be_ok
end
it 'returns updated user' do
put "/users/#{user.id}", update_attributes.to_json,
'CONTENT_TYPE' => 'application/json'
expect(json_response['name']).to eq('Updated Name')
end
end
context 'with invalid attributes' do
it 'returns 422 status' do
put "/users/#{user.id}", { email: 'invalid' }.to_json,
'CONTENT_TYPE' => 'application/json'
expect(last_response.status).to eq(422)
end
it 'does not update user' do
original_email = user.email
put "/users/#{user.id}", { email: 'invalid' }.to_json,
'CONTENT_TYPE' => 'application/json'
user.reload
expect(user.email).to eq(original_email)
end
end
context 'when user does not exist' do
it 'returns 404' do
put '/users/99999', update_attributes.to_json,
'CONTENT_TYPE' => 'application/json'
expect(last_response.status).to eq(404)
end
end
end
describe 'DELETE /users/:id' do
let!(:user) { create(:user) }
context 'when user exists' do
it 'deletes the user' do
expect {
delete "/users/#{user.id}"
}.to change(User, :count).by(-1)
end
it 'returns 204 status' do
delete "/users/#{user.id}"
expect(last_response.status).to eq(204)
end
it 'returns empty body' do
delete "/users/#{user.id}"
expect(last_response.body).to be_empty
end
end
context 'when user does not exist' do
it 'returns 404' do
delete '/users/99999'
expect(last_response.status).to eq(404)
end
end
end
# Authentication tests
describe 'authentication' do
let(:protected_path) { '/users' }
let(:http_method) { :get }
let(:path) { protected_path }
it_behaves_like 'authenticated endpoint'
end
# Content negotiation tests
describe 'content negotiation' do
let(:user) { create(:user) }
context 'with Accept: application/json' do
it 'returns JSON' do
get "/users/#{user.id}", {}, { 'HTTP_ACCEPT' => 'application/json' }
expect(last_response.content_type).to include('application/json')
end
end
context 'with Accept: application/xml' do
it 'returns XML' do
get "/users/#{user.id}", {}, { 'HTTP_ACCEPT' => 'application/xml' }
expect(last_response.content_type).to include('application/xml')
end
end
end
end
```
### Step 4: Generate Middleware Tests
```ruby
# spec/middleware/custom_middleware_spec.rb
require_relative '../spec_helper'
RSpec.describe CustomMiddleware do
let(:app) { ->(env) { [200, {}, ['OK']] } }
let(:middleware) { CustomMiddleware.new(app) }
let(:request) { Rack::MockRequest.new(middleware) }
describe 'request processing' do
it 'passes request to next middleware' do
response = request.get('/')
expect(response.status).to eq(200)
end
it 'adds custom header to response' do
response = request.get('/')
expect(response.headers['X-Custom-Header']).to eq('value')
end
it 'modifies request environment' do
env = {}
middleware.call(env)
expect(env['custom.key']).to be_present
end
end
describe 'error handling' do
let(:app) { ->(env) { raise StandardError, 'Error' } }
it 'catches errors from downstream' do
response = request.get('/')
expect(response.status).to eq(500)
end
it 'logs error' do
expect { request.get('/') }.to change { error_log.size }.by(1)
end
end
describe 'configuration' do
let(:middleware) { CustomMiddleware.new(app, option: 'value') }
it 'accepts configuration options' do
expect(middleware.options[:option]).to eq('value')
end
it 'applies configuration to behavior' do
response = request.get('/')
expect(response.headers['X-Option']).to eq('value')
end
end
end
```
### Step 5: Generate Helper Tests
```ruby
# spec/helpers/application_helpers_spec.rb
require_relative '../spec_helper'
RSpec.describe ApplicationHelpers do
let(:dummy_class) do
Class.new do
include ApplicationHelpers
# Mock request/session for helper context
def request
@request ||= Struct.new(:path_info).new('/test')
end
def session
@session ||= {}
end
end
end
let(:helpers) { dummy_class.new }
describe '#current_user' do
context 'when user is logged in' do
before do
helpers.session[:user_id] = 1
allow(User).to receive(:find).with(1).and_return(
double('User', id: 1, name: 'John')
)
end
it 'returns current user' do
expect(helpers.current_user).to be_present
expect(helpers.current_user.id).to eq(1)
end
it 'memoizes user' do
expect(User).to receive(:find).once
helpers.current_user
helpers.current_user
end
end
context 'when user is not logged in' do
it 'returns nil' do
expect(helpers.current_user).to be_nil
end
end
end
describe '#logged_in?' do
it 'returns true when current_user exists' do
allow(helpers).to receive(:current_user).and_return(double('User'))
expect(helpers.logged_in?).to be true
end
it 'returns false when current_user is nil' do
allow(helpers).to receive(:current_user).and_return(nil)
expect(helpers.logged_in?).to be false
end
end
describe '#format_date' do
let(:date) { Time.new(2024, 1, 15, 10, 30, 0) }
it 'formats date with default format' do
expect(helpers.format_date(date)).to eq('2024-01-15')
end
it 'accepts custom format' do
expect(helpers.format_date(date, '%m/%d/%Y')).to eq('01/15/2024')
end
it 'handles nil date' do
expect(helpers.format_date(nil)).to eq('')
end
end
describe '#truncate' do
let(:long_text) { 'This is a very long text that should be truncated' }
it 'truncates text to specified length' do
expect(helpers.truncate(long_text, 20)).to eq('This is a very long...')
end
it 'does not truncate short text' do
short_text = 'Short'
expect(helpers.truncate(short_text, 20)).to eq('Short')
end
it 'accepts custom omission' do
expect(helpers.truncate(long_text, 20, omission: '…')).to include('…')
end
end
end
```
### Step 6: Generate Minitest Tests (Alternative)
**If framework is Minitest:**
```ruby
# test/test_helper.rb
ENV['RACK_ENV'] = 'test'
require 'simplecov'
SimpleCov.start
require_relative '../config/environment'
require 'minitest/autorun'
require 'minitest/spec'
require 'rack/test'
class Minitest::Spec
include Rack::Test::Methods
def json_response
JSON.parse(last_response.body)
end
end
# test/controllers/users_controller_test.rb
require_relative '../test_helper'
describe UsersController do
def app
UsersController
end
describe 'GET /users' do
it 'returns success' do
get '/users'
assert last_response.ok?
end
it 'returns JSON' do
get '/users'
assert_includes last_response.content_type, 'application/json'
end
describe 'with existing users' do
before do
@users = 3.times.map { User.create(name: 'Test') }
end
it 'returns all users' do
get '/users'
assert_equal 3, json_response.length
end
end
end
describe 'POST /users' do
let(:valid_params) { { name: 'John', email: 'john@example.com' } }
it 'creates user' do
assert_difference 'User.count', 1 do
post '/users', valid_params.to_json,
'CONTENT_TYPE' => 'application/json'
end
end
it 'returns 201' do
post '/users', valid_params.to_json,
'CONTENT_TYPE' => 'application/json'
assert_equal 201, last_response.status
end
end
end
```
### Step 7: Generate Integration Tests
```ruby
# spec/integration/user_registration_spec.rb
require_relative '../spec_helper'
RSpec.describe 'User Registration Flow' do
def app
Sinatra::Application
end
describe 'complete registration process' do
let(:user_params) do
{
name: 'John Doe',
email: 'john@example.com',
password: 'SecurePass123'
}
end
it 'allows new user to register and log in' do
# Step 1: Register
post '/register', user_params.to_json,
'CONTENT_TYPE' => 'application/json'
expect(last_response.status).to eq(201)
user_id = json_response['id']
# Step 2: Verify email confirmation sent
expect(EmailService.last_email[:to]).to eq('john@example.com')
# Step 3: Confirm email
token = EmailService.last_email[:token]
get "/confirm/#{token}"
expect(last_response.status).to eq(200)
# Step 4: Log in
post '/login', { email: 'john@example.com', password: 'SecurePass123' }.to_json,
'CONTENT_TYPE' => 'application/json'
expect(last_response.status).to eq(200)
expect(json_response).to have_key('token')
# Step 5: Access protected resource
token = json_response['token']
get '/profile', {}, auth_header(token)
expect(last_response).to be_ok
expect(json_response['id']).to eq(user_id)
end
end
end
```
### Step 8: Create Test Documentation
**Generate test README:**
```markdown
# Test Suite Documentation
## Running Tests
### All Tests
```bash
bundle exec rspec
```
### Specific Test File
```bash
bundle exec rspec spec/controllers/users_controller_spec.rb
```
### By Tag
```bash
bundle exec rspec --tag focus
```
## Test Structure
- `spec/controllers/` - Route and controller tests
- `spec/middleware/` - Middleware tests
- `spec/helpers/` - Helper method tests
- `spec/models/` - Model tests (if applicable)
- `spec/integration/` - End-to-end integration tests
- `spec/support/` - Shared examples and helpers
## Coverage
Run tests with coverage report:
```bash
COVERAGE=true bundle exec rspec
```
View coverage report:
```bash
open coverage/index.html
```
## Testing Patterns
### Route Testing
- Test successful responses
- Test error cases (404, 422, 500)
- Test authentication/authorization
- Test parameter validation
- Test content negotiation
### Helper Testing
- Test with various inputs
- Test edge cases
- Test nil handling
- Mock dependencies
### Integration Testing
- Test complete user flows
- Test interactions between components
- Test external service integration
```
## Output
**Generated files report:**
```
Test Generation Complete!
Framework: RSpec
Test Type: all
Generated Files:
✓ spec/spec_helper.rb
✓ spec/support/factory_helper.rb
✓ spec/support/shared_examples.rb
✓ spec/controllers/users_controller_spec.rb (45 examples)
✓ spec/controllers/posts_controller_spec.rb (38 examples)
✓ spec/middleware/custom_middleware_spec.rb (12 examples)
✓ spec/helpers/application_helpers_spec.rb (15 examples)
✓ spec/integration/user_registration_spec.rb (5 examples)
✓ TEST_README.md
Total Examples: 115
Coverage Target: 90%
Run tests: bundle exec rspec
```
## Error Handling
- Handle applications without routes gracefully
- Skip already existing test files (or offer to overwrite)
- Detect testing framework from Gemfile
- Warn if test dependencies missing
- Handle parse errors in application files