rails wisper under test - ruby-on-rails

I have a project which used wisper https://github.com/krisleech/wisper to provide publisher and subscribers functionalities.
The gem works perfectly under development and production modes. However, when I try to add some tests for them (rake test:integration), the newly added tests refused to work. The publisher (maybe also the listener) in the tests mode stopped working anymore.
Core::Request.subscribe(Listener::Studentlistener, async: true)
Core::Request.subscribe(Listener::Tutorlistener, async: true)
I used the sidekiq as a async backend, i used wisper-sidekiq gem to handle the async requests, not sure if this would be the problem?
,puma as the server, MRI ruby 2.0.0
Do I have to a set up something in order for the test to run?
it "Student can get latest status after looking for xxx tutor" do
post api_v1_students_request_look_for_xxx_tutor_path,
{ subject: 'nothing' },
{ "AUTHORIZATION" => "xxx"}
expect(response).to be_success
get api_v1_students_status_path, nil,
{ "AUTHORIZATION" => "xxx"}
expect(response).to be_success
json_response = JSON.parse(response.body)
expect(json_response['state']).to eq('matching')
end
The listener should receive the publishing between these two posts and update the state to be "matching". However, now when I run rspec the test failed because the publisher never publish anything and hence the state is not updated correctly.

Even the authors are relying on some mocking/stubbing in the integrations tests, so that might be the correct way.
class MyCommand
include Wisper::Publisher
def execute(be_successful)
if be_successful
broadcast('success', 'hello')
else
broadcast('failure', 'world')
end
end
end
describe Wisper do
it 'subscribes object to all published events' do
listener = double('listener')
expect(listener).to receive(:success).with('hello')
command = MyCommand.new
command.subscribe(listener)
command.execute(true)
end
https://github.com/krisleech/wisper/blob/master/spec/lib/integration_spec.rb

Related

How do I test whether a Sidekiq worker is sending the right data to an external API?

I have a Sidekiq worker that reaches out to an external API to get some data back. I am trying to write tests to make sure that this worker is designed and functioning correctly. The worker grabs a local model instance and examines two fields on the model. If one of the fields is nil, it will send the other field to the remote API.
Here's the worker code:
class TokenizeAndVectorizeWorker
include Sidekiq::Worker
sidekiq_options queue: 'tokenizer_vectorizer', retry: true, backtrace: true
def perform(article_id)
article = Article.find(article_id)
tokenizer_url = ENV['TOKENIZER_URL']
if article.content.nil?
send_content = article.abstract
else
send_content = article.content
end
# configure Faraday
conn = Faraday.new(tokenizer_url) do |c|
c.use Faraday::Response::RaiseError
c.headers['Content-Type'] = 'application/x-www-form-urlencoded'
end
# get the response from the tokenizer
resp = conn.post '/tokenize', "content=#{URI.encode(send_content)}"
# the response's body contains the JSON for the tokenized and vectorized article content
article.token_vector = resp.body
article.save
end
end
I want to write a test to ensure that if the article content is nil that the article abstract is what is sent to be encoded.
My assumption is that the "right" way to do this would be to mock responses with Faraday such that I expect a specific response to a specific input. By creating an article with nil content and an abstract x I can mock a response to sending x to the remote API, and mock a response to sending nil to the remote API. I can also create an article with x as the abstract and z as the content and mock responses for z.
I have written a test that generically mocks Faraday:
it "should fetch the token vector on ingest" do
# don't wait for async sidekiq job
Sidekiq::Testing.inline!
# stub Faraday to return something without making a real request
allow_any_instance_of(Faraday::Connection).to receive(:post).and_return(
double('response', status: 200, body: "some data")
)
# create an attrs to hand to ingest
attrs = {
data_source: #data_source,
title: Faker::Book.title,
url: Faker::Internet.url,
content: Faker::Lorem.paragraphs(number: 5).join("<br>"),
abstract: Faker::Book.genre,
published_on: DateTime.now,
created_at: DateTime.now
}
# ingest an article from the attrs
status = Article.ingest(attrs)
# the ingest occurs roughly simultaneously to the submission to the
# worker so we need to re-fetch the article by the id because at that
# point it will have gotten the vector saved to the DB
#token_vector_article = Article.find(status[1].id)
# we should've saved "some data" as the token_vector
expect(#token_vector_article.token_vector).not_to eq(nil)
expect(#token_vector_article.token_vector).to eq("some data")
end
But this mocks 100% of uses of Faraday with :post. In my particular case, I have no earthly idea how to mock a response of :post with a specific body...
It's also possible that I'm going about testing this all wrong. I could be instead testing that we are sending the right content (the test should check what is being sent with Faraday) and completely ignoring the right response.
What is the correct way to test that this worker does the right thing (sends content, or sends abstract if content is nil)? Is it to test what's being sent, or test what we are getting back as a reflection of what's being sent?
If I should be testing what's coming back as a reflection of what's being sent, how do I mock different responses from Faraday depending on the value of something being sent to it/
** note added later **
I did some more digging and thought, OK, let me test that I'm sending the request I expect, and that I'm processing the response correctly. So, I tried to use webmock.
it "should fetch token vector for article content when content is not nil" do
require 'webmock/rspec'
# don't wait for async sidekiq job
Sidekiq::Testing.inline!
request_url = "#{ENV['TOKENIZER_URL']}/tokenize"
# webmock the expected request and response
stub = stub_request(:post, request_url)
.with(body: 'content=y')
.to_return(body: 'y')
# create an attrs to hand to ingest
attrs = {
data_source: #data_source,
title: Faker::Book.title,
url: Faker::Internet.url,
content: "y",
abstract: Faker::Book.genre,
published_on: DateTime.now,
created_at: DateTime.now
}
# ingest an article from the attrs
status = Article.ingest(attrs)
# the ingest occurs roughly simultaneously to the submission to the
# worker so we need to re-fetch the article by the id because at that
# point it will have gotten the vector saved to the DB
#token_vector_article = Article.find(status[1].id)
# we should have sent a request with content=y
expect(stub).to have_been_requested
# we should've saved "y" as the token_vector
expect(#token_vector_article.token_vector).not_to eq(nil)
expect(#token_vector_article.token_vector).to eq("y")
end
But I think that webmock isn't getting picked up inside the sidekiq job, because I get this:
1) Article tokenization and vectorization should fetch token vector for article content when content is not nil
Failure/Error: expect(stub).to have_been_requested
The request POST https://zzzzz/tokenize with body "content=y" was expected to execute 1 time but it executed 0 times
The following requests were made:
No requests were made.
============================================================
If I try to include webmock/rspec in any of the other places, for example, at the beginning of my file, random things start to explode. For example, if I have these lines in the beginning of this spec file:
require 'spec_helper'
require 'rails_helper'
require 'sidekiq/testing'
require 'webmock/rspec'
Then I get:
root#c18df30d6d22:/usr/src/app# bundle exec rspec spec/models/article_spec.rb:174
database: test
Run options: include {:locations=>{"./spec/models/article_spec.rb"=>[174]}}
There was an error creating the elasticsearch index for Article: #<NameError: uninitialized constant Faraday::Error::ConnectionFailed>
There was an error removing the elasticsearch index for Article: #<NameError: uninitialized constant Faraday::Error::ConnectionFailed>
Which I am guessing is because the test suite is trying to initialize stuff, but webmock is interfering...
I ended up abandoning Faraday and a more complicated test as an approach. I decomposed the worker into both a Service class and a worker. The worker simply invokes the Service class. This allows me to test the service class directly, and then just validate that the worker calls the service class correctly, and that the model calls the worker correctly.
Here's the much simpler service class:
require 'excon'
# this class is used to call out to the tokenizer service to retrieve
# a tokenized and vectorized JSON to store in an article model instance
class TokenizerVectorizerService
def self.tokenize(content)
tokenizer_url = ENV['TOKENIZER_URL']
response = Excon.post("#{tokenizer_url}/tokenize",
body: URI.encode_www_form(content: content),
headers: { 'Content-Type' => 'application/x-www-form-urlencoded' },
expects: [200])
# the response's body contains the JSON for the tokenized and vectorized
# article content
response.body
end
end
Here's the test to see that we are calling the right destination:
require 'rails_helper'
require 'spec_helper'
require 'webmock/rspec'
RSpec.describe TokenizerVectorizerService, type: :service do
describe "tokenize" do
it "should send the content passed in" do
request_url = "#{ENV['TOKENIZER_URL']}/tokenize"
# webmock the expected request and response
stub = stub_request(:post, request_url).
with(
body: {"content"=>"y"},
headers: {
'Content-Type'=>'application/x-www-form-urlencoded',
}).
to_return(status: 200, body: "y", headers: {})
TokenizerVectorizerService.tokenize("y")
expect(stub).to have_been_requested
end
end
end

Webmock and VCR, allow Http Connections if there is no cassette

I have a problem, I can run a test that uses vcr on its own and it works, it creates the cassette and it uses that on the next test. Great.
The problem is when I run all my tests together this particular test fails, because webmock disables http connections, I have seen this example on the Github repo page that explains how to expect real and not stubbed requests
My question is how Do I say: Allow Http connections for requests UNLESS there is a cassette. It should also CREATE the cassette when HTTP connections are allowed.
The VCR Settings
require 'vcr'
VCR.configure do | c |
if !ARGV.first.nil?
c.default_cassette_options = { :record => :new_episodes, :erb => true }
c.filter_sensitive_data('<BLACKBIRD_API_KEY>') {YAML.load(File.read('config/application.yml'))['BLACKBIRD_API_KEY'].to_s}
c.filter_sensitive_data('<BLACKBIRD_API_URL>') {YAML.load(File.read('config/application.yml'))['BLACKBIRD_API_URL'].to_s}
c.debug_logger = File.open(ARGV.first, 'w')
c.cassette_library_dir = 'spec/vcr'
c.hook_into :webmock
end
end
the above if statement exists because not EVERY test creates a cassette. So we want them to run when a cassette isn't needed.
The Test
require 'spec_helper'
describe Xaaron::Publishers::Users do
context "publish created users" do
before(:each) do
Xaaron.configuration.reset
no_user_member_roles_relation
Xaaron.configuration.publish_to_black_bird = true
Xaaron.configuration.black_bird_api_url = YAML.load(File.read('config/application.yml'))['BLACKBIRD_API_URL']
Xaaron.configuration.black_bird_api_key =YAML.load(File.read('config/application.yml'))['BLACKBIRD_API_KEY']
end
it "should publish to blackbird" do
VCR.use_cassette 'publisher/create_user_response' do
expect(
Xaaron::Publishers::Users.publish_new_user({user: {
first_name: 'adsadsad', user_name: 'sasdasdasdsa' ,
email: 'asdassad#sample.com', auth_token: 'asdsadasdasdsa'
}}).code
).to eql 200
end
end
end
end
Runs fine on its own, creates the cassette, fails when run with all other tests due to webmock.
The Failure
Failure/Error: Xaaron::Publishers::Users.publish_new_user({user: {
WebMock::NetConnectNotAllowedError:
Real HTTP connections are disabled. Unregistered request: GET some_site_url_here with headers {'Http-Authorization'=>'api_key_here', 'User-Agent'=>'Typhoeus - https://github.com/typhoeus/typhoeus'}
You can stub this request with the following snippet:
stub_request(:get, "some site url here").
with(:headers => {'Http-Authorization'=>'some api key here', 'User-Agent'=>'Typhoeus - https://github.com/typhoeus/typhoeus'}).
to_return(:status => 200, :body => "", :headers => {})

RSpec before suite not being run

I'm trying to stub any external API calls in my test suite, but the before(:suite) is never executed. Webmock always reports that I need to stub the maps.googleapis.com even though no tests have been run yet (no green dots, no red Fs).
spec_helper.rb:
require 'webmock/rspec'
WebMock.disable_net_connect!(allow_localhost: true)
...
config.before(:suite) do
puts "THIS NEVER SHOWS"
stub_request(:get, "maps.googleapis.com").
with(headers: {'Accept'=>'*/*', 'User-Agent'=>'Ruby'}).
to_return(status: 200, body: "", headers: {})
end
The geocoder gem ends up trying to save the lat/lon from googleapis.com and an error is raised by Webmock saying that the URL is unregistered.
EDIT: Error snippet:
$ bundle exec rspec spec/factories_spec.rb
/home/jake/.rvm/gems/ruby-2.1.0#global/gems/webmock-1.17.4/lib/webmock/http_lib_adapters/net_http.rb:114:in `request': Real HTTP connections are disabled. Unregistered request: GET http://maps.googleapis.com/maps/api/geocode/json?address=[private]&language=en&sensor=false with headers {'Accept'=>'*/*', 'Accept-Encoding'=>'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'User-Agent'=>'Ruby'} (WebMock::NetConnectNotAllowedError)
You can stub this request with the following snippet:
stub_request(:get, "http://maps.googleapis.com/maps/api/geocode/json?address=[private]&language=en&sensor=false").
with(:headers => {'Accept'=>'*/*', 'Accept-Encoding'=>'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'User-Agent'=>'Ruby'}).
to_return(:status => 200, :body => "", :headers => {})
============================================================
from /home/jake/.rvm/gems/ruby-2.1.0#global/gems/geocoder-1.1.9...
...
Again, I'll stress that this has to do with the fact that the code in the config.before(:each) block is never run. Why? Because if it was, I could "raise 'WTF'" and 'WTF' should appear in the console output instead of the error you see above. I only see 'WTF' when I "un-bundle" the Webmock gem.
Well I was doing "something cute" with my RSpec tests by creating tests at runtime depending on whether or not the Factory has an attribute that is a file. Due to the way my factories/models were set up, factories were being created (saved) when the attributes for a certain factory were being read, so the block of code that's generating the tests runs outside of RSpec's config.before(:suite) and WebMock raises the error.
https://github.com/bblimke/webmock/issues/378
Moreover, here's specifically what I was doing wrong - not related to WebMock:
1) In my factories.rb, I was calling create() for associations which may not yet exist. Why? Because RSpec was giving me errors saying "[association] was blank". It was doing that because I had validates_presence_of :association_id instead of just :association. When I used create() instead of build(), it "worked". Of course when it came time to use WebMock, I was creating (and thus saving) objects calling geocoder to do it's thing. The solution was to fix validates_presence_of to use the right attribute and use build() instead of create() in my factories.
Bad Example:
# In spec/factories.rb
factory :review, class: Manager::Review do
rating 4
wine { Manager::Wine.first || create(:wine) }
reviewer { Manager::Reviewer.first || create(:reviewer) }
date Time.now
association :referral, referrable_id: 1, referrable_type: Manager::Review, strategy: :build
end
# In app/models/manager/review.rb
validates_presence_of :rating_id, :wine_id, :reviewer_id, :date
Good Example:
# In spec/factories.rb
factory :review, class: Manager::Review do
rating 4
wine { Manager::Wine.first || build(:wine) }
reviewer { Manager::Reviewer.first || build(:reviewer) }
date Time.now
association :referral, referrable_id: 1, referrable_type: Manager::Review, strategy: :build
end
# In app/models/manager/review.rb
validates_presence_of :rating, :wine, :reviewer, :date
2) FWIW, I told geocoder to fetch the geocode before_save, not after_validate like it suggests in their home page.
Also, you cannot stub with WebMock in the before(:suite), but it works in before(:each)

Rspec Suite Failing on OmniAuth Mock Auth Hash

My Rspec tests are all passing individually but are failing as the whole suite.
I have narrowed the issue down to using the mock omniauth hash describe in spec/support/devise.rb:
OmniAuth.config.test_mode = true
OmniAuth.config.mock_auth[:facebook] = {
"uid" => "1111",
"provider" => "facebook",
"credentials" => {
"token" => "token",
"secret" => "secret"
},
"extra" => {
"raw_info" => {
"name" => "Adam Waite",
"username" => "adamjwaite",
"email" => "adam#adam.com"
}
}
}
OmniAuth.config.add_mock(:facebook, OmniAuth.config.mock_auth[:facebook])
When I inspect OmniAuth.config.mock_auth[:facebook] just before the tests fails (using pry) it returns :invalid_credentials if run in the suite. If I run the same test in an individual test file it appears as it's displayed.
Here's the failing test in the registration method in my UsersController:
describe "GET :new" do
describe "as an unauthenticated user with a facebook omniauth session" do
before do
session[:omniauth_facebook] = OmniAuth.config.mock_auth[:facebook]
get :new
end
specify { assigns[:registering_with_facebook].should == true }
specify { assigns[:registering_with_twitter].should == false }
specify { response.should be_success }
end
end
It's also worth mentioning that the application function correctly too. I would just like the suite to pass.
Anyone shine any light on what's happening?
Whenever a test works in isolation, but fails with other tests, you have a test ordering issue. Some earlier test is changing global state and leaving it that way, which negatively affects this test.
I've created a small tool to help me find ordering issues in my own suites: rspec-search-and-destroy. It will take your test suite and bisect it until it finds the one test that is setting the bad global state. Of course, you can do this yourself by hand, but hopefully the tool can automate the drudgery.
Once you have found the earlier test, then you need to inspect it to figure out what global state is being set and how you can properly sandbox that change to just the test that needs it.

Stubbing out Doorkeep Token using rspec_api_documentation

I'm building an API in Rails 4 using rspec_api_documentation and have been really impressed. Having opted to use DoorKeeper to secure my endpoints, I'm successfully able to test this all from the console, and got it working.
Where I am having difficulty now is how to spec it out, and stub the token.
DoorKeeper's documentation suggests using the following:
describe Api::V1::ProfilesController do
describe 'GET #index' do
let(:token) { stub :accessible? => true }
before do
controller.stub(:doorkeeper_token) { token }
end
it 'responds with 200' do
get :index, :format => :json
response.status.should eq(200)
end
end
end
However, I've written an acceptance test in line with rspec_api_documentation. This is the projects_spec.rb that I've written:
require 'spec_helper'
require 'rspec_api_documentation/dsl'
resource "Projects" do
header "Accept", "application/json"
header "Content-Type", "application/json"
let(:token) { stub :accessible? => true }
before do
controller.stub(:doorkeeper_token) { token }
end
get "/api/v1/group_runs" do
parameter :page, "Current page of projects"
example_request "Getting a list of projects" do
status.should == 200
end
end
end
When I run the test I get the following:
undefined local variable or method `controller' for #<RSpec::Core
I suspect this is because it's not explicitly a controller spec, but as I said, I'd rather stick to this rspec_api_documentation way of testing my API.
Surely someone has had to do this? Is there another way I could be stubbing the token?
Thanks in advance.
I had the same problem and I created manually the access token with a specified token. By doing that, I was then able to use my defined token in the Authorization header :
resource "Projects" do
let(:oauth_app) {
Doorkeeper::Application.create!(
name: "My Application",
redirect_uri: "urn:ietf:wg:oauth:2.0:oob"
)
}
let(:access_token) { Doorkeeper::AccessToken.create!(application: oauth_app) }
let(:authorization) { "Bearer #{access_token.token}" }
header 'Authorization', :authorization
get "/api/v1/group_runs" do
example_request "Getting a list of projects" do
status.should == 200
end
end
end
I wouldn't recommend stubbing out DoorKeeper in an rspec_api_documentation acceptance test. One of the benefits of RAD is seeing all of the headers in the examples that it generates. If you're stubbing out OAuth2, then people reading the documentation won't see any of the OAuth2 headers while they're trying to make a client.
I'm also not sure it's possible to do this nicely. RAD is very similar to a Capybara feature test and a quick search makes it seem difficult to do.
RAD has an OAuth2MacClient which you can possibly use, here.
require 'spec_helper'
resource "Projects" do
let(:client) { RspecApiDocumentation::OAuth2MACClient.new(self) }
get "/api/v1/group_runs" do
example_request "Getting a list of projects" do
status.should == 200
end
end
end

Resources