Given a standard has_many relationship between two objects. For a simple example, let's go with:
class Order < ActiveRecord::Base
has_many :line_items
end
class LineItem < ActiveRecord::Base
belongs_to :order
end
What I'd like to do is generate a stubbed order with a list of stubbed line items.
FactoryGirl.define do
factory :line_item do
name 'An Item'
quantity 1
end
end
FactoryGirl.define do
factory :order do
ignore do
line_items_count 1
end
after(:stub) do |order, evaluator|
order.line_items = build_stubbed_list(:line_item, evaluator.line_items_count, :order => order)
end
end
end
The above code does not work because Rails wants to call save on the order when line_items is assigned and FactoryGirl raises an exception:
RuntimeError: stubbed models are not allowed to access the database
So how do you (or is it possible) to generate an stubbed object where it's has_may collection is also stubbed?
TL;DR
FactoryGirl tries to be helpful by making a very large assumption when it
creates it's "stub" objects. Namely, that:
you have an id, which means you are not a new record, and thus are already persisted!
Unfortunately, ActiveRecord uses this to decide if it should
keep persistence up to date.
So the stubbed model attempts to persist the records to the database.
Please do not try to shim RSpec stubs / mocks into FactoryGirl factories.
Doing so mixes two different stubbing philosophies on the same object. Pick
one or the other.
RSpec mocks are only supposed to be used during certain parts of the spec
life cycle. Moving them into the factory sets up an environment which will
hide the violation of the design. Errors which result from this will be
confusing and difficult to track down.
If you look at the documentation for including RSpec into say
test/unit,
you can see that it provides methods for ensuring that the mocks are properly
setup and torn down between the tests. Putting the mocks into the factories
provides no such guarantee that this will take place.
There are several options here:
Don't use FactoryGirl for creating your stubs; use a stubbing library
(rspec-mocks, minitest/mocks, mocha, flexmock, rr, or etc)
If you want to keep your model attribute logic in FactoryGirl that's fine.
Use it for that purpose and create the stub elsewhere:
stub_data = attributes_for(:order)
stub_data[:line_items] = Array.new(5){
double(LineItem, attributes_for(:line_item))
}
order_stub = double(Order, stub_data)
Yes, you do have to manually create the associations. This is not a bad thing,
see below for further discussion.
Clear the id field
after(:stub) do |order, evaluator|
order.id = nil
order.line_items = build_stubbed_list(
:line_item,
evaluator.line_items_count,
order: order
)
end
Create your own definition of new_record?
factory :order do
ignore do
line_items_count 1
new_record true
end
after(:stub) do |order, evaluator|
order.define_singleton_method(:new_record?) do
evaluator.new_record
end
order.line_items = build_stubbed_list(
:line_item,
evaluator.line_items_count,
order: order
)
end
end
What's Going On Here?
IMO, it's generally not a good idea to attempt to create a "stubbed" has_many
association with FactoryGirl. This tends to lead to more tightly coupled code
and potentially many nested objects being needlessly created.
To understand this position, and what is going on with FactoryGirl, we need to
take a look at a few things:
The database persistence layer / gem (i.e. ActiveRecord, Mongoid,
DataMapper, ROM, etc)
Any stubbing / mocking libraries (mintest/mocks, rspec, mocha, etc)
The purpose mocks / stubs serve
The Database Persistence Layer
Each database persistence layer behaves differently. In fact, many behave
differently between major versions. FactoryGirl tries to not make assumptions
about how that layer is setup. This gives them the most flexibility over the
long haul.
Assumption: I'm guessing you are using ActiveRecord for the remainder of
this discussion.
As of my writing this, the current GA version of ActiveRecord is 4.1.0. When
you setup a has_many association on it,
there's
a
lot
that
goes
on.
This is also slightly different in older AR versions. It's very different in
Mongoid, etc. It's not reasonable to expect FactoryGirl to understand the
intricacies of all of these gems, nor differences between versions. It just so
happens that the has_many association's writer
attempts to keep persistence up to date.
You may be thinking: "but I can set the inverse with a stub"
FactoryGirl.define do
factory :line_item do
association :order, factory: :order, strategy: :stub
end
end
li = build_stubbed(:line_item)
Yep, that's true. Though it's simply because AR decided not to
persist.
It turns out this behavior is a good thing. Otherwise, it would be very
difficult to setup temp objects without hitting the database frequently.
Additionally, it allows for multiple objects to be saved in a single
transaction, rolling back the whole transaction if there was a problem.
Now, you may be thinking: "I totally can add objects to a has_many without
hitting the database"
order = Order.new
li = order.line_items.build(name: 'test')
puts LineItem.count # => 0
puts Order.count # => 0
puts order.line_items.size # => 1
li = LineItem.new(name: 'bar')
order.line_items << li
puts LineItem.count # => 0
puts Order.count # => 0
puts order.line_items.size # => 2
li = LineItem.new(name: 'foo')
order.line_items.concat(li)
puts LineItem.count # => 0
puts Order.count # => 0
puts order.line_items.size # => 3
order = Order.new
order.line_items = Array.new(5){ |n| LineItem.new(name: "test#{n}") }
puts LineItem.count # => 0
puts Order.count # => 0
puts order.line_items.size # => 5
Yep, but here order.line_items is really an
ActiveRecord::Associations::CollectionProxy.
It defines it's own build,
#<<,
and #concat
methods. Of, course these really all delegate back to the association defined,
which for has_many are the equivalent methods:
ActiveRecord::Associations::CollectionAssocation#build
and ActiveRecord::Associations::CollectionAssocation#concat.
These take into account the current state of the base model instance in order
to decide whether to persist now or later.
All FactoryGirl can really do here is let the behavior of the underlying class
define what should happen. In fact, this lets you use FactoryGirl to
generate any class, not
just database models.
FactoryGirl does attempt to help a little with saving objects. This is mostly
on the create side of the factories. Per their wiki page on
interaction with ActiveRecord:
...[a factory] saves associations first so that foreign keys will be properly
set on dependent models. To create an instance, it calls new without any
arguments, assigns each attribute (including associations), and then calls
save!. factory_girl doesn’t do anything special to create ActiveRecord
instances. It doesn’t interact with the database or extend ActiveRecord or
your models in any way.
Wait! You may have noticed, in the example above I slipped the following:
order = Order.new
order.line_items = Array.new(5){ |n| LineItem.new(name: "test#{n}") }
puts LineItem.count # => 0
puts Order.count # => 0
puts order.line_items.size # => 5
Yep, that's right. We can set order.line_items= to an array and it isn't
persisted! So what gives?
The Stubbing / Mocking Libraries
There are many different types and FactoryGirl works with them all. Why?
Because FactoryGirl doesn't do anything with any of them. It's completely
unaware of which library you have.
Remember, you add the FactoryGirl syntax to your test library of choice.
You don't add your library to FactoryGirl.
So if FactoryGirl isn't using your preferred library, what is it doing?
The Purpose Mocks / Stubs Serve
Before we get to the under the hood details, we need to define what
a
"stub"
is
and its intended purpose:
Stubs provide canned answers to calls made during the test, usually not
responding at all to anything outside what's programmed in for the test.
Stubs may also record information about calls, such as an email gateway stub
that remembers the messages it 'sent', or maybe only how many messages it
'sent'.
this is subtly different from a "mock":
Mocks...: objects pre-programmed with expectations which form a
specification of the calls they are expected to receive.
Stubs serve as a way to setup collaborators with canned responses. Sticking to
only the collaborators public API which you touch for the specific test keeps
stubs lightweight and small.
Without any "stubbing" library, you can easily create your own stubs:
stubbed_object = Object.new
stubbed_object.define_singleton_method(:name) { 'Stubbly' }
stubbed_object.define_singleton_method(:quantity) { 123 }
stubbed_object.name # => 'Stubbly'
stubbed_object.quantity # => 123
Since FactoryGirl is completely library agnostic when it comes to their
"stubs", this is the approach they take.
Looking at the FactoryGirl v.4.4.0 implementation, we can see that the
following methods are all stubbed when you build_stubbed:
persisted?
new_record?
save
destroy
connection
reload
update_attribute
update_column
created_at
These are all very ActiveRecord-y. However, as you have seen with has_many,
it is a fairly leaky abstraction. The ActiveRecord public API surface area is
very large. It's not exactly reasonable to expect a library to fully cover it.
Why does the has_many association not work with the FactoryGirl stub?
As noted above, ActiveRecord checks it's state to decide if it should
keep persistence up to date.
Due to the stubbed definition of new_record?
setting any has_many will trigger a database action.
def new_record?
id.nil?
end
Before I throw out some fixes, I want to go back to the definition of a stub:
Stubs provide canned answers to calls made during the test, usually not
responding at all to anything outside what's programmed in for the test.
Stubs may also record information about calls, such as an email gateway stub
that remembers the messages it 'sent', or maybe only how many messages it
'sent'.
The FactoryGirl implementation of a stub violates this tenet. Since it has no
idea what you are going to be doing in your test/spec, it simply tries to
prevent database access.
Fix #1: Do Not Use FactoryGirl to Create Stubs
If you wish to create / use stubs, use a library dedicated to that task. Since
it seems you are already using RSpec, use it's double feature (and the new verifying
instance_double,
class_double,
as well as object_double
in RSpec 3). Or
use Mocha, Flexmock, RR, or anything else.
You can even roll your own super simple stub factory (yes there are issues with
this, it's simply an example of an easy way to make an object with canned
responses):
require 'ostruct'
def create_stub(stubbed_attributes)
OpenStruct.new(stubbed_attributes)
end
FactoryGirl makes it very easy to create 100 model objects when really you
needed 1. Sure, this is a responsible usage issue; as always great power comes
create responsibility. It's just very easy to overlook deeply nested
associations, which don't really belong in a stub.
Additionally, as you have noticed, FactoryGirl's "stub" abstraction is a bit
leaky forcing you to understand both its implementation and your database
persistence layer's internals. Using a stubbing lib should completely free you
from having this dependency.
If you want to keep your model attribute logic in FactoryGirl that's fine.
Use it for that purpose and create the stub elsewhere:
stub_data = attributes_for(:order)
stub_data[:line_items] = Array.new(5){
double(LineItem, attributes_for(:line_item))
}
order_stub = double(Order, stub_data)
Yes, you do have to manually setup the associations. Though you only setup
those associations which you need for the test/spec. You don't get the 5 other
ones that you do not need.
This is one thing that having a real stubbing lib helps make explicitly clear.
This is your tests/specs giving you feedback on your design choices. With a
setup like this, a reader of the spec can ask the question: "Why do we need 5
line items?" If it's important to the spec, great it's right there up front
and obvious. Otherwise, it shouldn't be there.
The same thing goes for those a long chain of methods called a single object,
or a chain of methods on subsequent objects, it's probably time to stop. The
law of demeter is there to help
you, not hinder you.
Fix #2: Clear the id field
This is more of a hack. We know that the default stub sets an id. Thus, we
simply remove it.
after(:stub) do |order, evaluator|
order.id = nil
order.line_items = build_stubbed_list(
:line_item,
evaluator.line_items_count,
order: order
)
end
We can never have a stub which returns an id AND sets up a has_many
association. The definition of new_record? that FactoryGirl setup completely
prevents this.
Fix #3: Create your own definition of new_record?
Here, we separate the concept of an id from where the stub is a
new_record?. We push this into a module so we can re-use it in other places.
module SettableNewRecord
def new_record?
#new_record
end
def new_record=(state)
#new_record = !!state
end
end
factory :order do
ignore do
line_items_count 1
new_record true
end
after(:stub) do |order, evaluator|
order.singleton_class.prepend(SettableNewRecord)
order.new_record = evaluator.new_record
order.line_items = build_stubbed_list(
:line_item,
evaluator.line_items_count,
order: order
)
end
end
We still have to manually add it for each model.
I've seen this answer floating around, but ran into the same problem you had:
FactoryGirl: Populate a has many relation preserving build strategy
The cleanest way that I've found is to explicitly stub out the association calls as well.
require 'rspec/mocks/standalone'
FactoryGirl.define do
factory :order do
ignore do
line_items_count 1
end
after(:stub) do |order, evaluator|
order.stub(line_items).and_return(FactoryGirl.build_stubbed_list(:line_item, evaluator.line_items_count, :order => order))
end
end
end
Hope that helps!
I found the solution of Bryce to be the most elegant but it produces a deprecation warning about the new allow() syntax.
In order to use the new (cleaner) syntax I did this :
UPDATE 06/05/2014 : my first proposition was using a private api method, thanks to Aaraon K for a much nicer solution, please read the comment for further discussion
#spec/support/initializers/factory_girl.rb
...
#this line enables us to use allow() method in factories
FactoryGirl::SyntaxRunner.include(RSpec::Mocks::ExampleMethods)
...
#spec/factories/order_factory.rb
...
FactoryGirl.define do
factory :order do
ignore do
line_items_count 1
end
after(:stub) do |order, evaluator|
items = FactoryGirl.build_stubbed_list(:line_item, evaluator.line_items_count, :order => order)
allow(order).to receive(:line_items).and_return(items)
end
end
end
...
Related
In Rails models we usually have attributes and relations tests, like:
describe 'attributes' do
it { is_expected.to have_db_column(:identifier).of_type(:uuid) }
it { is_expected.to have_db_column(:content).of_type(:jsonb) }
it { is_expected.to have_db_column(:created_at).of_type(:datetime) }
end
describe 'relations' do
it { is_expected.to belong_to(:user).class_name('User') }
end
And using a TDD style it seems to be some useful tests, however I have been dwelling if these are really necessary tests, and I would like to know if there is some common knowledge about it, is it good practice to create these tests? or are we just testing rails?
Amongst the purposes of a unit test are...
Does it work?
Does it still work?
If it's a promise, if other things rely on it, you should test it to ensure you keep that promise. This is regression testing.
But don't test more than you promise. You'll be stuck with it, or your code will break when you make an internal change.
For example...
it { is_expected.to have_db_column(:identifier).of_type(:uuid) }
This promises that it has a column called identifier which is a UUID. Usually you don't promise all that detail; it is glass-box testing and it makes your test brittle.
Instead, promise as little as you can. Its ID is a UUID. This is black-box testing.
require "rspec/uuid"
describe '#id' do
subject { thing.id }
let(:thing) { create(:thing) }
it 'has a uuid ID' do
expect(thing.id).to be_a_uuid
end
end
It's possible there is an even higher level way to express this without holding yourself specifically to a UUID.
it { is_expected.to have_db_column(:content).of_type(:jsonb) }
Similarly, don't promise it has a jsonb column. That is blackbox testing. Promise that you can store complex data structures.
describe '#content' do
subject { create(:thing) }
it 'can round trip complex data' do
data = [1, { two: 3, four: [5] }]
thing.update!(content: data)
# Force it to re-load content from the database.
thing.reload
expect(thing.content).to eq data
end
end
it { is_expected.to belong_to(:user).class_name('User') }
Instead of promising what it belongs to, promise the relationship.
describe '#user' do
let(:thing) { create(:thing) }
let(:user) { create(:user) }
before {
user.things << thing
}
it 'belongs to a user' do
expect(thing.user).to eq user
expect(user.things).to contain(thing)
end
end
I have answered a nearly identical question here: https://stackoverflow.com/a/74195850/14837782
In summary: If it is end-developer code, I believe it should be tested. If it can be fat-fingered, I believe it should be tested. If you're going to remove it deliberately, I also believe you should have to remove a test deliberately as well. If it can fail, there should be a specific test for that failure mode.
This is not to be confused with testing the Rails framework. You obviously want to design your tests so that you're not testing Rails itself or Rails implementation, only your own code.
Attributes should be tested. Here is how I do it in minitest:
test/models/car_test.rb
class CarTest < ActiveSupport::TestCase
###################################################################
#
# Attributes
#
###################################################################
test 'describe some attr_reader fields' do
expected = [:year, :make, :model, :vin]
assert_has_attr_readers(Car, expected)
end
###############################################
test 'describe some attr_writer fields' do
expected = [:infotainment_fimrware_version]
assert_has_attr_writers(Car, expected)
end
###############################################
test 'describe some attr_accessor fields' do
expected = [:owner, :color, :mileage]
assert_has_attr_readers(Car, expected)
assert_has_attr_writers(Car, expected)
end
end
test/test_helpers/attributes_helper.rb
# frozen_string_literal: true
module AttributesHelper
###################################################################
#
# Assertions
#
###################################################################
#
# Performs an assertion that the given class contains reader/getter methods for the given attribute names.
# This helper checks for the existence of `attribute_name` methods on the class, and does not concern itself
# with how those methods are declared: directly defined, attr_reader, attr_accessor, etc.
#
def assert_has_attr_readers(klass, attribute_names)
# Get public and protected method names, passing `false` to exclude methods from super classes.
actual_method_names = klass.instance_methods(false).map(&:to_s)
attribute_names.each do |attribute|
message = "Expected class #{klass.name} to contain a reader for attribute #{attribute}"
assert_includes(actual_method_names, attribute.to_s, message)
end
end
#
# Performs an assertion that the given class contains writer/setter methods for the given attribute names.
# This helper checks for the existence of `attribute_name=` methods on the class, and does not concern itself
# with how those methods are declared: directly defined, attr_writer, attr_accessor, etc.
#
def assert_has_attr_writers(klass, attribute_names)
# Get public and protected method names, passing `false` to exclude methods from super classes.
actual_method_names = klass.instance_methods(false).map(&:to_s)
attribute_names.each do |attribute|
message = "Expected class #{klass.name} to contain a writer for attribute #{attribute}"
assert_includes(actual_method_names, "#{attribute}=", message)
end
end
#
# Performs an assertion that the given class implements attr_encrypted for the given attribute names.
# This helper is tied to the implementation details of the attr_encrypted gem. Changes to how attributes
# are encrypted will need to be accounted for here.
#
def assert_has_encrypted_attrs(klass, attribute_names)
message = "Expected class #{klass.name} to encrypt specific attributes"
actual_attributes = klass.encrypted_attributes.keys
assert_equal(attribute_names.map(&:to_s).sort, actual_attributes.map(&:to_s).sort, message)
end
end
Your example tests seem to be testing the existence of DB fields, not getter/setter model attributes. Database fields are impossible to fat-finger (they require a migration to modify) so if that's what you're talking about, I do not believe it makes sense to test them. (And I personally believe it is useful to test nearly everything.)
Although I guess in the case where the DB is accessible by other applications and could potentially be modified outside of a single application then it could make sense to test for the existence of those fields as well, as pointed out by Dave Newton in a comment below.
Ultimately it is up to you, and if your one application is the only one with access to the DB but you still want to test DB field existence and settings, maybe a 3rd option is some sort of migration test that you're looking for to make sure the migration is written properly. I've not written anything like that yet, but it might be feasible. I would hate to try to write one, and it does seem to go too far, but it's an idea...
I'm using Rails 5. I have a model that looks like this
class CryptoIndexCurrency < ApplicationRecord
belongs_to :crypto_currency
end
I have a service method where I want to populate this table with records, which I do like so
CryptoIndexCurrency.delete_all
currencies.each do |currency|
cindex_currency = CryptoIndexCurrency.new({:crypto_currency => currency})
cindex_currency.save
end
The problem is the above is not very transactional, in as far as if something happens after the first statement, the "delete_all" will have executed but nothing else will have. What is the proper way to create a transaction here and equally as important, where do I place that code? Would like to know the Rails convention here.
I think you can just do:
CryptoIndexCurrency.transaction do
CryptoIndexCurrency.delete_all
CryptoIndexCurrency.create(currencies.map{ |c| {crypto_currency: c} })
end
If you are using Activerecord you can use the builtin transaction mechanism. Otherwise, one way would be to make sure you validate all your data and only save when everything is valid. Take a look at validates_associate and the like.
That said, if your process is inherently non validatable/nondeterministic (eg. you call external APIs to validate a payment) then the best is to ensure you have some cleaning methods that take care of your failure
If you have deterministic failures:
def new_currencies_valid?(currencies)
currencies.each do
return false if not currency.valid?(:create)
end
true
end
if new_currencies_valid?(new_currencies)
Currency.delete_all # See note
new_currencies.each(&:save)
end
A sidenote : unless you really understand what you are doing, I suggest calling destroy_all which runs callbacks on deletion (such as deleting dependent: :destroy) associations
I have two models, User and Account.
# account.rb
belongs_to :user
# user.rb
has_one :account
Account has an attribute name. And in my views, I was calling current_user.account.name multiple times, and I heard that's not the great of a way to do it. So I was incredibly swift, and I created the following method in my user.rb
def account_name
self.account.name
end
So now in my view, I can simply call current_user.account_name, and if the association changes, I only update it in one place. BUT my question is, do I test this method? If I do, how do I test it without any mystery guests?
I agree there is nothing wrong with current_user.account.name - while Sandi Metz would tell us "User knows too much about Account" this is kind of the thing you can't really avoid w/ Active Record.
If you found you were doing a lot of these methods all over the User model you could use the rails delegate method:
delegate :name, :to => :account, :prefix => true
using the :prefix => true option will prefix the method in the User model so it is account_name. In this case I would assume you could write a very simple unit test on the method that it returns something just incase the attribute in account would ever change your test would fail so you would know you need to update the delegate method.
There's nothing wrong with current_user.account.name
There's no difference between calling it as current_user.account.name, or making current_user.account_name call it for you
You're probably not calling current_user in the model, like you say
You should have a spec for it if you use it
Personally I see no good reason for any of this. Just use current_user.account.name.
If you are worrying about efficiency, have current_user return a user that joins account.
This is going to be a bit off-topic. So, apologies in advance if it's not interesting or helpful.
TL;DR: Don't put knowledge of your models in your views. Keep your controllers skinny. Here's how I've been doing it.
In my current project, I've been working to make sure my views have absolutely no knowledge of anything about the rest of the system (to reduce coupling). This way, if you decide to change how you implement something (say, current_user.account.name versus current_user.account_name), then you don't have to go into your views and make changes.
Every controller action provides a #results hash that contains everything the view needs to render correctly. The structure of the #results hash is essentially a contract between the view and the controller.
So, in my controller, #results might look something like {current_user: {account: {name: 'foo'}}}. And in my view, I'd do something like #results[:current_user][:account][:name]. I like using a HashWithIndifferentAccess so I could also do #results['current_user']['account']['name'] and not have things blow up or misbehave.
Also, I've been moving as much logic as I can out of controllers into service objects (I call them 'managers'). I find my managers (which are POROs) a lot easier to test than controllers. So, I might have:
# app/controllers/some_controller.rb
class SomeController
def create
#results = SomeManager.create(params)
if #results[:success]
# happy routing
else
# sad routing
end
end
end
Now, my controllers are super skinny and contain no logic other than routing. They don't know anything about my models. (In fact, almost all of my controller actions look exactly the same with essentially the same six lines of code.) Again, I like this because it creates separation.
Naturally, I need the manager:
#app/managers/some_manager.rb
class SomeManager
class << self
def create(params)
# do stuff that ends up creating the #results hash
# if things went well, the return will include success: true
# if things did not go well, the return will not include a :success key
end
end
end
So, in truth, the structure of #results is a contract between the view and the manager, not between the view and the controller.
I'm trying to decide how to test a method that simply calculates an average of values on associated records. I'm concerned about testing the implementation vs the actual result returned.
Say I have the following models...
class User
has_many :interviews
def interview_grade
interviews.average(:score).round unless interviews.empty?
end
end
class Interview
belongs_to :user
end
And in user_spec.rb I have...
describe "interview_grade" do
let(:user) {User.new}
context "when the user has interviews" do
before { user.stub_chain(:interviews, :empty?){false} }
it "should return an average of the appraisal ratings" do
user.interviews.should_receive(:average).with(:score).and_return(3.2)
user.work_history_grade.should == 3
end
end
context "when the user has no interviews" do
before {Interview.destroy_all}
it "should return nil" do
user.interview_grade.should be_nil
end
end
end
These tests pass but it feels fragile to me. What if interview_grade should actually calculate the sum of the scores (for example). As I'm just testing that a particular chain of methods is called, this passing test wouldn't tell me that the result is actually incorrect.
I have tried stubbing user.interviews in order to setup the available scores for the test to work with but this seems tricky to do in Rails 3 due to the way associations are lazy loaded. i.e. I can't just create an array of Interview objects because it doesn't respond to the average method.
Any advice greatly appreciated.
Coming back to this 3 years later. I would would approach it entirely differently.
The benefit of the code below is that in order to write tests for InterviewGrader I would no longer need to worry about how the scores are attained.
I just give it the scores and test it gives me the correct output.
Also I would never need to worry about the underlying implementation of InterviewGrader. However, if the logic was changed at a later date, the tests would fail.
The new scores method on User would need to be tested separately.
class InterviewGrader
def self.run scores
new(scores).run
end
attr_reader :scores
def initialize(scores)
#scores = scores
end
def run
scores.inject { |sum, score|
sum + score
}.to_f / number_of_scores
end
private
def number_of_scores
scores.length
end
end
class User
has_many :interviews
def scores
interviews.map(&:score)
end
def interview_grade
InterviewGrader.run(scores)
end
end
class Interview
belongs_to :user
end
This is incorrect usage of stubbing and mocking.
In this case you should only test, that interview_grade works, when average returns nil (and this is only case interviews.empty? is used).
The average method is tested by rails itself. round method by ruby tests (i guess). So you not need to test this methods. This is a general idea to test only your own code.
And if you want to test, how interview_grade is calculated, you should create test data (with fixtures or factories). Because you should test separate (in some case) part of system, and in this case separation is wrong: interviews.average and interviews.empty? are dependent in your code, but in spec they independent.
def interview_grade
interviews.average(:score).try(:round)
end
If you rewrite your method in this way, you no need in stubbing and mocking
I have a callback on my ActiveRecord model as shown below:
before_save :sync_to_external_apis
def sync_to_external_apis
[user, assoc_user].each {|cuser|
if cuser.google_refresh
display_user = other_user(cuser.id)
api = Google.new(:user => cuser)
contact = api.sync_user(display_user)
end
}
end
I would like to write an rspec test which tests that calling save! on an instance of this model causes sync_user to be called on a new Google instance when google_refresh is true. How could I do this?
it "should sync to external apis on save!" do
model = Model.new
model.expects(:sync_to_external_apis)
model.save!
end
As an aside, requesting unreliable resources like the internet during the request-response cycle is a bad idea. I would suggest creating a background job instead.
The usual method for testing is to ensure the results are as expected. Since you're using an API in this case that may complicate things. You may find that using mocha to create a mock object you can send API calls would allow you to substitute the Google class with something that works just as well for testing purposes.
A simpler, yet clunkier approach is to have a "test mode" switch:
def sync_to_external_apis
[ user, assoc_user ].each do |cuser|
if (Rails.env.test?)
#synced_users ||= [ ]
#synced_users << cuser
else
# ...
end
end
end
def did_sync_user?(cuser)
#synced_users and #synced_users.include?(cuser)
end
This is a straightforward approach, but it will not validate that your API calls are being made correctly.
Mocha is the way to go. I'm not familiar with rspec, but this is how you would do it in test unit:
def test_google_api_gets_called_for_user_and_accoc_user
user = mock('User') # define a mock object and label it 'User'
accoc_user = mock('AssocUser') # define a mock object and label it 'AssocUser'
# instantiate the model you're testing with the mock objects
model = Model.new(user, assoc_user)
# stub out the other_user method. It will return cuser1 when the mock user is
# passed in and cuser2 when the mock assoc_user is passed in
cuser1 = mock('Cuser1')
cuser2 = mock('Cuser2')
model.expects(:other_user).with(user).returns(cuser1)
model.expects(:other_user).with(assoc_user).returns(cuser2)
# set the expectations on the Google API
api1 - mock('GoogleApiUser1') # define a mock object and lable it 'GoogleApiUser1'
api2 - mock('GoogleApiUser2') # define a mock object and lable it 'GoogleApiUser2'
# call new on Google passing in the mock user and getting a mock Google api object back
Google.expects(:new).with(:user => cuser1).returns(api1)
api1.expects(:sync_user).with(cuser1)
Google.expects(:new).with(:user => cuser2).returns(api2)
api2.expects(:sync_user).with(cuser2)
# now execute the code which should satisfy all the expectations above
model.save!
end
The above may seem complicated, but it's not once you get the hang of it. You're testing that when you call save, your model does what it is supposed to do, but you don't have the hassle, or time expense of really talking to APIs, instantiating database records, etc.