1

I am working on a project where I have to test an external api which is making call to github.

Let's take the code below as example, the class below does not have intializer

Class CodeReviewSignedOff

  def pullrequestreview(github)
     #the following line return a json response, it require two parameter a string and a number

     github.client.pull_request_reviews("string",number).each do |github| 
       #block of code which perform some operation
     end

     return count #return integer response
   end
end

As you can see my class CodeReviewSignedOff relies on the github object which is defined in my existing project and using the object to make external API call. But the thing is, I do not want to actually call the API.

I want stub request github.client.pull_request_reviews

allow(github).to receive_message_chain(:client,:pull_request_review_requests).and_return json

My question here is, How should I return the json response because if I include it in the double quote it will be interpreted as string.

My second question How do I test my pullrequestreview function.

github=double #I am creating a fake github
object=CodeReviewSignedOff.new
expect(pullrequestreview).with(double).and_return(3)


Is the above logic correct?

1 Answer 1

2

Good question!

Disclaimer: I'm still learning about RoR, but I can try to answer this question as best as I can. If anyone has suggestions about how I can improve my response, please let me know!

Testing a class method (using stubs)

So, in order to test your method called "pullrequestreview", you'll need to stub a method within the Github::Client class. I personally have not worked with GitHub's API, so let's assume that it is a static class with a few class methods (including pull_request_reviews). (Note: I renamed "pullrequestreview" to pull_request_review, because it is more idiomatic in Ruby)

In the example below, I stubbed Github::Client.pull_request_reviews, so that it will return res_data whenever it is called:

Github::Client.stubs(:pull_request_reviews).returns(res_data)

Next, I use a "factory" to generate a github_client argument to pass into code_review_sign_off.pull_request_review. Since the GitHub method is stubbed, it doesn't really matter what I pass into it. In more complex test scenarios, it might be a good idea to make use of a factory bot gem to build out different GitHub related objects (with different trait options). For example, using a factory, you could define a new github_client with a "pull_request" trait like so:

github_client = create(:github_client, :with_pull_request)

In your post, code_review_sign_off.pull_request_review is expecting an iterable object from GitHub. Let's assume that code_review_sign_off.pull_request_review returns the number of elements within the array. Since I have control over GitHub's response, I know that only one element will be returned, so the result should be 1, which I assert using:

assert_equal pull_request_review_response, 1

So, in general when testing, you want to issolate the function that you're interested in and use stubs to keep everything else constant.

require 'test_helper'

class CodeReviewSignedOffTest < ActionDispatch::IntegrationTest
  test 'with CodeReviewSignedOff method' do
    res_data = [{
      :id => 1,
      :data => "some_data"
    }]

    Github::Client.stubs(:pull_request_reviews).returns(res_data)

    github_client = create(:github_client, :with_pull_request)
    code_review_sign_off = CodeReviewSignedOff.new
    pull_request_review_response = code_review_sign_off.pull_request_review(github_client)
      ...

      assert_equal pull_request_review_response, 1
    end
end

(Note: I'm using the mocha/minitest in the examples)

Bonus: Mocking an API endpoint

So, first, I am going to assume you have your test environment for RSpec properly configured (you ran rails generate rspec:install command) and you have the appropriate /spec directory.

The first step to mocking an API endpoint is to stub the calls to the actual endpoints by using a gem like WebMock. So, install the gem and add require 'webmock/rspec' to /spec/spec_helper.rb

Within /spec/spec_helper.rb you'll need to define a few things,

First, add the following to ensure that you're not continuing to make real calls to the API: WebMock.disable_net_connect!(allow_localhost: true)

Next, you'll need a stub for each API endpoint that you're mocking. For example, in your case, you'd need to define something like:

RSpec.configure do |config|
  config.before(:each) do
    pull_request_review_response = [
      {
        "id": 80,
        "node_id": "MDE3OlB1bGxSZXF1ZXN0UmV2aWV3ODA=",
        "user": {
          "login": "octocat",
          "id": 1,
          "node_id": "MDQ6VXNlcjE=",
          "avatar_url": "https://github.com/images/error/octocat_happy.gif",
          "gravatar_id": "",
          "url": "https://api.github.com/users/octocat",
          "html_url": "https://github.com/octocat",
          "followers_url": "https://api.github.com/users/octocat/followers",
          "following_url": "https://api.github.com/users/octocat/following{/other_user}",
          "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
          "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
          "subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
          "organizations_url": "https://api.github.com/users/octocat/orgs",
          "repos_url": "https://api.github.com/users/octocat/repos",
          "events_url": "https://api.github.com/users/octocat/events{/privacy}",
          "received_events_url": "https://api.github.com/users/octocat/received_events",
          "type": "User",
          "site_admin": false
        },
        "body": "Here is the body for the review.",
        "state": "APPROVED",
        "html_url": "https://github.com/octocat/Hello-World/pull/12#pullrequestreview-80",
        "pull_request_url": "https://api.github.com/repos/octocat/Hello-World/pulls/12",
        "_links": {
          "html": {
            "href": "https://github.com/octocat/Hello-World/pull/12#pullrequestreview-80"
          },
          "pull_request": {
            "href": "https://api.github.com/repos/octocat/Hello-World/pulls/12"
          }
        },
        "submitted_at": "2019-11-17T17:43:43Z",
        "commit_id": "ecdd80bb57125d7ba9641ffaa4d7d2c19d3f3091",
        "author_association": "COLLABORATOR"
      }
    ]
    
    owner = 'octocat',
    repo = 'hello-world',
    pull_number = 42
    stub_request(:get, "https://api.github.com/repos/#{owner}/#{repo}/pulls/#{pull_number}/reviews").
      to_return(status: 200, body: pull_request_review_response.to_json, 
        headers: {'Accept'=>'*/*', 'User-Agent'=>'Ruby'})
  end

  # ....
end

The important part of this code is stub_request which takes an HTTP request method, a URL path, and it returns the arguments passed into to_return (as an HTTP response). The response object will include a status, body, and a header.

I used the examples from the WebMock README and the API doc from GitHub to produce the example above (Note: It is untested).

Now, you can define a new test that calls the Mock endpoint:

require 'spec_helper'

class CodeReviewSignedOffTest < ActionDispatch::IntegrationTest
 test 'GET pull_request_review_response endpoint from GitHub mock' do
     uri = URI('https://api.github.com/repos/octocat/hello-world/pulls/42/reviews')
     response = Net::HTTP.get(uri)
     data = JSON.parse(response&.body)

     ...
   end
end

Hopefully you'll see the data you defined within your mock API come through. I haven't tested this code, so it might require some debugging.

Let me know if you have any questions or run into trouble writing your tests! Good luck.

Sign up to request clarification or add additional context in comments.

2 Comments

Really Sorry for the late response. I had trouble getting the json output from the stub request and after further exploration. I found this article which uses Sinatra and returns JSON response, perfectly. I was able to configure it for my project and get a JSON response. However, I am not using Ruby on Rails, it is a simple Ruby project. For the minitest, How can I configure it for my ruby project. class CodeReviewSignedOffTest < ActionDispatch::IntegrationTest. I do not have a action dispatch, so how should change it. Again, thank you for the wonderful explanation
Hey @C-mmon, yeah, unfortunately, ActionDispatch::IntegrationTest is a Rails class, but I did find a guide you can use to setup your Ruby project with Minitest: semaphoreci.com/community/tutorials/… One thing to be aware of is that all minitest descriptions must begin with the string test_

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.