I have two controllers that create a user, and on creation, I want to send them an email. Because of DRY, I moved the email logic to an on_create callback. However, now, every time I create a user in my rspec tests (e.g. with factorygirl), it will send an email.
Some possible ideas I've had:
I could mock out this behavior, but then I need to mock it all the time...
I could make it a parameter, (e.g. a boolean that's true only if an email should get sent), but then I'd only add this for the test
What would be the cleanest way of doing this?
It is best practice to invoke the sending of emails from the controllers context, not the models. From the framework's point of view, mailers are very similar to controllers in how they are linked with views and control the flow of action.
I would move the callback logic back into each controller. You are correct it is not strictly DRY but in my opinion this type of scenario it is the most readable and explicit approach, worth the trade.
In your controller tests, you can check a mail was delivered:
before(:each) { ActionMailer::Base.deliveries = [] }
it "does a thing" do
subject
expect(ActionMailer::Base.deliveries.count).to eq 1
end
This approach is future proof should you have to make a change to differentiate these two emails and any developer who picks up the code will not go down a blind alley scratching their head while stray emails go out from them being hidden in the model.
Related
I am using Rails 5.
I have an Affiliate model, with a boolean attribute email_notifications_on.
I am building a quite robust email drip system for affiliates and can't figure out where the best place is to check if the affiliate has email notifications on before delivering the email.
Most of my emails are being sent from Resque BG jobs, a few others from controllers.
Here is an example of how I am checking the subscribe status from a BG job:
class NewAffiliateLinkEmailer
#queue = :email_queue
def self.perform(aff_id)
affiliate = Affiliate.find(aff_id)
if affiliate.email_notifications_on?
AffiliateMailer.send_links(affiliate).deliver_now
end
end
end
It seems like writing if affiliate.email_notifications_on? in 10+ areas is not the right way to do this, especially if I need another condition to be met in the future. Or is this fine?
I thought maybe some sort of callback in the AffiliteMailer would work, but saw many people advising against business logic in the Mailer.
Any thoughts/advice would be appreciated.
To be honest, I don't think any better way than creating a method in Affiliate model as follows,
def should_send_email?
# all business logic come here
# to start with you will just have following
# email_notifications_on?
# later you can add `&&` or any business logic for more conditions
end
You can use this method instead of the attribute. It is more re-usable and extendable. You will still have to use the method in every call. If you like single liners then you can use lambda.
(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!'
How do I write rspec tests defensively, so that in a scenario at least one expectation must be met yet the failure of others is accepted? (without the input changing). AND is easy enough by listing multiple expectations, but how is OR expressed?
As an example, a user has many posts, and user Bob hacks a form so that when he submits his create post form it sends the id of user Dunc. Currently the application ignores the passed Dunc id, and uses Bob's id as Bob is creating the post. So we could test that the newly created Post has Bob's user_id. However, if in future the code is refactored so that it returns an error message instead of assuming Bob's id, that test would wrongly fail. I want to test the intent, not the implementation.
So i need to test that either no post is created, or that if one is created, its for Bob.
This example is so simple it can be solved by testing
expect { run }.not_to change( Post.where(user_id: #other_user.id), :count )
However I'm looking for the general solution, in more complex cases there can be many conditions. How is "OR" achieved in Rspec? (or is it not possible?)
I don't think it is possible.
I do think you are mistaken when you say that you would be testing implementation, instead of intent in your example.
When you write a test, you test whether what comes out matches your expectation.
Creating a user is something completely different than returning error messages.
In my opinion it would be strange to say: when I do this, I expect this, or that, or that, or that to happen.
In my opinion you should write one test, that tests whether a user is created when you send the correct parameters, and another test that deals with what happens when a user tries to send illegal parameters.
I use RoR 3 and i guess something changed in controller's tests.
There is no
def test_should_create_post
but
test "should create user" do
...
end
Is there any decription how is that mapping etc? Because i dont get it.
And second thing. How to program (what assertion) use to test login?
so the test "something here" style is rails way of helping us out. It is fundamentally the same as def test_as_you_want but they helped us out by taking away those nasty '_(underscores)' and wrapping the actual test wording in a string. This change came back, phew... maybe 2.3.x. that fact has to be checked but at least a year and a half ago.
Your second thing is a little more harder to answer man. What plugin are you using, or are you one of those guys who are writing their own auth system?
Either way, check out how the 'famous' auth plugins do it. from Restful Auth to Devise, basically you want test that you can:
Signup for the User account
all of your confirmation emails are sent etc..
Most of these 'cheat' or take the easy way out by passing a helper called signed_in users(:one) for instance. Assuming you are cool and using fixtures.
Basically here is what a helper method looks like if your Auth plugin/gem doesn't have one, like Clearance which didn't have it when i was first writing my tests... not sure if it has it now but it sheds light on how it should look. Notice I've commented out Restful Auth and how he/they did it:
#login user
def login_user(user = users(:one))
#Restful Auth Example
# #request.session[:user_id] = user ? users(user).id : nil
# Clearance
#controller.class_eval { attr_accessor :current_user }
#controller.current_user = user
return user
end
Actually i think i stole this from their shoulda login helper... that's probably what i did. Either way it shows you how to fake login a user.
Now when you are testing, just pass this login_user method to your test when you need a user logged in and start testing the rest of the method without worrying about them actually signing in. That is what the plugin is supposed to do and the 1000 people following it on github would scream if it didn't at least LOG that guy in.
cheers