I have a basic model like the following
class MyModel
def initialize(attrs)
#attrs = attrs
#rest_client = Some::REST::Client.new
end
def do_a_rest_call(some_str)
#rest_client.create_thing(some_str)
end
end
For testing purposes, I don't want #rest_client to make remote calls. Instead, in a test environment, I just want to make sure that #rest_client gets called with a specific some_str when it goes through certain branches of code.
In an ideal world, I'd have an assertion similar to:
expect(my_model_instance).to.receive(do_a_rest_call).with(some_str) where in the test I will pass some_str to make sure it's the right one.
What's the best way to do this using RSpec 3.8 and Rails 5.2.2?
A solution that should work without any additional gems:
let(:rest_client_double) { instance_double(Some::REST::Client, create_thing: response) }
it 'sends get request to the RestClient' do
allow(Some::REST::Client).to receive(:new).and_return(rest_client_double)
MyModel.new(attrs).do_a_rest_call(some_str)
expect(rest_client_duble).to have_received(:create_thing).with(some_str).once
end
Basically, you are creating a double for REST client.
Then, you make sure that when calling Some::REST::Client.new the double will be used (instead of real REST client instance).
Finally, you call a method on your model and check if double received given message.
Related
I'm currently changing our rails mailers to use the newer way of using the mailer that uses parameterization, which brings our code base inline with the rails guide, but more importantly it also allows the parameters to be filtered appropriately in the logs and 3rd party apps like AppSignal.
ie. I'm changing this
UserMailer.new_user_email(user).deliver_later
to
UserMailer.with(user: user).new_user_email.deliver_later
But we have a quite a few specs that use Rspec Mocks to confirm that a mailer was called with the appropriate params. Generally these test that a controller actually asked the mailer to spend the email correctly.
We generally have something like:
expect(UserMailer).to receive(:new_user_email)
.with(user)
.and_return(OpenStruct.new(deliver_later: true))
.once
But now with the parameterization of the mailer, I don't see any easy way to use rspec mocks to verify that the correct mailer method was called with the correct params. Does anyone have any ideas on how best to test this now? Readability of the expectation is probably the biggest factor here, ideally it is one line without multiple lines of mocking setup.
Note: that I don't really want to actually run the mailer, we have mailer unit specs that test the actual mailer is working.
When you have couple of methods which are chained you can use receive_message_chain
But there is one backdraw - it doesn't support the whole fluent interface of counters like once twice
So you have to do one trick here:
# set counter manually
counter = 0
expect(UserMailer).to receive_message_chain(
:with, :new_user_email
).with(user: user).with(no_args).and_return(OpenStruct.new(deliver_later: true)) do
counter += 1
end
# Very important: Here must be call of your method which triggers `UserMailer` mailer. For example
UserNotifier.notify_user(user)
expect(counter).to eq(1)
# class for example
class UserNotifier
def self.notify_user(user)
UserMailer.with(user: user).new_user_email.deliver_later
end
end
So for anyone else that hits this problem in the future. I ended up adding a helper method in the specs/support directory with something like this
def expect_mailer_call(mailer, action, params, delivery_method = :deliver_later)
mailer_double = instance_double(mailer)
message_delivery_double = instance_double(ActionMailer::MessageDelivery)
expect(mailer).to receive(:with).with(params).and_return(mailer_double)
expect(mailer_double).to receive(action).with(no_args).and_return(message_delivery_double)
expect(message_delivery_double).to receive(delivery_method).once
end
Which can then be called in a spec like this
expect_mailer_call(UserMailer, :new_user_email, { to:'email#email.com', name: kind_of(String) })
or for deliver_now
expect_mailer_call(UserMailer, :new_user_email, { to:'email#email.com', name: kind_of(String) }, :deliver_now)
It works good enough for our situation, but you might need to adapt it and add a part for the amount of emails or something if you need to configure the once restriction.
(I think this question generalises to stubbing any extensively-pinged API, but I'm asking the question based on the code I'm actually working with)
We're using the Contentful Model extensively in our controllers and views including in our layouts. This means that in any feature test where we visit (say) the homepage, our controller action will include something like this:
class HomepageController < ApplicationController
def homepage
# ... other stuff
#homepage_content = Homepage.find ('contentful_entry_id')
end
end
... where Homepage is a subclass of ContentfulModel::Base, and #homepage_content will have various calls on it in the view (sometimes chained). In the footer there's a similar instance variable set and used repeatedly.
So for feature testing this is a pain. I've only come up with two options:
Stub every single call (dozens) on all Contentful model instances, and either stub method chains or ensure they return a suitable mock
or
Use a gem like VCR to store the Contentful responses for every feature spec
Both of these (at least the way I'm doing them) have pretty bad drawbacks:
1) leads to a bunch of test kruft that will have to be updated every time we add or remove a field from the relevant model;
2) means we generate a vcr yaml files for every feature test - and that we have to remember to clear the relevant yml file whenever we change an element of the test that would change the requests it sends
Am I missing a third option? Or is there some sensible way to do either of the above options without getting the main drawbacks?
I'm the maintainer of contentful_model.
We use VCR to stub API Calls, so that you can test with real data and avoid complicated test code.
Cheers
I created an RSpec spec to test if a POST #create action works properly:
describe "POST #create" do
it "creates a career" do
expect {
post "/careers/", career: attributes_for(:career)
}.to change(Career, :count).by 1
end
end
The above code works correctly. The issue happens when I create another test to allow only users with roles of "admin". Do I need to create a new user, log them in, and then run the above test? Do I need to do this for all future tests which have a restriction based on the user's role?
Is there another way to do this type of testing? 1) Just test if the create method works, and 2) only allow Users with "admin" role access the GET #new and POST #create methods?
When your feature is fully developed you'll want to have the following tests:
one happy-path test in which an admin creates a career
one in which a non-admin tries to create a career but is prevented from doing so
possibly another in which a not-logged-in user tries to create a career but is prevented from doing so (whether you want this depends on whether you have to write different code to handle non-logged-in and logged-in non-admin users), and
possibly other tests of different scenarios in which an admin creates a career.
This idea of having one complete, happy-path test is one of the most fundamental patterns in testing, but I'm not aware that it has a name, other than being implied by the term "happy path".
It looks like you're doing TDD. Great! To get from where you are now to the above list of tests, the next test to write is the one where the non-logged-in user is prevented from creating a career. To make both tests pass at the same time you'll need to change the first test to log in an admin. And if you need more tests of successfully creating a career (bullet 4), yes, you'll need to log in an admin in those too.
Side notes:
Unless you already have it, I'd write your happy-path spec not as a controller spec but as a feature spec (an acceptance test), so that you specify the important parts of the UI and integration-test the entire stack. Your failed-authentication specs might work as controller specs, although you might decide you need to acceptance-test the UI when a user doesn't have permissions for at least one of those scenarios.
I really don't like that expect {}.to change syntax. It prevents you from making any other expectations on the result of posting. In your example I would want to expect that the HTTP response status is 200 (response.should be_success). As I said, though, my first spec would be a feature spec, not a controller spec.
So, this is an interesting question. Yes, you should definitely (IMO) test authentication separately from the target method/action. Each of these constitutes a unit of functionality and should be tested as such.
In my current project, I'm favoring POROs (I often keep them in a directory called 'managers' although I know many people prefer to call them 'services') for all sorts of things because it lets me isolate functionality and test it independently. So, I might end up with something like:
# controllers/foo_controller.rb
class FooController < ApplicationController
before_action :authenticate
def create
#results = FooManager.create(params)
redirect_to (#results[:success] ? my_happy_path : my_sad_path)
end
def authenticate
redirect_to unauthorized_path unless AuthenticationManager.authenticate(params, request)
end
end
# managers/foo_manager.rb
class FooManager
class << self
def create(params)
# do a bunch of great stuff and return a hash (perhaps a
# HashWithIndifferentAccess, if you like) which will
# allow for evaluation of #results[:success] back in the
# controller.
end
end
end
# managers/authentication_manager.rb
class AuthenticationManager
class << self
def authenticate(params, request)
# do a bunch of great stuff and return a boolean
end
end
end
With an approach like this, I can very easily test FooManager.create and AuthenticationManager.authenticate (as well as FooController.create and FooController.authenticate routing) all independently. Hooray!
Now, whether your authentication framework or controller method is behaving correctly at a unit level, as Dave points out very well, is a separate issue from whether your entire system is behaving as expected. I'm with him on having high-level integration tests so you're clear about what 'done' looks like and you know when to holler 'ship it!'
I am no stranger to testing. I pride my self on have 97% - 100% test coverage. In fact anything below 95% is poor (but thats off topic). I have the following rails controller:
module Api
module Internal
class TwitterController < Api::V1::BaseController
# Returns you 5 tweets with tons of information.
#
# We want 5 specific tweets with the hash of #AisisWriter.
def fetch_aisis_writer_tweets
tweet_array = [];
tweet = twitter_client.search("#AisisWriter").take(5).each do |tweet|
tweet_array.push(tweet)
end
render json: tweet_array
end
private
# Create a twitter client connection.
def twitter_client
client = Twitter::REST::Client.new do |config|
config.consumer_key = ENV['CONSUMER_KEY']
config.consumer_secret = ENV['CONSUMER_SECRET_KEY']
config.access_token = ENV['ACCESS_TOKEN']
config.access_token_secret = ENV['ACCESS_TOKEN_SECRET']
end
end
end
end
end
It's extremely basic to see whats going on. Now I could write the rspec tests to say call this action, I expect json['bla']['text'] to eql bla.
But there is a couple issues. In order to effectively test this you need twitter API credentials. Thats coupling my code with another service that I am hoping is up and running.
In fact my controller is essentially coupled to twitter.
So - My question is, with out having to mock a web service or a api call (I have seen some blog posts out there on this, and for this piece of code, I feel they are over kill) - How would you test this?
Some people have suggested VCR. Any thoughts on testing API calls like this?
I've found VCR to be a great tool for tests like this - where you don't need a ton of control over what the external service returns, because you don't have a lot of cases to test. You just want to eliminate test flakiness based on whether or not the service is up, and you want to make sure that you get exactly the same fake response every time. I wouldn't say VCR is overkill at all, it's very simple to use - you just wrap your test in a use_cassette block, run your test, and VCR records the actual response from the service and uses it as the mocked response from then on.
I will say that the "cassettes" that VCR uses to store the mocked responses are fairly complex YAML, and they're not super readable/easy to edit. If you want to be able to easily manipulate the data that's returned so that you can test several code paths, and easily read it so that your mocked data can serve as documentation of the code, I'd look into something more like HttpMock.
One other option, of course, would be to just stub out the private method that calls the external service and have it return your mock data directly. Usually I'd avoid that, so that you can refactor your private method and still be covered, but it might be an option in some cases where the private method is dead simple and unlikely to change, and stubbing it out makes for significantly cleaner tests.
I have a rails application that I am implementing the Twilio SMS API on, and I am a bit lost on how to test drive my design.
To start I've just made a model that is an SMS mailer that will encapsulate the twilio API and I want to be able to test it and ensure functionality without using up SMS credits or bombarding someone with test text messages.
I know how to implement the API and get it working in the code but what I need help with is actually testing the code to make sure it works and prevent breakage in the future. Could anyone provide some advice?
Thanks!
You could use my gem Twilio.rb, which is already tested, and then mock it out in your tests, e.g. with mocha
Twilio::SMS.expects(:create).with :to => '+19175551234', :from => '+12125551234', :body => 'this is easy!'
Your unit tests should never hit external services, they should always be mocked. This is follows from a general principle of unit testing that tests should not extend the class boundary of the object being tested and collaborator objects should be mocked/stubbed.
Hope this helps!
https://github.com/stevegraham/twilio-rb
My experience with testing, and with testing Twilio applications, is that you test to eliminate risk you add. You'll want to use the Twilio gem rather than rolling your own SMS code against their REST endpoint: this minimizes the amount of risk.
Wrap the API as thinly as possible in your business logic class, and test primarily the business logic. For example, in my system, SMSes get sent out of the Reminder class. The code looks something like this:
class SomeWrapperClass
if (RAILS_ENV == "testing")
##sent_smses = []
cattr_accessor :sent_smses
end
def send_a_message(to, from, message, callback_url = nil)
unless RAILS_ENV == "testing"
Twilio::SMS.message(to, from, message, callback_url)
else
##sent_smses << {:to => to, :from => from, :message => message, :callback_url => callback_url}
end
end
end
This lets me write tests focusing on my business logic, which is the stuff I'm going to screw up. For example, if I want to test some method send_reminder(client) which sends a SMS message:
test "sends reminder to client" do
SomeWrapperClass.sent_smses = []
client = clients(:send_reminder_test_case)
Reminder.send_reminder(client)
sent_message = SomeWrapperClass.sent_smses.last
assert !sent_message.blank?, "Sending a reminder should fire an SMS to client."
assert sent_message.index(client.name) >= 0, "Sending a reminder should fire an SMS with the client's name in it.
...
end
Now I'm testing the actual risk I've added, which is that I'm screwing up Reminder.send_reminder. The wrapper, on the other hand, should be close to risk-free.
Obviously separate as much of the logic as possible. By doing this you can test everything else around as much as possible and then only leave the calls to the external API needing tests.
Working with external API's can be tricky. One option is to mock the response to something that you know will work for you or to the response you would expect, this can obviously be a bit brittle though. Another option is to look at something like VCR. This will record the call to the external API once and play it back again whenever you call it again.
This guy seems to have started solving your problem: https://github.com/arfrank/Fake-Twilio-Api
You probably don't need to test twiliolib's code, but if you don't want to stub twiliolib's methods you could use the FakeWeb gem, where you define the response for specified requests.
Similar to Steve mentioned, I just stub out the request with mocha:
# In Twilio initializer
TWILIO_ACCOUNT = Twilio::RestAccount.new(TWILIO_CONFIG[:sid], TWILIO_CONFIG[:token])
# In a test helper file somewhere
class ActiveSupport::TestCase
# Call this whenever you need to test twilio requests
def stub_twilio_requests
# Stub the actual request to Twilio
TWILIO_ACCOUNT.stubs(:request).returns(Net::HTTPSuccess.new(nil, nil, nil).tap { |n|
n.stubs(:body).returns("<?xml version=\"1.0\"?>\n<TwilioResponse></TwilioResponse>\n")
})
end
end