Best way to deal with environment-sourced constants when rspec testing? - ruby-on-rails

I have a model that has a constant that looks like:
SOURCE_QUOTA = {
'free' => (ENV['FREE_SOURCE_QUOTA'] || '5').to_i,
'premium' => (ENV['PREMIUM_SOURCE_QUOTA'] || '100').to_i
}.freeze
RuboCop suggests "Style/MutableConstant" and that the freeze should be there.
However, during testing, I want to use slightly different values. Now, I could actually modify the ENV of the bash session where I launch rspec. Sure. But modifying it in the code makes life easier. For example:
describe "a thing" do
it "does stuff" do
ENV['FREE_SOURCE_QUOTA'] = '2'
DataSource::SOURCE_QUOTA["free"] = ENV['FREE_SOURCE_QUOTA'].to_i
# test code
The above works when the constant isn't frozen. I assume that's because, when frozen, the Model is loaded during the loading of the Rails environment and then this value cannot be muted (it's frozen!).
What would be the best thing to do here?
stick with frozen constant and be sure to set desired env before the rspec run?
don't freeze the constant and keep the above test
do something else that involves unfreezing/refreezing/reloading model?
Thanks!

I would think anout g a class method instead of a constant. Something like this:
# in your model
def self.source_quota
{
'free' => (ENV['FREE_SOURCE_QUOTA'] || '5').to_i,
'premium' => (ENV['PREMIUM_SOURCE_QUOTA'] || '100').to_i
}
end
and then mock that method in your tests like this:
allow(DataSource).to receive(source_quota)and_return('free' => 2)

One way is to use RSpec's stub_const feature. This allows you to stub the constant value to whatever you want in your example.
Your test code might look something like
describe 'a thing' do
before { stub_const('DataSource::SOURCE_QUOTA', {'free' => 2}) }
it 'does stuff' do
expect(DataSource::SOURCE_QUOTA["free"]).to eq 2
# test code

Related

Ruby constant date variable with respect to timecop in rspec

Hi I have a Ruby class with some constant variables using Date:
START_DATE = Date.current.at_beginning_of_month.in_time_zone + 2.days
LAST_DATE = Date.current.at_beginning_of_month.in_time_zone + 10.days
and I have some methods which uses this date inside like below:
Date.current.in_time_zone.between?(START_DATE, LAST_DATE)
in my rspec file I'm using Timecop.freeze and it's breaking my tests.
Is there a workaround to use the same variable for most of my methods? or am I using this incorrectly?
I would appreciate any help!
I actually got this answer from the Ruby slack community I got a suggestion to make it a method.
so something like:
def start_date
Date.current.at_beginning_of_month.in_time_zone + 2.days
end
I also just learned what #spickermann meant with why I shouldn't use constant variable because it will stay constant from the start of the server it will have the initial value. and technically, it's not a constant. :sweatsmile:
Whether or not you use Timecop for other interactions in your tests, you may also want to consider stubbing the constants themselves. Once you've tested the logic involved with setting the constants, consider using stub_const to ensure that the constants are set to the values you want in your test suite. For example, you might include a block in your test suite that looks something like this:
before :each do
stub_const("MyClass::START_DATE", <start_time>)
stub_const("MyClass::END_DATE", <end_time>)
end
Updated:
Comment below says this doesn't work, which is odd... definitely works for me. Just tested this like this:
class User
MY_CONST = "foo"
def my_method
MY_CONST
end
end
and then in rspec:
describe User do
it "works unstubbed" do
expect(User.new.my_const).to eq("foo")
end
it "works stubbed" do
stub_const("User::MY_CONST", "bar")
expect(User.new.my_const).to eq("bar")
end
end

RSpec: Expect to change multiple

I want to check for many changes in a model when submitting a form in a feature spec. For example, I want to make sure that the user name was changed from X to Y, and that the encrypted password was changed by any value.
I know there are some questions about that already, but I didn't find a fitting answer for me. The most accurate answer seems like the ChangeMultiple matcher by Michael Johnston here: Is it possible for RSpec to expect change in two tables?. Its downside is that one only check for explicit changes from known values to known values.
I created some pseudo code on how I think a better matcher could look like:
expect {
click_button 'Save'
}.to change_multiple { #user.reload }.with_expectations(
name: {from: 'donald', to: 'gustav'},
updated_at: {by: 4},
great_field: {by_at_leaset: 23},
encrypted_password: true, # Must change
created_at: false, # Must not change
some_other_field: nil # Doesn't matter, but want to denote here that this field exists
)
I have also created the basic skeleton of the ChangeMultiple matcher like this:
module RSpec
module Matchers
def change_multiple(receiver=nil, message=nil, &block)
BuiltIn::ChangeMultiple.new(receiver, message, &block)
end
module BuiltIn
class ChangeMultiple < Change
def with_expectations(expectations)
# What to do here? How do I add the expectations passed as argument?
end
end
end
end
end
But now I'm already getting this error:
Failure/Error: expect {
You must pass an argument rather than a block to use the provided matcher (nil), or the matcher must implement `supports_block_expectations?`.
# ./spec/features/user/registration/edit_spec.rb:20:in `block (2 levels) in <top (required)>'
# /Users/josh/.rvm/gems/ruby-2.1.0#base/gems/activesupport-4.2.0/lib/active_support/dependencies.rb:268:in `load'
# /Users/josh/.rvm/gems/ruby-2.1.0#base/gems/activesupport-4.2.0/lib/active_support/dependencies.rb:268:in `block in load'
Any help in creating this custom matcher is highly appreciated.
In RSpec 3 you can setup multiple conditions at once (so the single expectation rule is not broken). It would look sth like:
expect {
click_button 'Save'
#user.reload
}.to change { #user.name }.from('donald').to('gustav')
.and change { #user.updated_at }.by(4)
.and change { #user.great_field }.by_at_least(23}
.and change { #user.encrypted_password }
It is not a complete solution though - as far as my research went there is no easy way to do and_not yet. I am also unsure about your last check (if it doesn't matter, why test it?). Naturally you should be able to wrap it within your custom matcher.
If you want to test that multiple records were not changed, you can invert a matcher using RSpec::Matchers.define_negated_matcher. So, add
RSpec::Matchers.define_negated_matcher :not_change, :change
to the top of your file (or to your rails_helper.rb) and then you can chain using and:
expect{described_class.reorder}.to not_change{ruleset.reload.position}.
and not_change{simple_ruleset.reload.position}
BroiSatse's answer is the best, but if you are using RSpec 2 (or have more complex matchers like .should_not), this method also works:
lambda {
lambda {
lambda {
lambda {
click_button 'Save'
#user.reload
}.should change {#user.name}.from('donald').to('gustav')
}.should change {#user.updated_at}.by(4)
}.should change {#user.great_field}.by_at_least(23)
}.should change {#user.encrypted_password}
The accepted answer is not 100% correct since the full compound matcher support for change {} has been added in RSpec version 3.1.0. If you try to run the code given in accepted answer with the RSpec version 3.0, you would get an error.
In order to use compound matchers with change {}, there are two ways;
First one is, you have to have at least RSpec version 3.1.0.
Second one is, you have to add def supports_block_expectations?; true; end into the RSpec::Matchers::BuiltIn::Compound class, either by monkey patching it or directly editing the local copy of the gem. An important note: this way is not completely equivalent to the first one, the expect {} block runs multiple times in this way!
The pull request which added the full support of compound matchers functionality can be found here.

Mocks and Stubs. I don't get the basics

I am in the process of freeing myself from FactoryGirl (at least in the lib folder). So, I start writing strange stuff like "mock" and "stub". Can somebody help a novice out?
I have this module
module LogWorker
extend self
def check_todo_on_log(log, done)
if done == "1"
log.todo.completed = true
log.todo.save!
elsif done.nil?
log.todo.completed = false
log.todo.save!
end
end
end
log and todo are rails models with a todo :has_many logs association. But that should really not matter when working with stubs and mocks, right?
I have tried many things, but when I pass the mock to the method nothing happens,
describe LogWorker do
it 'should check_todo_on_log'do
todo = mock("todo")
log = mock("log")
log.stub!(:todo).and_return(todo)
todo.stub!(:completed).and_return(false)
LogWorker.check_todo_on_log(log,1)
log.todo.completed.should eq true
end
end
Failures:
1) LogWorker should check_todo_on_log
Failure/Error: log.todo.completed.should eq true
expected: true
got: false
(compared using ==
I would really like to see some spec that would test the LogWorker.check_todo_on_log method with stubs and/or mocks.
Firstly, your check_todo_on_log method is pretty bad. Never, ever use strings as options, especially when the string is "1". Also, if you pass "2", nothing happens. I'll assume though it is just a partial method, and your code isn't really like that :P
Looking at your code, you have three main problems. Firstly, you call LogWorker.check_todo_on_log(log,1). This won't do anything, as your method only does stuff when the second param is the string "1" or nil. Secondly, you stub todo.completed so it always returns false: todo.stub!(:completed).and_return(false). You then test if it is true. Obviously this is going to fail. Finally, you don't mock the save! method. I don't know how the code is actually running for you (it doesn't work for me).
Below is how I would write your specs (note that they are testing weird behaviour as the check_todo_on_log method is also strange).
Firstly, there is an easier way to add mock methods to a mock object. You can pass keys and values to the mock methods, and they will automatically be created.
Next, I put the mocks into let blocks. This allows them to be recreated easily for each test. Finally, I add a test for each possible behaviour of the function.
# you won't need these two lines, they just let the code be run by itself
# without a rails app behind it. This is one of the powers of mocks,
# the Todo and Log classes aren't even defined anywhere, yet I can
# still test the `LogWorker` class!
require 'rspec'
require 'rspec/mocks/standalone'
module LogWorker
extend self
def check_todo_on_log(log, done)
if done == "1"
log.todo.completed = true
log.todo.save!
elsif done.nil?
log.todo.completed = false
log.todo.save!
end
end
end
describe LogWorker do
let(:todo) { mock("Todo", save!: true) }
let(:log) { mock("Log", todo: todo) }
describe :check_todo_on_log do
it 'checks todo when done is "1"'do
todo.should_receive(:completed=).with(true)
LogWorker.check_todo_on_log(log,"1")
end
it 'unchecks todo when done is nil'do
todo.should_receive(:completed=).with(false)
LogWorker.check_todo_on_log(log,nil)
end
it "doesn't do anything when done is not '1' or nil" do
todo.should_not_receive(:completed=)
LogWorker.check_todo_on_log(log,3)
end
end
end
Notice how I am using behaviour based testing? I'm not testing that an attribute on the mock has a value, I am checking that an appropriate methods are called on it. This is the key to correctly using mocks.

RSpec mocks not being called

I'm trying to create a test on a controller using an rspec mock of a model, and it seems to only work when I say
Type.any_instance.should_recieve(...)
instead of
instancename.should_receive(...)
My code looks like this. (normally I use FactoryGirl, but I am not in this example to make sure that it's not the problem)
it "calls blah on foo" do
foo = Foo.new
foo.save
foo.should_receive(:blah) #this fails because it's called 0 times
#Foo.any_instance.should_receive(:blah) #this would succeed
post :create, {:foo => foo}
end
and in my Controller
def create
foo = Foo.find_by_id(params[:foo])
foo.blah
#other stuff thats not related
end
I know I could mock Foo.find_by_id and have it return foo, but I feel like I shouldn't need to do that because it should be returned anyway, and that means the test would break if I stopped using find_by_id, which is really not an important detail.
Any idea what I'm doing wrong? I feel like my test would be better if I didn't have to say any_instance everywhere and didn't have to mock find_by_id.
Your code does not work because it is not the same foo object that is being called with blah in your actual code.
In your spec code, you create an instance and save it:
foo = Foo.new
foo.save
This saves a record to the db, which foo points to. You then put an expectation on the object:
foo.should_receive(:blah)
This expectation will only work if the spec and app code point to the same object. You can achieve this, as you note, by for example stubbing find_by_id to return it. Alternatively, you can also set an expectation on any instance, which you also note.
However, the expectation will not work as-is. In your actual code, you create a different object foo:
foo = Foo.find_by_id(params[:foo])
foo.blah
If params[:foo] is the id for the record, then both foo in your spec code and foo in your app code point to the same record, but that does not mean that they are the same object (they are not).
Also, if I understand correctly, I believe that this:
post :create, {:foo => foo}
should be:
post :create, {:foo => foo.id}
So, in a nutshell, if what you want is a message expectation, you'll need to either stub find or apply the expectation on any instance. (Note that you should be able to stub find rather than find_by_id, since the dynamic finders call through to find anyway and that should make your test more robust.)
Hope that helps.

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