Stubbing with WebMock not intercepting request - ruby-on-rails

I'm attempting to stub a request to a Controller using WebMock. However, as I'm creating the stub, the request isn't being intercepted the way I'd expect or want it to be.
The Controller does nothing but render JSON based on the query parameter:
def index
render json: MyThing.search(params[:query]).as_json(only: [:id], methods: [:name_with_path])
end
And the stubbing goes as follows:
mything_val = { ...json values... }
stub_request(:any, mything_path).with(query: { "query" => "a+thing" }).to_return(body: mything_val, status: 200)
page.find('.MyThingInput > input').set('a thing')
# Note: I've tried this with and without the `query:` parameter, as well as
with and without specifying header info.
This is triggering a React component. What it does is, when a word or words are entered into the input, it sends an AJAX request to mything_path with the inputted value, which returns as JSON several suggestions as to what the user might mean. These are in a li element within .MyThingInput-wrapper.
In the spec file, I include:
require 'support/feature_helper'
require 'support/feature_matchers'
require 'webmock/rspec'
WebMock.disable_net_connect!
What's actually happening when I input the text into the React component however is that regardless of the WebMock stub, it's hitting the Controller, making the DB request, and failing due to some restrictions of the testing environment. My understanding of how this should work is that when the request is made to mything_url, it should be intercepted by WebMock which would return the values I pre-defined, and never hit the Controller at all.
My guess is that somehow I'm mocking the wrong URI, but honestly, at this point I'm really uncertain. Any and all input is appreciated, and I'm happy to clarify any points I've made here. Thanks massively!

What ended up solving my problem was stubbing out the model. I'd tried stubbing the Controller but ran into issues; however, this code did the trick:
before do
mything_value = [{ "id" => "fb6135d12-e5d7-4e3r-b1h6-9bhirz48616", "name_with_path" => "New York|USA" }]
allow(MyThing).to receive(:search).and_return(mything_value.to_json)
end
This way, it still hits the controller but stubs out the DB query, which was the real problem because it made use of Elasticsearch (not running in test mode.)
I'm not super happy about hard-coding the JSON like that, but I've tried a few other methods without success. Honestly, at this point I'm just going with what works.
Interestingly enough, I'd tried this method before Infused's suggestion, but couldn't quite get the syntax right; same went with stubbing out the Controller action. Went to bed, woke up, tried it again with what I thought was the same syntax, and it worked. I'm just going to slowly back away and thank the code gods.

If elastic search is the problem, then maybe try
installing Webmock
# in your gemfile
group :test do
gem 'webmock'
end
stubbing out the requests to elasticsearch and returning the JSON
Something like this in spec_helper:
config.before(:each) do
WebMock.enable!
WebMock.stub_request(:get, /#{ELASTICSEARCH_URL}/).to_return(body: File.read('spec/fixtures/elasticsearch/search-res.json'))ELASTICSEARCH_URL
# and presumably, if you are using elasticsearch-rails, you'd want to stub out the updating as well:
WebMock.stub_request(:post, /#{ELASTICSEARCH_URL}/).to_return(status: "200")
WebMock.stub_request(:put, /#{ELASTICSEARCH_URL}/).to_return(status: "200")
WebMock.stub_request(:delete, /#{ELASTICSEARCH_URL}/).to_return(status: "200")
end
Of course, this stubs out all calls to elastic-search and returns the same JSON for all answers. Dig into the documentation for webmock if you need a different response for each query.

Related

How to create requests params for json api resources in requests tests?

I'm creating a API with Rails 6 and JSON API Resource in order to learn more. I got stuck with requests tests. What I'm doing for now is using the gem json_matchers to test the response of my endpoint, but my real problem is to build the body of requests for: post, put and patch.
Hi, thanks for you attention.
Here is the deal, I started adding to my RSpec files all the json needed for the tests, but these files ended up getting too big. So I thought, how can I makes this to stay more lean?
Then I copy all the json in my tests to json files and importing when needed, for example:
# RSpec Helper
def request_json(json_name)
request_directory = "#{Dir.pwd}/spec/support/api/requests"
request_path = "#{request_directory}/#{json_name}.json"
File.read(request_path)
end
# Example of params for user request
let(:params) { request_json("user") }
I thoutgh it was a great idea, but then I ran into two problems: I need to create a file for every request test and... how I will modify same value of this json file at runtime? For example, I create a object using FactoryBot and now I need to use the ID of this object in my request for a update. How can I do that?
Using regex?
let(:params) do
json = request_json("user")
json.gsub(/\"id\": \"1\"/, "\"id\": \"#{id}\"")
end
Ok... It works, but I don't like it! I think that this can turn to be a real mess. Another options was to convert this to hash and then json again... Nope, nope, nope, don't like it either.
Now I'm trying to create something more dynamic using FactoryBot and Faker. For now, with the code below, I can pass a factory name and receive a perfect json body for a post.
def serializer_for(resource)
{
"data": {
"type": resource.to_s.tableize,
"attributes": create_attributes_for(resource),
}
}.to_json
end
def create_attributes_for(resource)
attributes = attributes_for(resource)
attributes.reduce({}) do |hash, element|
hash.update(standardize_json_key() => element.last.to_s)
end
end
def standardize_json_key(symbol)
symbol.to_s.gsub("_", "-")
end
But then I began to think about the challenges of this approach:
Post/Put/Patch with relationships?(has_one or has_many)
How I'll add the ID field for put/path?
Add how I'll add the upload files when needed?
So... returning to my question: How can I prepare the json body for the request using factory bot (using even traits) with all the concerns above?
If you have/know a better answer for this problem, share with, please. If not, I'll try to create a gem for this.
Appreciate your time, thanks!

How to assert url was called from javascript using rspec and capybara

Scenario:
We use capybara integration tests to test that our frontend plumbing (javascript) is connected properly.
Sometimes all we need to validate the test is:
has content rendered properly on the page
has the js called the correct url open interaction
Problem:
Item 1 above is easy. However, with item 2 I can't seem to find an easy way to say:
Assert that url was called from js in browser.
Example:
it "should call coorect url with correct query string" do
visit widgets_path
# THIS IS WHAT I NEED TO KNOW
expect(something).to receive(:some_method).with(url: "my/test/url", params: {per_page: 2})
# In other words, I don't want the controller action to run. I don't care about the result since the controller is being tested elsewhere.
# I just need to know that the correct URL was called with the correct params.
within 'ul.pagination' do
click_on '2'
end
end
I've tried mocking the controller action, but there's no way to inspect the params. Or is there? Without inspecting the params, how can I know if the correct stuff was sent? All I know it's the correct route, which isn't enough.
If I could inspect the params then this would be solved... but otherwise?
If you are looking for the Rails solution, here it is! Tested with Rails 5.1.3.
1) Create a request params matcher spec/support/matchers/request_with_params.rb
RSpec::Matchers.define :request_with_params do |params|
match { |request| request.params.symbolize_keys.slice(*params.keys) == params }
end
2) Create a helper method for your acceptance tests (you can use some logics to pass symbol instead of class UsersController -> :users if needed)
def expect_request(controller, action, params = {})
expect_any_instance_of(ActionDispatch::Routing::RouteSet::Dispatcher)
.to receive(:dispatch)
.with(controller, action.to_s, request_with_params(params), anything)
end
end
3) Use it!
expect_request(UsersController, :index)
or with params
expect_request(UsersController, :show, { id: 1 })
OR
4) There is another way in using https://github.com/oesmith/puffing-billy Check this gem for intercepting requests sent by your browser. But it can be an overkill if you need to mock only certain requests to your backend app.
Capybara integration tests intentionally don't support that. They are end-to-end blackbox tests, shouldn't generally be mocked, and really only support checking for things visible to the user in the browser. In your example case that would mean expecting on whatever visible change is caused by the JS call to the specific URL. Something like
expect(page).to have_css('div.widget', count: 2)

Rails: For API's what should I write unit/functional/integration test cases?

I have rails microservices application for which I would like to write test cases, I would like to have suggestions
For API what are the possible type test cases available?
what type of test cases should I write functional/unit/integration?
What is the difference in functional/unit/integration if we talk about it in the context of API's?
Note: My application is having features like chatting, booking, payments
Well, first of all. You should understand that API it's just a controller. So you need just to check that your api's action do proper thing (crud or any other thing) and return proper fields. Much better to move this 'thing' to some command (this is a pattern), for example gem like this.
In this case your tests will be easier for support/maintain. Because in 'controller' spec you will just check what do you have in response. And for 'proper action' (for example creating of record) will respond your command.
So in the end you will have test for commands and controllers. Your controller spec will just check presence of values that is returned by serializers (AMS for example).
With commands all your controllers will look like:
def action
data = SomeImportantCommand.new(param1: params[:user], param2: param[:form]).call
respond_with data, serializer: Api::V1::SomeEpicSerializer
end
This is pseudo code, but it shows idea of command usage.
Such approach is more complicated, but it has advantages.
You are using commands that can be tested separately from controllers (here you have all your business logic).
Difficult logic can be splitted for few commands.
Because of 2-nd list item you will have simply controller test, that easy to maintain. And you can be absolutely sure that front-end application/developer will recieve all necessary data.
All your specs for controller will look like:
it 'returns some json' do
get '/api/v1/users'
expect(response.status).to eq 200
expect(response.body).to have_node(:name).with('Ivan')
# check other fields if you want
end
In code above api_matchers gem is used to parse json response.
p.s. Also you need tests for models, but this is ordinary thing, nothing special for API.

RSpec mocking response attribute

I'm stuck with a small issue which I can't seem to find on the RSpec Doc (or elsewhere). I suspect it's probably to do because it's trying an HTTP call but unsure.
I'm currently attempting to mock a response in my controller, the actual response is coming straight from aws-sdk (so it's an external service).
Controller:
response = {
snapshot_id: client.copy_snapshot({foo, bar}).snapshot_id
}
Is it possible to stub .snapshot_id?
_spec.rb
before do
allow(client).to receive(:copy_snapshot).and_return('Something')
put :update, format: :json
puts JSON.parse(response.body)
end
The tests passes successfully without adding .snapshot_id on the controller page and I can see
{"snapshot_id"=>"Something"}
Otherwise I get "this resource is currently unavailable." adding it in, I'm slightly lost if I'm doing it wrong or if this is intended behavior.
The reasoning is that when the server is on I will actually get a response back, however omitting snapshot_id causes the server to hang as amazon returns back a lot of data (headers, etc)
Any help is greatly appreciated.

How do I stub away send_file using mocha

The most direct attempt is to do
#controller.stubs(:send_file)
But that results in an output error like
ActionView::MissingTemplate: Missing template ...
So how do I stub away the send_file method from 2.3.x series.
The question is basically the same question that was asked on ruby-forum february 2009, which was never really answered.
Jarl
With Rails 4 (and maybe earlier)[1], the ImplicitRender handles this, and it checks against "performed?".
So, if you want to stub it out, stubbing the "performed?" should suffice:
subject.stubs(:performed?).returns(true)
subject.expects(:send_file).
with(image, disposition: "inline")
The implementation of performed is not that hard either:
performed?
response_body || (response && response.committed?)
end
So, in cases where you don't want, or cannot, stub performed simply ensure the response_body is not nil.
[1] With 5+ this has been moved into BasicImplicitRender.
Missing template errors probably points to the fact that send_file is now not doing anything (because of the stub) so Rails will try to go down the rendering chain and try to display the template.
You've effectively disabled the send_file call now, so you will need to change your stub to actually sends something Rails can interpret and think the request is handled because the file is served, in which case no template will need to be rendered anymore.
I've had to fight with this as well. As far as I can tell, the best solution for me has been:
(controller)
def some_action
send_file(some_action_filename)
end
private
def some_action_filename
"/public/something.tgz"
end
(test)
test "some_action" do
filename = Rails.root.join("tmp/testfile.tgz")
assert FileUtils.touch filename
#controller.expects(:some_action_filename).at_least_once().returns(filename)
post :some_action
end
I'm not a fan of altering your code to allow you to test, but I am less of a fan of spending hours and hours to find the right solution for a fairly trivial issue :)

Resources