Tools: Mock It ’Til You Make It: Testing External APIs Without Losing Your Mind

Tools: Mock It ’Til You Make It: Testing External APIs Without Losing Your Mind

Source: Dev.to

The Scenario ## The Tools ## Step 1: The Setup ## Step 2: Writing the Test ## Step 3: The Magic (First Run vs. Second Run) ## ⚠️ Crucial: Don't Leak Your API Keys! ## When to "Re-Record" ## Summary You’ve just finished integrating the Stripe API (or OpenAI, or Twilio) into your Rails app. It works perfectly in development. You push it, run your test suite, and... wait. And then BOOM. Your tests fail because: Stop making real HTTP requests in your test suite. It’s slow, it’s flaky, and it’s dangerous. Today, we’re going to solve this using the dynamic duo of the Ruby world: WebMock and VCR. Add these to your Gemfile in the test group: Next, configure VCR in your spec/rails_helper.rb (or test/test_helper.rb). Let's pretend we have a service that fetches a user's GitHub profile. Now, let's test it. We use the vcr: true metadata tag (if you configured configure_rspec_metadata!) or wrap the block manually. Run 1: When you run rspec the first time, VCR sees that you don't have a cassette recorded yet. It allows the real HTTP request to go through to GitHub, captures the response, and saves it to spec/vcr_cassettes/github/user_info.yml. Run 2: You run rspec again. VCR intercepts the request. It does not hit GitHub. It reads the YAML file. If you are testing an API that requires a Secret Key (like OpenAI or Stripe), VCR will record your real API key into the YAML file. If you commit that file to GitHub, bots will scrape it, and you will have a bad day. Fix this by filtering sensitive data in your config: Now, if you open your generated YAML cassette, you will see Authorization: Bearer <GITHUB_TOKEN> instead of your actual key. Safe to commit! External APIs change. If GitHub changes their JSON structure, your test will still pass (because it's reading the old cassette), but your production app will crash. Every once in a while, or when you modify the API logic, delete the cassette file: Run the test again to generate a fresh recording. Your test suite is now deterministic, lightning-fast, and rate-limit proof. Happy coding! Did this save your CI pipeline? Let me know in the comments! Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse COMMAND_BLOCK: group :test do gem 'rspec-rails' # Assuming you use RSpec, but works with Minitest too! gem 'webmock' gem 'vcr' end Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: group :test do gem 'rspec-rails' # Assuming you use RSpec, but works with Minitest too! gem 'webmock' gem 'vcr' end COMMAND_BLOCK: group :test do gem 'rspec-rails' # Assuming you use RSpec, but works with Minitest too! gem 'webmock' gem 'vcr' end COMMAND_BLOCK: # spec/rails_helper.rb require 'vcr' require 'webmock/rspec' VCR.configure do |config| # Where to save the 'cassettes' (the recorded interactions) config.cassette_library_dir = "spec/vcr_cassettes" # Hook into WebMock config.hook_into :webmock # Allow localhost so we don't block feature tests (Capybara/Selenium) config.ignore_localhost = true # If VCR throws an error, print the metadata config.configure_rspec_metadata! end Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: # spec/rails_helper.rb require 'vcr' require 'webmock/rspec' VCR.configure do |config| # Where to save the 'cassettes' (the recorded interactions) config.cassette_library_dir = "spec/vcr_cassettes" # Hook into WebMock config.hook_into :webmock # Allow localhost so we don't block feature tests (Capybara/Selenium) config.ignore_localhost = true # If VCR throws an error, print the metadata config.configure_rspec_metadata! end COMMAND_BLOCK: # spec/rails_helper.rb require 'vcr' require 'webmock/rspec' VCR.configure do |config| # Where to save the 'cassettes' (the recorded interactions) config.cassette_library_dir = "spec/vcr_cassettes" # Hook into WebMock config.hook_into :webmock # Allow localhost so we don't block feature tests (Capybara/Selenium) config.ignore_localhost = true # If VCR throws an error, print the metadata config.configure_rspec_metadata! end COMMAND_BLOCK: # app/services/github_service.rb class GithubService def self.user_info(username) response = Faraday.get("https://api.github.com/users/#{username}") JSON.parse(response.body) end end Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: # app/services/github_service.rb class GithubService def self.user_info(username) response = Faraday.get("https://api.github.com/users/#{username}") JSON.parse(response.body) end end COMMAND_BLOCK: # app/services/github_service.rb class GithubService def self.user_info(username) response = Faraday.get("https://api.github.com/users/#{username}") JSON.parse(response.body) end end COMMAND_BLOCK: # spec/services/github_service_spec.rb require 'rails_helper' RSpec.describe GithubService do # We name the cassette specifically here it 'fetches the user profile', vcr: { cassette_name: 'github/user_info' } do result = GithubService.user_info('dhh') expect(result['login']).to eq('dhh') expect(result['id']).to be_a(Integer) end end Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: # spec/services/github_service_spec.rb require 'rails_helper' RSpec.describe GithubService do # We name the cassette specifically here it 'fetches the user profile', vcr: { cassette_name: 'github/user_info' } do result = GithubService.user_info('dhh') expect(result['login']).to eq('dhh') expect(result['id']).to be_a(Integer) end end COMMAND_BLOCK: # spec/services/github_service_spec.rb require 'rails_helper' RSpec.describe GithubService do # We name the cassette specifically here it 'fetches the user profile', vcr: { cassette_name: 'github/user_info' } do result = GithubService.user_info('dhh') expect(result['login']).to eq('dhh') expect(result['id']).to be_a(Integer) end end COMMAND_BLOCK: # spec/rails_helper.rb VCR.configure do |config| # ... previous config ... # Replace your real ENV var with a placeholder string in the cassette config.filter_sensitive_data('<GITHUB_TOKEN>') { ENV['GITHUB_TOKEN'] } config.filter_sensitive_data('<STRIPE_SECRET>') { ENV['STRIPE_SECRET_KEY'] } end Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: # spec/rails_helper.rb VCR.configure do |config| # ... previous config ... # Replace your real ENV var with a placeholder string in the cassette config.filter_sensitive_data('<GITHUB_TOKEN>') { ENV['GITHUB_TOKEN'] } config.filter_sensitive_data('<STRIPE_SECRET>') { ENV['STRIPE_SECRET_KEY'] } end COMMAND_BLOCK: # spec/rails_helper.rb VCR.configure do |config| # ... previous config ... # Replace your real ENV var with a placeholder string in the cassette config.filter_sensitive_data('<GITHUB_TOKEN>') { ENV['GITHUB_TOKEN'] } config.filter_sensitive_data('<STRIPE_SECRET>') { ENV['STRIPE_SECRET_KEY'] } end CODE_BLOCK: rm spec/vcr_cassettes/github/user_info.yml Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: rm spec/vcr_cassettes/github/user_info.yml CODE_BLOCK: rm spec/vcr_cassettes/github/user_info.yml - The external API is down. - You hit a rate limit because your CI ran 50 tests in 2 seconds. - Your laptop isn't connected to WiFi. - WebMock: The bouncer. It intercepts every single HTTP request your application tries to make and says, "No." - VCR: The recorder. It records the HTTP interaction the first time it happens and saves it to a YAML "cassette." On future runs, it replays that YAML file instantly. - Speed: < 0.01 seconds. - Stability: 100%. - WebMock cuts the internet connection. - VCR records the request once and replays it forever. - Filter Sensitive Data so you don't leak keys.