How to test model method with new rspec syntax? - ruby-on-rails

In the below spec, I want to test that for a Notification instance, running the destructive method #mark_as_dismissed will change the has_been_read column to true. How do you do that while keeping the test nice and terse?
context "#mark_as_dismissed" do
subject { create(:notification) }
subject.mark_as_dismissed # How do I specify this
its(:has_been_read) { should be_true }
end

There's multiple ways to write tests and different preferred syntaxes, so this is quite an opinionated answer.
Following your style, it would look more like this:
describe "#mark_as_dismissed" do
subject { create(:notification) }
before { subject.mark_as_dissmissed }
its(:has_been_read) { should be_true }
end
Mine would be more like this:
describe "#mark_as_dismissed" do
let(:notification) { create(:notification) }
it "marks the notification as read" do
notification.mark_as_dissmissed
notification.has_been_read.should be_true
end
end
syntatic sugar: Rspec allows a special syntax for methods returning booleans. I would have to test it, I am not sure it would work in this case but perhaps you can do something like:
# for the first alternative
it { should have_been_read }
# for the second alternative
it "marks the notification as read" do
notification.mark_as_dissmissed
notification.should have_been_read
end
Bonus points
To remove the db dependency you can just 'build' the notification instead of using 'create' (which persists the model in the database). If the #mark_as_dismissed method does not need the db (can do a non persistent update), then the test should still work.
build(:notification) # instead of create(:notification)

Related

How to stub zendesk_api current_user for a spec?

I have a model method which I am trying to write a spec for. The method is like this:
def my_method
puts current_user.user_attirbute
end
Where current_user is provided by an authentication gem, zendesk_api-1.14.4. To make this method testable, I changed it to this:
def my_method(user_attribute = nil)
if user_attribute = nil
user_attribute = current_user.user_attribute
end
puts user_attribute
end
This refactor works and is testable, but doesn't seem like a good practice. Ideally the gem would provide some sort of test helper to help stub/mock the current_user, but I haven't been able to find anything. Any suggestions?
You can go simple way and just test returning of proper value by current_user#user_attribute method. Example:
describe '#my_method' do
subject { instance.my_method } # instance is an instance of your class where #my_method is defined
let(:user) { instance_spy(ZendeskAPI::User, user_attribute: attr) }
let(:attr} { 'some-value' }
before do
allow(instance).to receive(:current_user).and_return(user)
end
it { is_expected.to eq(attr) }
end
But I would go with VCR cassette(vcr gem is here) because it is related 3rd party API response - to minimize a risk of false positive result. Next example demonstrates testing with recorded response(only in case if #current_user method performs a request to zendesk):
describe '#my_method', vcr: { cassette_name: 'zendesk_current_user' } do
subject { instance.my_method }
it { is_expected.to eq(user_attribute_value) } # You can find the value of user_attribute_value in recorded cassette
end
P.S. I assumed that you put puts in your method for debugging. If it is intentional and it is part of the logic - replace eq with output in my example.

RSpec & Rails: Stub #virtual_path for translation helper to test an application helper

I have a helper page_title_default in ApplicationHelper:
def page_title_default(options = {})
t '.title', options
end
Now I want to test it like this:
describe '#page_title' do
subject { page_title }
it { ... }
end
end
This results in the following error:
Cannot use t(".title") shortcut because path is not available
According to this post it should be possible to stub the #virtual_path variable like this:
helper.instance_variable_set(:#virtual_path, "admin.path.form")
But this doesn't seem to help: While I am able to stub it and then to call something like helper.t '.something' directly in the test, it doesn't work for the translation helper which is used in the page_title_default method (which still has #virtual_path set to nil). So it seems it's not the same instance of translation helper. But how can I find the page_title_default method one's?
How about something like:
RSpec.describe PageHelper, :type => :helper do
describe "#page_title_default" do
before do
allow(helper).to receive(:t).with(".title", {}) { "Hello!" }
end
subject { helper.page_title_default }
it { is_expected.to eq "Hello!" }
end
end
We're stubbing the "translated" string returned here to decouple the spec of helper from "real" translations, which may appear to be fragile for the test of PageHelper itself - the tests would fail every time you change the translations of ".title".
On the other hand - if you change the key used, eg. from ".title" to ".default_title" it should fail, because it is change of behaviour.
I think the proper text displayed should be tested on different level of test (integration tests, to be specific). Please, check the following answer.
Hope that helps!

Ultimate DRY model rspec

I'm refactoring my model rspecs as to be "as DRY" as possible, leading to something like
require 'spec_helper'
describe Model do
subject { build(:model) }
it { should be_valid }
it { should validate_presence_of(:description) }
it { should ensure_length_of(:description).is_at_least(3).is_at_most(255) }
it { should validate_presence_of(:position) }
it { should validate_numericality_of(:position).is_greater_than_or_equal_to(1) }
end
Now, every file starts with
subject { build(:model) }
it { should be_valid }
so, you guess it, I would like to get rid of these two lines as well...
Any suggestions?
The it { should be_valid } test seems to be testing only your factory. It's not really important to the function of the Model. Consider moving these tests to a single factories_spec test if you'd like to test them. See: https://github.com/thoughtbot/suspenders/blob/master/templates/factories_spec.rb
The matchers you are using in your example don't really require a model built with FactoryGirl. They will work fine with the implicit, default subject (Model.new). When that's not the case, I'd suggest defining as much of the state of your test as possible inside the test -- that is, inside the it blocks. If that results in some duplication, so be it. Particularly costly duplication can be extracted to method calls, which are preferable to subject, let and before because there's no magic to them. As a developer coming back to the project in 6 months, looking at spec on line 75, you'll know exactly what the setup is.
See: http://robots.thoughtbot.com/lets-not
You can use rspec shared examples:
shared_examples "a model" do
subject { build described_class }
it { should be_valid }
end
describe Foo do
it_behaves_like "a model"
end
describe Bar do
it_behaves_like "a model"
end

RSpec: check if any model has been saved

In my Rails application I've created a method that creates a model hierarchy basing on JSON data. I'd like to assure that the method does not save anything to the database. I know I can write test like:
expect {
Importer.import(json)
}.not_to change(Model1, :count)
expect {
Importer.import(json)
}.not_to change(Model2, :count)
# etc.
But I'd like to do it in more generic way. Is there a method in RSpec to check if any model had been saved during a test?
If the "import" method can return a Model object, you can write expectation like this:
imported_obj = Importer.import(json)
expect(imported_obj).not_to be_persisted
The "be_persisted" is a predicate-matcher, it simply call persisted? method on imported_obj. The method "persisted?" is a Rails' method, which return true if imported_obj has been saved.
The way you are doing it is not too bad, though it is a bit verbose.
You could use the have matcher.
Importer.import(json)
collection1 = Model.all
collection1.should have(5).items
...
collection2.should_not have(5).items
# etc....
Though, for your context, reading it like your original test makes more sense (outside of creating a custom matcher that really makes sense). Your testing what the "Importer" is expected to do or not do.
have(n).items matcher
Not sure if Rspec could help you with this, but a quick manual trip to the database might do the job?
[Model1, Model2].each do |model|
model.where('updated_at > ?', Time.now - 2.seconds).count.should eq 0
end

What's the benefit of Class.new in this Rspec

I am reading through some Rspec written by someone who left the company. I am wondering about this line:
let(:mailer_class) { Class.new(AxeMailer) }
let(:mailer) { mailer_class.new }
describe '#check' do
before do
mailer_class.username 'username'
mailer.from 'tester#example.com'
mailer.subject 'subject'
end
subject { lambda { mailer.send(:check) } }
It is testing this class:
class AxeMailer < AbstractController::Base
def self.controller_path
#controller_path ||= name.sub(/Mailer$/, '').underscore
end
I want to know the difference between this and let(:mailer_class) { AxeMailer }.
I ask this because currently when I run the test, it will complain name is nil. But if I changed it, it will test fine.
I think this issue started after using Rails 3.2, and I think name is inherited from AbstractController::Base.
This is the same in the console (meaning it is not Rspec specific), I can do AxeMailer.name with no error, but if I do Class.new(AxeMailer) there is is the problem.
My questions are:
Is there a reason to use Class.new(AxeMailer) over AxeMailer
Is there a problem if I just change this?
Is there a way not change the spec and make it pass?
I'm guessing it was written this was because of the mailer_class.username 'username' line. If you just used AxeMailer directly, the username setting would be carried over between tests. By creating a new subclass for each test, you can make sure that no state is carried over between them.
I don't know if mailer_class is being used inside the actual spec or not, but this is what I think your setup should look like:
let(:mailer) { AxeMailer.new }
describe '#check' do
before do
AxeMailer.username 'username'
mailer.from 'tester#example.com'
mailer.subject 'subject'
end
subject { lambda { mailer.send(:check) } }
There just doesn't seem to be a need for the anonymous class that was being created. Also, this is just my opinion, but your subject looks a bit odd. Your spec should probably wrap the subject in a lambda if it needs to, but don't do that in your subject.
Regarding the error you were seeing originally, anonymous classes don't have names:
1.9.3-p0 :001 > Class.new.name
=> nil
Some part of ActionMailer::Base must attempt to use the class name for something (logging perhaps) and breaks when it's nil.

Resources