I need to verify that any instance of my class receives a certain method, but I don't care if many instances receive it (they're supposed to).
I tried like this:
expect_any_instance_of(MyClass).to receive(:my_method).at_least(:once)
But apparently, it only allows for a single instance to receive the method multiple times, but not for different instances.
Is there a way to achieve that?
If you need to live with the code smell, this rspec-mocks Github issue suggests a solution along these lines:
receive_count = 0
allow_any_instance_of(MyClass).to receive(:my_method) { receive_count += 1 }
# Code to test here.
expect(receive_count).to be > 0
This is a known issue in rspec-mocks. From the v3.4 documentation on Any instance:
The rspec-mocks API is designed for individual object instances, but this feature operates on entire classes of objects. As a result there are some semantically confusing edge cases. For example, in expect_any_instance_of(Widget).to receive(:name).twice it isn't clear whether a specific instance is expected to receive name twice, or if two receives total are expected. (It's the former.)
Furthermore
Using this feature is often a design smell. It may be that your test is trying to do too much or that the object under test is too complex.
Do you have any way to refactor your test or app code to avoid the "confusing edge case"? Perhaps by constructing a test double and expecting it to receive messages?
Related
I have the following line of code which very simply decrements a Shareholders number of stocks (integer in the DB) when they sell them to someone else. This works well and as expected.
#selling_shareholder.update_attribute(:number_of_stocks, #selling_shareholder.number_of_stocks -= #transaction.number_of_stocks)
Very simply what I'd like to do is have the decrement function stop when the number_of_stocks hits 0 i.e. having a negative number should not be possible.
I guess I could use a simple unless #selling_shareholder.number_of_stocks > 0 at the end of the line, but I'm wondering if that will actually work without using a loop?
I would suggest creating a method in your model called something like #decrement_stock that would handle that logic. You can set all kind of behaviors that should be expected, like raising exceptions. That approach follows the "tell don't ask" principle. It is also a simple unit of work performing a single task, making it easy to test with your favorite unit testing framework.
As engineersmnky suggested, simply adding validation on the model is also a good solution if you want the controller to handle errors.
In one of my old apps, I'm using several API connectors - like AWS or Mandill as example.
For some reason (may be I saw it somewhere, don't remember), I using class constant to initialize this objects on init stage of application.
As example:
/initializers/mandrill.rb:
require 'mandrill'
MANDRILL = Mandrill::API.new ENV['MANDRILL_APIKEY']
Now I can access MANDRILL class constant of my application in any method and use it. (full path MyApplication::Application::MANDRILL, or just MANDRILL). All working fine, example:
def update_mandrill
result = MANDRILL.inbound.update_route id, pattern, url
end
The question is: it is good practice to use such class constants? Or better create new class instance in every method that using this instance, like in example:
def update_mandrill
require 'mandrill'
mandrill = Mandrill::API.new ENV['MANDRILL_APIKEY']
result = mandrill.inbound.update_route id, pattern, url
end
Interesting question.
It's very handy approach but it may have cons in some scenarios.
Imagine you have a constant that either takes a lot of time to initialize or it loads a lot of data into memory. When its initialization takes long you essentially degrade app boot time (which may or may not be a problem, usually it will in development).
If it loads a lot of data into memory it may turn out it's gonna be a problem when running rake tasks for example which load entire environment. You may hit memory boundaries in use cases in which you essentially do not need this data at all.
I know one application which load a lot of data during boot - and it's done very deliberately. Sure, use case is a bit uncommon, but still.
Another thing to consider is - imagine, you're trying to establish connection to external service like Mongo or anything else. If this service is unavailable (what happens) your application won't be able to boot. Maybe this service is essential for app to work, and without it it would be "useless" anyway, but it's also possible that you essentially stop everything because storage in which you keeps log does not work.
I'm not saying you shouldn't use it as you suggested - I do it also in my apps, but you should be aware of potential drawbacks.
Yes, pre-creating a pseudo-constant object (like that api client) is usually a good idea. However, there is, approximately, a thousand ways go about it and the constant is not on top of my personal list.
These days I usually go with setting it in the env files.
# config/environments/production.rb
config.email_client = Mandrill::API.new ENV['MANDRILL_APIKEY'] # the real thing
# config/environments/test.rb
config.email_client = a_null_object # something that conforms to the same api, but does absolutely nothing
# config/environments/development.rb
config.email_client = a_dev_object # post to local smtp, or something
Then you refer to the client like this:
Rails.application.configuration.email_client
And the correct behaviour will be picked up in each env.
If I don't need this per-env variation, then I either use some kind of singleton object (EmailClient.get) or a global variable in the initializer ($email_client). It can be argued that a constant is better than global variable, semantically and because it raises a warning when you try to re-assign it. But I like that global variable stands out more. You see right away that it's something special. (And then again, it's only #3 in the list, so I don't do it very often.).
With delayed_job, I was able to do simple operations like this:
#foo.delay.increment!(:myfield)
Is it possible to do the same with Rails' new ActiveJob? (without creating a whole bunch of job classes that do these small operations)
ActiveJob is merely an abstraction on top of various background job processors, so many capabilities depend on which provider you're actually using. But I'll try to not depend on any backend.
Typically, a job provider consists of persistence mechanism and runners. When offloading a job, you write it into persistence mechanism in some way, then later one of the runners retrieves it and runs it. So the question is: can you express your job data in a format, compatible with any action you need?
That will be tricky.
Let's define what is a job definition then. For instance, it could be a single method call. Assuming this syntax:
Model.find(42).delay.foo(1, 2)
We can use the following format:
{
class: 'Model',
id: '42', # whatever
method: 'foo',
args: [
1, 2
]
}
Now how do we build such a hash from a given call and enqueue it to a job queue?
First of all, as it appears, we'll need to define a class that has a method_missing to catch the called method name:
class JobMacro
attr_accessor :data
def initialize(record = nil)
self.data = {}
if record.present?
self.data[:class] = record.class.to_s
self.data[:id] = record.id
end
end
def method_missing(action, *args)
self.data[:method] = action.to_s
self.data[:args] = args
GenericJob.perform_later(data)
end
end
The job itself will have to reconstruct that expression like so:
data[:class].constantize.find(data[:id]).public_send(data[:method], *data[:args])
Of course, you'll have to define the delay macro on your model. It may be best to factor it out into a module, since the definition is quite generic:
def delay
JobMacro.new(self)
end
It does have some limitations:
Only supports running jobs on persisted ActiveRecord models. A job needs a way to reconstruct the callee to call the method, I've picked the most probable one. You can also use marshalling, if you want, but I consider that unreliable: the unmarshalled object may be invalid by the time the job gets to execute. Same about "GlobalID".
It uses Ruby's reflection. It's a tempting solution to many problems, but it isn't fast and is a bit risky in terms of security. So use this approach cautiously.
Only one method call. No procs (you could probably do that with ruby2ruby gem). Relies on job provider to serialize arguments properly, if it fails to, help it with your own code. For instance, que uses JSON internally, so whatever works in JSON, works in que. Symbols don't, for instance.
Things will break in spectacular ways at first.
So make sure to set up your debugging tools before starting off.
An example of this is Sidekiq's backward (Delayed::Job) compatibility extension for ActiveRecord.
As far as I know, this is currently not supported. You can easily simulate this feature using a custom-defined proxy-job that accepts a model or instance, a method to be performed and a list of arguments.
However, for the sake of code testing and maintainability, this shortcut is not a good approach. It's more effective (even if you need to write a little bit more of code) to have a specific job for everything you want to enqueue. It forces you to think more about the design of your app.
I wrote a gem that can help you with that https://github.com/cristianbica/activejob-perform_later. But be aware that I believe that having methods all around your code that might be executed in workers is the perfect recipe for disaster is not handled carefully :)
I'm trying to make sense of the tests in an inherited app, and I need some help.
There are lots of spec groups like this one (view spec):
let(:job_post) { FactoryGirl.create(:job_post) }
# ...
before do
expect(view).to receive(:job_post).at_least(:once).and_return(job_post)
end
it "should render without error" do
render
end
... with job_post being an helper method defined on the controller. (yes, they could have used #instance variables, and I'm in the process of refactoring it).
Now, in my opinion using an expect inside a before block is wrong. Let's forget about that for a second.
Normally the test above is green.
However, if I remove the expect line, the test fails. It appears that in this case expect is stubbing the method on the view. In fact, replacing expect with allow seems to have exactly the same effect.
I think that what's going on is that normally – when run with a server – the view will call job_posts and the message will land on the helper method on the controller, which is the expected behaviour.
Here, however, expect is setting an expectation and, at the same time, stubbing a method on the view with a fixed return value. Since the view template will call that method, the test passes.
About that unexpected "stub" side effect of expect, I've found this in the rspec-mocks readme:
(...) We can also set a message expectation so that the example fails if find is not called:
person = double("person")
expect(Person).to receive(:find) { person }
RSpec replaces the method we're stubbing or mocking with its own test-double-like method. At the end of the example, RSpec verifies any message expectations, and then restores the original methods.
Does anyone have any experience with this specific use of the method?
Well, that's what expect().to receive() does! This is the (not so) new expectation syntax of rspec, which replaces the should_receive API
expect(view).to receive(:job_post).at_least(:once).and_return(job_post)
is equivalent to
view.should_receive(:job_post).at_least(:once).and_return(job_post)
and this API sets the expectation and the return value. This is the default behavior. To actually call the original method as well, you need to explicitly say so:
view.should_receive(:job_post).at_least(:once).and_call_original
On to some other issues:
(yes, they could have used #instance variables, and I'm in the process of refactoring it).
let API is very ubiquitous in rspec testing, and may be better than #instance variables in many cases (for example - it is lazy, so it runs only if needed, and it is memoized, so it runs at most once).
In fact, replacing expect with allow seems to have exactly the same effect.
The allow syntax replaces the stub method in the old rspec syntax, so yes, it has the same effect, but the difference is, that it won't fail the test if the stubbed method is not called.
As the OP requested - some explanations about should_receive - unit tests are expected to run in isolation. This means that everything which is not directly part of your test, should not be tested. This means that HTTP calls, IO reads, external services, other modules, etc. are not part of the test, and for the purpose of the test, you should assume that they work correctly.
What you should include in your tests is that those HTTP calls, IO reads, and external services are called correctly. To do that, you set message expectations - you expect the tested method to call a certain method (whose actual functionality is out of the scope of the test). So you expect the service to receive a method call, with the correct arguments, one or more times (you can explicitly expect how many times it should be called), and, in exchange for it actually being called, you stub it, and according to the test, set its return value.
Sources:
Message expectation
RSpec's New Expectation Syntax
Rspec is a meta-gem, which depends on the rspec-core, rspec-expectations and rspec-mocks gems.
Rspec-mocks is a test-double framework for rspec with support for method stubs, fakes, and message expectations on generated test-doubles and real objects alike.
allow().to receive
is the use of 'Method Stubs', however
expect().to receive
is the use of 'Message Expectations'
You can refer to the Doc for more details
If you don't want to stub as a side affect, you can always call the original.
https://relishapp.com/rspec/rspec-mocks/v/2-14/docs/message-expectations/calling-the-original-method
For example I once wanted to spy on a method, but also call the function else it has other side affects. That really helped.
https://relishapp.com/rspec/rspec-mocks/v/2-14/docs/message-expectations/calling-the-original-method
I have this code:
def self.generate_random_uniq_code
code = sprintf("%06d", SecureRandom.random_number(999999))
code = self.generate_random_uniq_code if self.where(code: code).count > 0
code
end
The goal is create random codes for a new register, the code can't exist already in the registers
I'm trying test this way, but when I mock the SecureRandom it always return the same value:
it "code is unique" do
old_code = Code.new
old_code.code = 111111
new_code = Code.new
expect(SecureRandom).to receive(:random_number) {old_code.code}
new_code.code = Code.generate_random_uniq_code
expect(new_code.code).to_not eq old_code.code
end
I was trying to find if there is a way to enable and disable the mock behavior, but I could not find it, I'm not sure I'm doing the test the right way, the code seems no work fine to me.
Any help is welcome, thanks!
TL;DR
Generally, unless you are actually testing a PRNG that you wrote, you're probably testing the wrong behavior. Consider what behavior you're actually trying to test, and examine your alternatives. In addition, a six-digit number doesn't really have enough of a key space to ensure real randomness for most purposes, so you may want to consider something more robust.
Some Alternatives
One should always test behavior, rather than implementation. Here are some alternatives to consider:
Use a UUID instead of a six-digit number. The UUID is statistically less likely to encounter collisions than your current solution.
Enforce uniqueness in your database column by adjusting the schema.
Using a Rails uniqueness validator in your model.
Use FactoryGirl sequences or lambdas to return values for your test.
Fix Your Spec
If you really insist on testing this piece of code, you should at least use the correct expectations. For example:
# This won't do anything useful, if it even runs.
expect(new_code.code).to_not old_code.code
Instead, you should check for equality, with something like this:
old_code = 111111
new_code = Code.generate_random_uniq_code
new_code.should_not eq old_code
Your code may be broken in other ways (e.g. the code variable in your method doesn't seem to be an instance or class variable) so I won't guarantee that the above will work, but it should at least point you in the right direction.