Testing such a scope method

100 views
Skip to first unread message

Javix

unread,
Nov 13, 2012, 3:46:02 AM11/13/12
to rs...@googlegroups.com
I can't figure out how to test a scope method (highlighted in bold)  which result includes data from 3 different tables:

class Account < ActiveRecord::Base
  attr_accessible :acc_number, :client_id
  belongs_to :client
  has_many :operations, dependent: :destroy
  scope :operations_by_client, joins(:client, :operations).select('clients.firstname, clients.lastname, sum(operations.total) as total').group('clients.id, clients.firstname, clients.lastname').order('clients.lastname')
end


class Client < ActiveRecord::Base
has_many: accounts
...
end


class Operation < ActiveRecord::Base
belongs_to :account
...
end

I get an array of Account objects as result (I hope so), so have no idea how to use 'assigns' or smth other in controller spec for #index page:


class OperationsController < ApplicationController

def index
   @operations = Account.operations_by_client.paginate(page: params[:page])
  end
end

the same is for model spec, how is it possible to test the scope method? Thanks

José Mota

unread,
Nov 13, 2012, 5:34:53 AM11/13/12
to rs...@googlegroups.com
Being no expert on Rails testing but perhaps I'd give a suggestion. When something is hard to test, then perhaps you should extract that something into an isolated scenario.

I personally would create a separate class for something like this, an `OperationFetcher` class of some sort.

* It would take the account's id and the client's id, I believe those are the dependencies.
* It is a result of data processing with the `select` and `order` commands.

Creating a class for this, IMO, is easier to test. No controller to mess you around. You would have something like this:

describe OperationFetcher do
  before do
    // Create necessary account, client and operations here.
    // Not sure what a convenient replacement of FactoryGirl could go here
  end
  
  let(:account_id) { 1 }
  let(:client_id) { 2 }
  subject { OperationFetcher.new account_id, client_id }

  it "fetches operations by client" do
    // your code here
  end
end

This was not tested. But it should be a starting point. When you find something really hard to test, then the design is probably not helping you. HTH!

-- 
José Mota
--
You received this message because you are subscribed to the Google Groups "rspec" group.
To post to this group, send email to rs...@googlegroups.com.
To unsubscribe from this group, send email to rspec+un...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msg/rspec/-/pWkCaWOzo2cJ.
For more options, visit https://groups.google.com/groups/opt_out.
 
 

Alex Chaffee

unread,
Nov 13, 2012, 11:54:56 AM11/13/12
to rs...@googlegroups.com

A scope is a method that returns an array (basically). So create some test objects in the db (in all 3 tables), then call the method, and assert that the result contains what it should contain. 

With such a complicated query, you may need several tests (examples) to cover all the possible scenarios. 

Javix

unread,
Nov 13, 2012, 3:25:12 PM11/13/12
to rs...@googlegroups.com
The quesion is not about the dificulty level, I knew that. I asked about the way to test the above sope method. So when calling it in the console, of course I'm getting an array, here is:

ruby-1.9.3-p0 :001 > operations = Account.operations_by_client
  Account Load (26.8ms)  SELECT clients.firstname, clients.lastname, sum(operations.total) as total FROM "accounts" INNER JOIN "clients" ON "clients"."id" = "accounts"."client_id" INNER JOIN "operations" ON "operations"."account_id" = "accounts"."id" GROUP BY clients.id, clients.firstname, clients.lastname ORDER BY clients.lastname
 => [#<Account >, #<Account >, #<Account >, #<Account >, #<Account >, #<Account >, #<Account >, #<Account >, #<Account >, #<Account >, #<Account >, #<Account >, #<Account >, #<Account >, #<Account >, #<Account >, #<Account >, #<Account >, #<Account >, #<Account >, #<Account >, #<Account >, #<Account >, #<Account >, #<Account >, #<Account >, #<Account >, #<Account >, #<Account >, #<Account >, #<Account >, #<Account >, #<Account >, #<Account >, #<Account >]
ruby-1.9.3-p0 :002 >

As you see, it is not a 'normal' array which we usually could test like that:

describe 'GET #index' do
  it "displays an array of all the operations by client" do
    account = create(:operation) #will NOT work because the scope method result is not an array of Account objects
    get :index
    assigns(:operations).should == [account]
  end

  it "renders the :index view" do
    get :index
    response.should render_template :index
  end
end

   
end

Regards

Javix

unread,
Nov 13, 2012, 3:46:37 PM11/13/12
to rs...@googlegroups.com
The result array contains ActiveRecord::Relation objects as explained in Rails API:

named scopes act like an Array, implementing Enumerable; Shirt.red.each(&block), Shirt.red.first, and Shirt.red.inject(memo, &block) all behave as if Shirt.red really was an Array.

So is there a way to either test them or transform into AR objects to test?

Alex Chaffee

unread,
Nov 13, 2012, 3:50:55 PM11/13/12
to rs...@googlegroups.com
Sure. They automatically convert to or act as arrays in many cases, or
you can explicitly convert them using to_a (or all). Try it and see!

Also, a scope is part of a model, so you should probably be writing a
unit test (account_spec.rb), not a controller test.

Good luck!

see also http://guides.rubyonrails.org/active_record_querying.html#scopes

--
Alex Chaffee - al...@stinky.com
http://alexchaffee.com
http://twitter.com/alexch

David Chelimsky

unread,
Nov 13, 2012, 3:59:08 PM11/13/12
to rs...@googlegroups.com
In cases like this you can specify ids (or more data sufficient to make the point), e.g.

account = create(:operation)
Account.operations_by_client.map(&:id).should eq [account.id]

That make sense?


To view this discussion on the web visit https://groups.google.com/d/msg/rspec/-/PUZR--nnFHAJ.

Javix

unread,
Nov 14, 2012, 10:38:56 AM11/14/12
to rs...@googlegroups.com
@dchel...@gmail.com:
Thank you for the idea, unfortunately it will not work, - as you can see, I have only 'firstname, lastname and total' that I can call on every element of the array.

array = Account.operations_by_client

  Account Load (0.0ms)  SELECT clients.firstname, clients.lastname, sum(operations.total) as total FROM "accounts" INNER JOIN "clients" ON "

clients"."id" = "accounts"."client_id" INNER JOIN "operations" ON "operations"."account_id" = "accounts"."id" GROUP BY clients.id, clients.f
irstname, clients.lastname ORDER BY clients.lastname
=> [#<Account >, #<Account >, #<Account >, #<Account >, #<Account >, #<Account >, #<Account >, #<Account >, #<Account >, #<Account >, #<Acco
unt >, #<Account >, #<Account >, #<Account >, #<Account >, #<Account >, #<Account >, #<Account >, #<Account >, #<Account >, #<Account >, #<A
ccount >, #<Account >, #<Account >, #<Account >, #<Account >, #<Account >, #<Account >, #<Account >, #<Account >, #<Account >, #<Account >,
#<Account >, #<Account >, #<Account >]
irb(main):003:0> ids = Account.operations_by_client.map(&:id)
  Account Load (0.0ms)  SELECT clients.firstname, clients.lastname, sum(operations.total) as total FROM "accounts" INNER JOIN "clients" ON "

clients"."id" = "accounts"."client_id" INNER JOIN "operations" ON "operations"."account_id" = "accounts"."id" GROUP BY clients.id, clients.f
irstname, clients.lastname ORDER BY clients.lastname
=> [nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, n
il, nil, nil, nil, nil, nil, nil, nil]
irb(main):004:0> ids = Account.operations_by_client.map(&:firstname)
  Account Load (0.0ms)  SELECT clients.firstname, clients.lastname, sum(operations.total) as total FROM "accounts" INNER JOIN "clients" ON "

clients"."id" = "accounts"."client_id" INNER JOIN "operations" ON "operations"."account_id" = "accounts"."id" GROUP BY clients.id, clients.f
irstname, clients.lastname ORDER BY clients.lastname
=> ["Alexis", "Ambre", "Clara", "Ambre", "Lisa", "Juliette", "Mathéo", "Gabriel", "Jeanne", "Célia", "Justine", "Adam", "Raphaël", "Alexis",
 "Mélissa", "Victor", "Anaïs", "Sarah", "Hugo", "Louna", "Nathan", "Maeva", "Carla", "Clémence", "Romain", "Lilou", "Baptiste", "Lena", "Nat
han", "Lucie", "Lucas", "Nathan", "Lucie", "Anaïs", "Ethan"]
irb(main):005:0> ids = Account.operations_by_client.map(&:total)
  Account Load (0.0ms)  SELECT clients.firstname, clients.lastname, sum(operations.total) as total FROM "accounts" INNER JOIN "clients" ON "

clients"."id" = "accounts"."client_id" INNER JOIN "operations" ON "operations"."account_id" = "accounts"."id" GROUP BY clients.id, clients.f
irstname, clients.lastname ORDER BY clients.lastname
=> [5380.2772339726025, 5184.132953424658, 5193.933681972603, 5050.373567123288, 4409.951164931506, 4224.2900646575345, 4446.3015346849315,
4332.022661260274, 4806.364291287671, 3986.5937578082194, 4765.242442520548, 5055.00624, 4157.1036887671235, 5097.631978082191, 5714.1788876
71233, 4214.344964383562, 3901.800284931507, 4564.252172273973, 4262.283531178083, 4924.093100273973, 5125.269859945205, 5343.249759561644,
4753.427991452054, 5557.8009600000005, 4445.941435616438, 4168.82107090411, 4890.077375123287, 6185.918728767123, 4734.1683349041095, 5046.5
6093369863, 4945.257031890411, 6050.124824547945, 4449.04845369863, 5478.534525369863, 4807.532230136986]
irb(main):006:0>


David Chelimsky

unread,
Nov 14, 2012, 10:44:44 AM11/14/12
to rs...@googlegroups.com
Then specify the last names instead o the IDs. The point is you can spec attributes rather than the objects themselves.

Sent from my iPhone
To view this discussion on the web visit https://groups.google.com/d/msg/rspec/-/BDB7BFQMSlsJ.

Javix

unread,
Nov 14, 2012, 10:55:26 AM11/14/12
to rs...@googlegroups.com
Thanks David for the reply. In such a situation, where I should test the index page displaying th result of the above scope method, I'm a little bit confused on WHAT exactly I should test... Usually I test the array contains an object we created before:

message = create(:message)
get :index
assigns(:messages).should == [message] #here we have ONE and the ONLY object in the array what is not the case in my test.

In my case, I'd rather create a new client and then verify that the result array generated by the scope method, contains an object which last name, for example, or any of available in the result set attributes is equal to the one I created before.
Right ? Or any other approach is possible.

Thanks and regards,
Reply all
Reply to author
Forward
0 new messages