Introduction
I have a hierarchy parent-children in my RoR project
and I wrote a new feature to our ruby on rails project, that shows two random children records from a parent.
class Article < ActiveRecord::Base
has_many :reviews
def to_json
{
...
reviews: reviews.n_random.as_json
...
}
end
end
and
class Review < ActiveRecord::Base
belongs_to :article
scope :n_random, ->(n=2) { order("RANDOM()").limit(n) }
end
Now, the problem that I have is even though the randomness works correctly, even in tests, I have problems with few tests that actually test this feature indirectly.
Let's say that I have an ArticlesControllerTest test suite, that contains a method
test 'show renders correct article' do
# given
params = { format: :json, id: 1 }
article = Article.find(params[:id])
# when
post :get, params
response_article = JSON.parse(response.body, symbolize_names: true)
#then
assert_response 200
assert_equal response_article, article.to_json
end
Problem
The last assert_equal fails, because for example:
response_article contains ids 1, 2
article.to_json contains ids 1, 3
Question
Is it possible to write some kind of a filter, that makes postgres's RANDOM() return always constant value? I know that I can use SELECT setseed(0.5); to set seed, so that next SELECT RANDOM(); returns the same value (although the next RANDOM() will change), but what I would like to achieve is to do something like setseed(0.5) before every possible select from active records.
I'll gladly take any other responses that will help me with this problem, because I know that RoR and Postgres are two different servers and I have no idea how to test this randomness from postgres's side.
inb4: I don't want to modify tests in a huge way.
You should probably use mocks / stubs for this, ensuring a consistent value just for the scope of this test. For example, with Mocha:
Article.any_instance.stubs(:to_json).returns({
...
reviews: reviews.last(2).as_json,
...
})
Or
Review.expects(:n_random).returns(Review.last(2))
And, in this example, you can revoke these using, for example:
Article.any_instance.unstub(:to_json)
N.B. I'm not certain of the syntax for the :n_random stub on a class as I've not got the environment to test it, but hopefully you get the idea (source here).
This means, within your test you will see consistent data, overriding the RANDOM() ordering. That way you can test your controller is doing what's expected of it, without worrying about the random data being used outside of the test env.
To implement, simply include one of the above in your test, i.e.
test 'show renders correct article' do
Review.expects(:n_random).returns(Review.last(2))
# given
params = { format: :json, id: 1 }
article = Article.find(params[:id])
# when
post :get, params
response_article = JSON.parse(response.body, symbolize_names: true)
#then
assert_response 200
assert_equal response_article, article.to_json
end
Related
I am trying to write two RSpec tests for two different problems that are much more advanced that what I'm used to writing.
What I'm trying to test within my controller:
def index
#buildings ||= building_class.active.where(place: current_place)
end
My attempt at writing the RSpec test:
describe 'GET :index' do
it "assigns #buildings" do
#buildings ||= building_class.active.where(place: current_place)
get :index
expect(assigns(:buildings)).to eq([building])
end
end
This test failed and wouldn't even run so I know I'm missing something.
My second test is needing to test the returned value of a class method. Here is what I am needing to test within the controller:
def class_name
ABC::Accountant::Business
end
Here is my attempt at testing this method:
describe "class name returns ABC::Accountant::Business" do
subject do
expect(subject.class_name).to eq(ABC::Accountant::Business)
end
end
For the first test I would do something like this:
First, I would move that .active.where(place: current_place) to a scope (I'm guessing building_class returns Building or something like that):
class Building << ApplicationRecord
scope :active_in, -> (place) { active.where(place: place)
Then it's easier to stub for the test
describe 'GET :index' do
it "assigns #buildings" do
scoped_buildings = double(:buildings)
expect(Building).to receive(:active_in).and_return(scoped_buildings)
get :index
expect(assigns(:buildings)).to eq(scoped_buildings)
end
end
Then your controller will do
#buildings ||= building_class.active_in(current_place)
This way you are testing two things: that the controller actually calls the scope and that the controller assigns the returned value on the #buildings variable (you don't really need to test the actual buidlings, you can test the scope on the model spec).
Personally, I feel like it would be better to do something like #buildings = current_place.active_buildings using the same idea of the scope to test that you are getting the active buildings of the current place.
EDIT: if you can't modify your controller, then the stubbing is a little different and it implies some chaining of methods that I don't like to explicitly test.
scoped_buildings = double(:buildings)
controller.stub_chain(:building_class, :active, :where).and_return(scoped_building)
get :index
expect(assings(:buildings)).to eq scoped_buildings
Note that now your test depends on a specific implementation and testing implementation is a bad practice, one should test behaviour and not implementation.
For the second, I guess something like this should work:
describe ".class_name" do
it "returns ABC::Accountant::Business" do
expect(controller.class_name).to eq(ABC::Accountant::Business)
end
end
IMHO, that the method's name if confusing, class_name gives the idea that it returns a string, you are not returnin a name, you are returning a class. Maybe you can change that method to resource_class or something less confusing.
How could I write a test to find the last created record?
This is the code I want to test:
Post.order(created_at: :desc).first
I'm also using factorybot
If you've called your method 'last_post':
def self.last_post
Post.order(created_at: :desc).first
end
Then in your test:
it 'should return the last post' do
expect(Post.last_post).to eq(Post.last)
end
On another note, the easiest way to write your code is simply
Post.last
And you shouldn't really be testing the outcome of ruby methods (you should be making sure the correct ruby methods are called), so if you did:
def self.last_post
Post.last
end
Then your test might be:
it 'should send the last method to the post class' do
expect(Post).to receive(:last)
Post.last_post
end
You're not testing the outcome of the 'last' method call - just that it gets called.
The accepted answer is incorrect. Simply doing Post.last will order the posts by the ID, not by when they were created.
https://apidock.com/rails/ActiveRecord/FinderMethods/last
If you're using sequential IDs (and ideally you shouldn't be) then obviously this will work, but if not then you'll need to specify the column to sort by. So either:
def self.last_post
order(created_at: :desc).first
end
or:
def self.last_post
order(:created_at).last
end
Personally I'd look to do this as a scope rather than a dedicated method.
scope :last_created -> { order(:created_at).last }
This allows you to create some nice chains with other scopes, such as if you had one to find all posts by a particular user/account, you could then chain this pretty cleanly:
Post.for_user(user).last_created
Sure you can chain methods as well, but if you're dealing with Query interface methods I feel scopes just make more sense, and tend to be cleaner.
If you wanted to test that it returns the correct record, in your test you could do something like:
let!(:last_created_post) { factory_to_create_post }
. . .
it "returns the correct post"
expect(Post.last_post).to eq(last_created_post)
end
If you wanted to have an even better test, you could create a couple records before the last record to verify the method under test is pulling the correct result and not just a result from a singular record.
I'm new to rails and i've done a simple sorting of dates in descending order. and now i need to write a test for it. my controller looks like this
def index
#article = Article.all.order('date DESC')
end
i tried writing a test but it doesn't work this is my code
def setup
#article1 = articles(:one)
end
test "array should be sorted desc" do
sorted_array = article1.sort.reverse
assert_equal article1, sorted_array, "Array sorted"
end
You should write a better description, by saying what each part of the code refers to, like:
# this is my controller_whatever.rb
def index
#article = Article.all.order('date DESC')
end
#this is my test/controllers/controller_whatever_test.rb
def setup
#article1 = articles(:one)
end
...
In your case you didn't create a "sorting", you created a controller action that queries records in descending order, so to test it you either need a controller test or an integration test (controller tests I believe are being dropped of use in favour of integration ones), which are more complex, since you need to visit the path in the test, then assert that somehow your results are as expected.
I think the best way to do this is to actually create a scope for your model, use that when querying in index and then test that scope.
This would be something like:
# app/models/article.rb
scope :default -> { order(date: :desc) }
Which then you could test it with:
#test/models/article_test.rb
def setup
#articles = Article.all
end
test "should be most recently published first" do
assert_equal articles(:last), #articles.first
assert_equal articles(:first), #articles.last
end
And you would need two fixtures with different dates at least, but I would advise you to have 4 or 5 with different dates and written in a different order in the articles.yml file (to make sure the test passes because it's correct and not simply because of randomness), and change your index action to simply:
def index
#article = Article.all # since now you have a default_scope
end
(if you have other places where you query Articles and you need them ordered in another way, instead of a default_scope, create a particular one and use that, both on controller and model test)
I would write a functional test in a test class according to the controller of your index action.
I assume your controller is named ArticlesController then the test class name is ArticlesControllerTest placed in test/controllers/articles_controller_test.rb.
In the test method you call/request the index action of your controller and you check for a successful answer first. Then you catch the articles, which your controller returns in the #article1 instance variable, with assigns(:article1).
Now you could check your articles are set and you can check the dates. Here I loop through all articles in an easy way and compare the date of the article before is greater or equal to the date of the current article, because of the descending order. For a simple test it should be acceptable, because you should not have a big amount of test records. May be there's a better way to check the order.
class ArticlesControllerTest < ActionController::TestCase
test "index should provide sorted articles" do
get :index
assert_response :success
articles = assigns(:article1)
assert_not_nil articles
date = nil
articles.each do |article|
if date
assert date >= article.date
end
date = article.date
end
end
end
Read about Functional Tests for Your Controllers in the Rails 4.2 Guides for more information.
I am finding it very hard to stub certain attributes of a model on a controller test. I want to make sure to stub as little as possible.
EDIT: I have been demoved of using stubs for such integration. I understood that the stubs won't reach the action call. The correct question would now be:
How can one use mocks and stubs to simulate a certain state in a Rails controller test?
So I've reached something like the following:
Spec
require 'spec_helper'
describe TeamsController do
let(:team) { FactoryGirl.create :team }
context "having questions" do
let(:competition) { FactoryGirl.create :competition }
it "allows a team to enter a competition" do
post(:enter_competition, id: team.id, competition_id: competition.id)
assigns(:enroll).team.should == team
assigns(:enroll).competition.should == competition
end
end
# ...
end
Factories
FactoryGirl.define do
factory :team do
name "Ruby team"
end
factory :competition, class: Competition do
name "Competition with questions"
after_create do |competition|
competition.
stub(:questions).
and_return([
"something"
])
end
end
factory :empty_competition, class: Competition do
name "Competition without questions"
questions []
after_create do |competition|
competition.stub(:questions).and_return []
end
end
end
Production code
class TeamsController < ApplicationController
def enter_competition
#team = Team.find params[:id]
#competition = Competition.find params[:competition_id]
#enroll = #team.enter_competition #competition
render :nothing => true
end
end
class Team < ActiveRecord::Base
def enter_competition competition
raise Competition::Closed if competition.questions.empty?
enroll = Enroll.new team: self, competition: competition
enroll.save
enroll
end
end
When I run the test, the questions attribute comes as being nil and so the test fails in the model when checking for nil.empty?.
Why isn't the stub being used so that the state of that message is correctly used? I expected that #competition.questions would be [ "question" ] but instead I get nil.
The problem you're running into is that stub works on an instance of a Ruby object; it doesn't affect all ActiveRecord objects that represent the same row.
The quickest way to fix your test would be to add this to your test, before the post:
Competition.stub(:find).and_return(competition)
The reason that's necessary is that Competition.find will return a fresh Competition object that doesn't have questions stubbed out, even though it represents the same database row. Stubbing find as well means that it will return the same instance of Competition, which means the controller will see the stubbed questions.
I'd advise against having that stub in your factory, though, because it won't be obvious what's stubbed as a developer using the factory, and because it means you'll never be able to test the real questions method, which you'll want to do in the Competition unit test as well as any integration tests.
Long story short: if you stub out a method on an instance of your model, you also need to stub out find for that model (or whatever class method you're using to find it), but it's not a good idea to have such stubs in a factory definition.
When you call create on FactoryGirl, it creates database records which you then retrieve back in your controller code. So the instances you get (#team, #competition) are pure ActiveRecord, without any methods stubbed out.
Personally I would write you test like this (not touching database at all):
let(:team) { mock_model(Team) }
let(:competition) { mock_model(Competition) }
before do
Team.stub(:find) { team }
Competition.stub(:find) { competition }
end
and then in your test something like this:
it "should call enter_competition on #team with #competition" do
team.should_receive(:enter_competition).with(competition)
post :enter_competition, id: 7, competition_id: 10
I don't really understand what your controller is supposed to do or what are you testing for that matter, sorry :(
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