26 Mar 2020 · Semaphore News

    Managing Externals in Ruby Tests

    5 min read
    Contents

    In this post we’ll explore options for dealing with external resources when writing tests. Generally, a common solution is to use a mock instance of the resource that is not part of your system under test. It is however important to make sure that your mock, or stubbed response is a faithful copy and does not get out of date.

    Bending time

    With Timecop, it is not necessary to create records and then modify their timestamps manually in order to verify some time-based conditions. Consider the following example:

    describe ".recent" do
    
      before do
        2.times do
          post = create(:post)
          post.update_attribute(:created_at, 2.months.ago)
        end
    
        @recent_post = create(:post)
        @recent_post.update_attribute(:created_at, 10.days.ago)
      end
    
      it "returns posts from the past month" do
        Post.recent.count.should eql(1)
        Post.recent.should include(@recent_post)
      end
    end
    

    READMORE

    Real-life examples of course get more messy, for example when you’re dealing with more elaborate conditions for a leaderboard. Compare the same spec with a version using Timecop:

    describe ".recent" do
    
      before do
        Timecop.travel(2.months.ago) do
          2.times { create(:post)
        end
    
        Timecop.travel(10.days.ago) { @recent_post = create(:post) }
      end
      
      it "returns posts from the past month" do
        Post.recent.count.should eql(1)
        Post.recent.should include(@recent_post)
      end
    end
    

    Making HTTP requests

    Depending on external resources makes the test suite slow and prone to unexpected errors in case of connectivity issues or service outages. For these reasons you should avoid making real HTTP requests in your tests. A common solution is to stub the requests and, optionally, responses. Ruby has very good tools to achieve this.

    Webmock lets you stub and set expectations on HTTP requests:

    stub_request(:get, "www.example.com").with(:query => {"a" => ["b", "c"]})
    
    RestClient.get("http://www.example.com/?a[]=b&a[]=c")    # ===> Success
    

    When dealing with many HTTP requests, especially if they are important enough that you want to stub the full response, I recommend using VCR. It works with any test framework and lets you record your test suite’s HTTP interactions in YAML files and “replay” them, ie reuse them as stubbed responses, during future test runs. We are using VCR to test Semaphore’s interaction with the GitHub API, for example.

    describe HookCreationService do
    
      let(:project) { FactoryGirl.build(:project, :hash_id => "1a3b5") }
    
      vcr_options = { :cassette_name => "HookCreationService/connection_working",
                      :record => :new_episodes }
    
      context "connection working", vcr: vcr_options do
    
        it "sets project's github_hook_id" do
          project.should_receive(:update_attribute)
            .with(:github_hook_id, instance_of(Fixnum))
    
          HookCreationService.execute(project)
        end
      end
    end
    

    The first time this test runs (in practice on your development machine), the actual HTTP request is made. VCR stores the full response, including headers in fixtures/cassette_library/HookCreationService/connection_working.yml. Future runs of the test will use Webmock under the hood to stub the request and return the saved response. Not only will they be fast, as they won’t reach for the internet, but you can be sure that they’ll be accurate, since they will use a bit-exact copy of response. If we ever hit that URL with different options, VCR will record a new copy. All this is provided by the :new_episodes option. Configuration can be provided globally of course so you don’t repeat yourself.

    You can find more information about VCR on its documentation site.

    A word of caution: VCR does not insulate you from changes in external APIs. If an API changes and you are not paying attention, your tests will still be passing but the production system will stop working. Ideally your API provider is responsible and is not making any changes without announcing them. In that case, follow the API changes feed and when something does change, delete your cassette files and re-record them.

    Testing an OAuth flow

    Developers of applications that use OAuth to authenticate users via a third-party service can find it challenging to test that flow. I will present how we test GitHub authentication for Semaphore.

    If you are using the excellent OmniAuth gem and one of its many providers, you have an option to use its facilities for integration testing. In short, you can short-circuit the flow to immediately redirect to the authentication callback (that’s a route in your app) with a provided mock hash. This is good because a fairly simple hash is what OmniAuth distills for you in production as well.

    Since we do integration testing with Cucumber, this is our features/support/omniauth.rb:

    Before("@omniauth_test") do
      OmniAuth.config.test_mode = true
    
      OmniAuth.config.mock_auth[:github] = {
        "uid" => "1",
        "provider" => "github",
        "info" => { "nickname" => "darkofabijan", "name" => "Darko Fabijan" },
        "credentials" => { "token" => "63146da137f3612f..." }
      }
    end
    
    After("@omniauth_test") do
      OmniAuth.config.test_mode = false
    end
    

    The first time this was set up, we authenticated Semaphore (a local development instance or production, does not matter) with GitHub and pasted the credentials token above. The Cucumber suite is using VCR, so the next step was to record cassettes with real API responses. A scenario only needs to include the @omniauth_test tag to apply this token to all requests to GitHub API:

    gherkin
    Feature: Building projects
    
      @omniauth_test @javascript
      Scenario: New user signs up, creates a project without collaborators and builds it
        Given I am on the homepage
        #...
    

    Again, first test runs create YAML files with real responses which we can reuse in developing the feature:

    ---
    http_interactions:
    - request:
        method: get
        uri: https://api.github.com/user/repos?access_token=63146da137f3612f...
        body:
          encoding: US-ASCII
          string: ""
        headers:
          Accept-Encoding:
          - gzip;q=1.0,deflate;q=0.6,identity;q=0.3
          Accept:
          - "*/*"
          User-Agent:
          - Ruby
      response:
        status:
          code: 200
          message: OK
        headers:
          Server:
          - GitHub.com
          #...
    

    At this point the test suit can run autonomously. The only thing left, as a security measure, is to revoke access on GitHub in order to invalidate this particular API token.

    Leave a Reply

    Your email address will not be published. Required fields are marked *

    Avatar
    Writen by:
    Marko Anastasov is a software engineer, author, and co-founder of Semaphore. He worked on building and scaling Semaphore from an idea to a cloud-based platform used by some of the world’s engineering teams.