is it good to use global variables in specs? - ruby-on-rails

is it good to use global variables in specs?
I want to use this var in my tests. And define it in spec_helper.rb
$now = DateTime.parse('2020-01-01 00:00:01 -0500')
Is this a good idea or not? and why?

Global variables are discouraged, in RSpec, and in general. Here's why:
Modifying a global variable anywhere affects code everywhere.
So, if a test ever modified the $now variable, all subsequent tests would be affected by the modification. This could lead to some very hard to debug test failures. Hard to debug, because the test would be correct. But, the value of the global would not. So, you could not find the bug in the tests by simple inspection. You'd have to debug it at runtime.
Automated tests need to be independent of each other. This is why RSpec has the lazily executed let statement. let allows you to define a variable within the context of a single example. Here's what the RSpec docs say:
Use let to define a memoized helper method. The value will be cached across
multiple calls in the same example but not across examples.
What this means is that if you define now like this:
let(:now) { DateTime.parse('2020-01-01 00:00:01 -0500') }
The value is guaranteed to be exactly what you say it is in every single test. Because the value is memoized on a per example basis, one test can never affect the value that another test receives when executing.
If you're still not convinced, I would recommend using a global constant over a global variable. At least then, Ruby would warn you if you tried to change the value.

It's a bad idea. Actually, you should avoid using global variables anywhere in your code.
You can use before block to set any variable to need to use along with your tests examples.
Example:
describe Thing do
before(:each) do
#now = DateTime.parse('2020-01-01 00:00:01 -0500')
end
describe "initialized in before(:each)" do
it "example 1" do
// here #now is available
end
it "example 2" do
// here #now is also available
end
end
end

Related

Class has leaked into another example and can no longer be used in spec

I'm unable to replicate this locally, but for some reason I am getting the following error when running tests in CircleCi:
<Double Mylogger> was originally created in one example but has leaked into another example and can no longer be used. rspec-mocks' doubles are designed to only last for one example, and you need to create a new one in each example you wish to use it for.
Here is a simplified version of my code:
# frozen_string_literal: true
describe 'my_rake_task' do
let(:my_log) { Mylogger.new }
subject { Rake::Task['my_rake_task'].execute }
describe 'one' do
context 'logs' do
let(:logs) do
[
['My message one'],
['My message two'],
]
end
after { subject }
it 'correctly' do
logs.each { |log| expect(my_log).to receive(:info).with(*log) }
end
end
end
describe 'two' do
context 'logs' do
let(:logs) do
[
['My message three'],
['My message four'],
]
end
after { subject }
it 'correctly' do
logs.each { |log| expect(my_log).to receive(:info).with(*log) }
end
end
end
end
Why is it saying MyLogger is a double? Why would it be leaking?
The reason that the error is saying that MyLogger is a double is because it is one. When you call expect(my_log).to receive or allow(my_log).to receive, you transform the instance into a partial-double.
As for why my_log is leaking: it's impossible to tell from the code that you posted. In order to cause a leak, some code either in your rake task or in the spec itself would need to be injecting my_log into some global state, like a class variable.
Mostly commonly this sort of thing is caused by storing something in a class variable. You will have to figure out where that is, and how to clear it or avoid using a class variable - it could be in your class or in a gem.
Best practice, where using a class variable or an external system is causing inter-test issues, is to clean this sort of thing between tests, if possible. For example ActionMailer::Base.deliveries and Rails.cache are common things that should be cleared. You should also clear Faker::UniqueGenerator or RequestStore if you're using those gems, and I'm sure there are more.
Once you have found the class variable, if it's in your code, and you have determined a class variable is the correct approach, you can add a reset or clear class method to the class and call it in a before(:each) RSpec block in your spec_helper.rb or rails_helper.rb.
Note that while a lot of things will clear themselves automatically between tests (such as RSpec mocks), and make you think this is all automatic, in practice it is often anything but.
Tests will only remain independent by (a) mostly making use of objects created in the tests and mostly only storing data in there and (b) ensuring anything else is cleared between tests by your explicit code or within the responsible gem.
This can be especially annoying when dealing with external third-party systems, which rarely provide an API to clear a staging environment, and hence sometimes require considerable care even when using the vcr gem.

How does Rspec 'let' helper work with ActiveRecord?

It said here https://www.relishapp.com/rspec/rspec-core/v/3-5/docs/helper-methods/let-and-let what variable defined by let is changing across examples.
I've made the same simple test as in the docs but with the AR model:
RSpec.describe Contact, type: :model do
let(:contact) { FactoryGirl.create(:contact) }
it "cached in the same example" do
a = contact
b = contact
expect(a).to eq(b)
expect(Contact.count).to eq(1)
end
it "not cached across examples" do
a = contact
expect(Contact.count).to eq(2)
end
end
First example passed, but second failed (expected 2, got 1). So contacts table is empty again before second example, inspite of docs.
I was using let and was sure it have the same value in each it block, and my test prove it. So suppose I misunderstand docs. Please explain.
P.S. I use DatabaseCleaner
P.P.S I turn it off. Nothing changed.
EDIT
I turned off DatabaseCleaner and transational fixtures and test pass.
As I can understand (new to programming), let is evaluated once for each it block. If I have three examples each calling on contact variable, my test db will grow to three records at the end (I've tested and so it does).
And for right test behevior I should use DatabaseCleaner.
P.S. I use DatabaseCleaner
That's why your database is empty in the second example. Has nothing to do with let.
The behaviour you have shown is the correct behaviour. No example should be dependant on another example in setting up the correct environment! If you did rely on caching then you are just asking for trouble later down the line.
The example in that document is just trying to prove a point about caching using global variables - it's a completely different scenario to unit testing a Rails application - it is not good practice to be reliant on previous examples to having set something up.
Lets, for example, assume you then write 10 other tests that follow on from this, all of which rely on the fact that the previous examples have created objects. Then at some point in the future you delete one of those examples ... BOOM! every test after that will suddenly fail.
Each test should be able to be tested in isolation from any other test!

Use RSpec let(:foo) across examples

I'm using let(:foo) { create_foo() } inside my tests. create_foo is a test helper that does some fairly time expensive setup.
So everytime a test is run, foo is created, and that takes some time. However, the foo object itself does not change, I just want to unit test methods on that object, one by one, seperated into single tests.
So, is there a RSpec equivalent of let to share the variable across multiple examples, but keep the nice things like lazy loading (if foo isn't needed) and also the automatic method definition of foo so that it can be used in shared examples, without referencing it with a #foo?
Or do I have to simply define a
def foo
create_foo()
end
Can you just put it in shared examples but use memoization?
def foo
#foo ||= create_foo()
end
Using let in this way goes against what it was designed for. You should consider using before_all which runs once per test group:
before :all do
#instancevar = create_object()
end
Keep in mind that this may not be wise if create_object() hits a database since it may introduce coupling and maintain state between your tests.

Understanding Mutant Failures

I have the following ActiveRecord model class method:
def self.find_by_shortlink(shortlink)
find_by!(shortlink: shortlink)
end
When I run Mutant against this method, I'm told there were 17 mutations and 16 are still "alive" after the test has run.
Here's one of the "live" mutations:
-----------------------
evil:Message.find_by_shortlink:/home/peter/projects/kaboom/app/models/message.rb:29:3f9f2
## -1,4 +1,4 ##
def self.find_by_shortlink(shortlink)
- find_by!(shortlink: shortlink)
+ find_by!(shortlink: self)
end
If I manually make this same change, my tests fail - as expected.
So my question is: how do I write a unit test that "kills" this mutation?
Disclaimer, mutant author speaking.
Mini cheat sheet for such situations:
Make sure your specs are green right now.
Change the code as the diff shows
Try to observe an unwanted behavior change.
Impossible?
(likely) Take the mutation as better code.
(unlikely) Report a bug to mutant
Found a behavior change: Encode it as a test, or change a test to cover that behavior.
Rerun mutant to verify the death of the mutation.
Make sure mutant actually lists the tests you added as used for that mutation. If not restructure the tests to cover the subject of the mutation in the selected tests.
Now to your case: If you apply the mutation to your code. The argument gets ignored and essentially hardcoded (the value for key :shortlink used in your finder does not change depending on argument shortlink). So the only thing you need to do in your test is adding a case where the argument shortlink matters to the expectation you place in the test.
If passing self as value for the :shortlink finder has the same effect as passing in the current argument you test, try to use a different argument. Coercion of values in finders can be tricky in AR, there is the chance your model coerces to the same value you are testing as argument.

How do I test `rand()` with RSpec?

I have a method that does something like this:
def some_method
chance = rand(4)
if chance == 1 do
# logic here
else
# another logic here
end
end
When I use RSpec to test this method, rand(4) inside it always generates 0. I am not testing rand method of Rails, I am testing my method.
What is a common practice to test my method?
There are two approaches I would consider:
Approach 1:
Use a known value of seed in srand( seed ) in a before :each block:
before :each do
srand(67809)
end
This works across Ruby versions, and gives you control in case you want to cover particular combinations. I use this approach a lot - thinking about it, that's because the code I was testing uses rand() primarily as a data source, and only secondarily (if at all) for branching. Also it gets called a lot, so exerting call-by-call control over returned values would be counter-productive, I would end up shovelling in lots of test data that "looked random", probably generating it in the first place by calling rand()!
You may wish to call your method multiple times in at least one test scenario to ensure you have reasonable coverage of combinations.
Approach 2:
If you have branch points due to values output from rand() and your assertions are of the type "if it chooses X, then Y should happen", then it is also reasonable in the same test suite to mock out rand( n ) with something that returns the values you want to make assertions about:
require 'mocha/setup'
Kernel.expects(:rand).with(4).returns(1)
# Now run your test of specific branch
In essence these are both "white box" test approaches, they both require you to know that your routine uses rand() internally.
A "black box" test is much harder - you would need to assert that behaviour is statistically OK, and you would also need to accept a very wide range of possibilities since valid random behaviour could cause phantom test failures.
I'd extract the random number generation:
def chance
rand(4)
end
def some_method
if chance == 1 do
# logic here
else
# another logic here
end
end
And stub it:
your_instance.stub(:chance) { 1 }
This doesn't tie your test to the implementation details of rand and if you ever decide to use another random number generator, your test doesn't break.
It seems that best idea is to use stub, instead of real rand. This way you would be able to test all values that you are interested in. As rand is defined in Kernel module you should stub it using:
Kernel.stub(:rand).with(anything) { randomized_value }
In particular contexts you can define randomized_value with let method.
I found that just stubbing rand ie. using Kernel.stub(:rand) as answered by Samuil did not initially work. My code to be tested called rand directly e.g
random_number = rand
However, if I changed the code to
random_number = Kernel.rand
then the stubbing worked.
This works in RSpec:
allow_any_instance_of(Object).to receive(:rand).and_return(1)

Resources