--- 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