I'm not very experienced with Rails and RSpec and often have troubles with writing tests. So, my next trouble is that I don't know how to test order in model's relationship properly.
Let's say I have simple model like this:
class Kitchen < ActiveRecord::Base
has_many :orders, -> { order(completed_at: :desc) }
end
And simple test for that model:
require 'rails_helper'
RSpec.describe Kitchen, :type => :model do
before { #kitchen = FactoryGirl.create(:kitchen) }
subject { #kitchen }
describe "orders" do
before do
#kitchen.orders.build(description: "Test description 1",
completed_at: 1.day.ago.localtime)
#kitchen.orders.build(description: "Test description 2",
completed_at: Time.now)
end
it "should be sorted by completion date in descending order" do
expect(#kitchen.orders.first.completed_at).to be > #kitchen.orders.last.completed_at
end
end
end
As a result I have got the error:
Failure/Error: expect(#kitchen.orders.first.completed_at).to be > #kitchen.orders.last.completed_at
expected: > Fri, 15 May 2015 12:21:54 UTC +00:00
got: Thu, 14 May 2015 12:21:54 UTC +00:00
Any help will be appreciated.
You are using build. This does not persist to the database, so when you call #kitchen.orders.first, you are not hitting the database, just getting back the first one you created, which is the wrong one.
Use create instead, then call #kitchen.reload to refresh from the database.
How about the following:
it {
is_expected.to have_many(:orders).order( completed_at: :desc )
}
I would do something like this:
RSpec.describe Kitchen, :type => :model do
let(:kitchen) { FactoryGirl.create(:kitchen) }
describe 'orders' do
let(:yesterday) { 1.day.ago }
let(:today) { Time.now }
before do
# `create` actually saves the objects into the database
kitchen.orders.create(description: '1', completed_at: today)
kitchen.orders.create(description: '2', completed_at: yesterday)
end
# `(true)` forces a reload of the associated objects
subject(:orders) { kitchen.orders(true) }
it 'should be sorted by completion date in descending order' do
expect(orders.map(&:completed_at)).to eq [yesterday, today]
end
end
end
Related
I'm having this scope function inside my model
# models/Post.rb
def self.filtered (params)
unless params[:year].blank? && params[:month].blank?
year = params[:year].to_i
month = params[:month].to_i
return where(created_at: DateTime.new(year, month, 1).beginning_of_day..DateTime.new(year, month, -1).end_of_day)
end
self
end
# controllers/posts_controller.rb
#posts = Post.filtered(params)
Which basically returns all archived posts of specific year and month
SELECT `posts`.* FROM `posts`
WHERE (`posts`.`created_at` BETWEEN '2017-10-01 00:00:00' AND '2017-10-31 23:59:59')
I'm trying to write a test for this method to make sure that a post is was created in the requested year and month, how can I do this?
# spec/models/post_spec.rb
describe '.filtered' do
let!(:older) { FactoryGirl.create(:post, created_at: 1.month.ago) } # this post should not appear in the list
let!(:newer) { FactoryGirl.create(:post, created_at: Time.zone.now) } # this post should appear in the list
it 'is within specific year and month' do
expect(Post.filtered({year: Date.today.strftime("%Y"), month: Date.today.strftime("%m")}).map { |post| post.created_at }).to be ???
end
end
Use the include matcher to verify a record is included in the result set.
expect(Post.filtered({year: Date.today.strftime("%Y"), month: Date.today.strftime("%m")}).to include(newer)
Use #contain_exactly to match elements when order should be disregarded.
# spec/models/post_spec.rb
describe '.filtered' do
let!(:older) { FactoryGirl.create(:post, created_at: 1.month.ago) } # this post should not appear in the list
let!(:newer) { FactoryGirl.create(:post, created_at: Time.zone.now) } # this post should appear in the list
it 'is within specific year and month' do
expect(Post.filtered({year: Date.today.strftime("%Y"), month: Date.today.strftime("%m")}).map { |post| article.created_at }).to contain_exactly(newer)
end
end
By the way, instead of creating a class method like what you did here, you might want to consider a scope so it can be chained with other scopes.
I'm with the following problem:
Environment: Ruby: 2.3.1 and Rails 5.0.0.1
I'm trying to validate a datetime field with RSpec and Factory Girl.
I got this error:
expected: "2016-11-11 13:30:31 UTC" (From Factory Girl)
got: "2016-11-11T13:30:31.218Z" (From database)
My code:
klass_object = FactoryGirl.create(:character)
klass = Character
RSpec.shared_examples 'API GET #index' do |klass|
before { get :index, params: params, accept: Mime[:json] }
it "returns a list of #{klass.to_s.underscore.pluralize}" do
object_array = json(response.body)
klass_attributes = klass.attribute_names.without("id", "created_at", "updated_at").map(&:to_sym)
klass_attributes.each do |attribute|
object_array.each do |object|
expect(object[attribute].to_s).to eq(klass_object[attribute].to_s)
end
end
end
...
end
Factory:
FactoryGirl.define do
factory :character do
marvel_id { Faker::Number.number(6).to_i }
name { Faker::Superhero.name }
description { Faker::Hipster.paragraphs(1) }
modified { Faker::Date.between(DateTime.now - 1, DateTime.now) }
factory :invalid_character do
id ''
name ''
marvel_id ''
modified ''
end
end
end
How can I correct this problem?
I did that, it works but I think it is not so good. There is a better way to do it?
object_array.each do |object|
if ActiveSupport::TimeWithZone == klass_object[attribute].class
expect(object[attribute].to_datetime.strftime("%Y-%m-%d %H:%M:%S")).to eq(klass_object[attribute].to_datetime.strftime("%Y-%m-%d %H:%M:%S"))
else
expect(object[attribute].to_s).to eq(klass_object[attribute].to_s)
end
end
Thanks for your help.
I can suggest you to change your approach to compare the results. You can use approach, which based on the idea of the golden master.
In according to this approach you take a snapshot of an object, and then compare all future versions of the object to the snapshot.
In your case you can write json fixture first time, check that json is correct and compare it with result json next time.
For example
approved.json
[
{
"travel_time_seconds": 43200,
"available_seats_amount": 10,
"departure_at": "2016-04-08T02:00:00.000+03:00",
"arrival_at": "2016-04-08T17:00:00.000+03:00",
"source_point_name": "New York",
"destination_point_name": "Moscow",
"tickets_count": 2
}
]
controller_spec.rb
RSpec.shared_examples 'API GET #index' do |klass|
before { get :index, params: params, accept: Mime[:json] }
it "returns a list of #{klass.to_s.underscore.pluralize}" do
verify(format: :json) { json(response.body).map {|o| o.except('id', 'created_at', 'updated_at' }
end
...
end
approvals gem, for example, can help you with that
I know this is a very old question but I just came across the solution to this today and couldn't find another answer. I've been using Faker too, but the Date/Time formats don't seem to work very well with Ruby time math without a lot of finagling.
However, if you use Time in your factory, and then convert it .to_i, it will get sent to the db in the correct format for comparison in rspec.
Migration:
class CreateExperiences < ActiveRecord::Migration[5.2]
def change
create_table :experiences do |t|
t.datetime :start_date
Factory:
FactoryBot.define do
when_it_started = Time.zone.now - rand(3000).days
factory :experience do
start_date { when_it_started.to_i }
Spec:
RSpec.describe "Experiences API", type: :request do
let!(:valid_attributes) { attributes_for(:experience) }
describe "POST /v1/experiences" do
before { post "/v1/experiences", params: valid_attributes }
it "creates an experience" do
expect(JSON.parse(response.body).except("id", "created_at", "updated_at")).to eq(valid_attributes.stringify_keys)
end
Then my tests passed. Hopefully this will help someone else!
Try to use to_datetime instead to_s
expect(object[attribute].to_datetime).to eq(klass_object[attribute].to_datetime)
I have a failing test for a GET request — the issue lies with the way ActiveRecord's timestamps are formatted in the response. The RSpec test expects the data format to be Mon, 29 Dec 2014 13:37:09 UTC +00:00,, but it instead receives 2014-12-29T13:37:09Z. Is there any way to change the formatting of the ActiveRecord timestamp, or what the RSpec test expects?
Edit:
Here's the failing test:
describe 'GET /v1/tasks/:id' do
it 'returns an task by :id' do
task = create(:task)
get "/v1/tasks/#{task.id}"
expect(response_json).to eq(
{
'id' => task.id,
'title' => task.title,
'note' => task.note,
'due_date' => task.due_date,
'created_at' => task.created_at,
'updated_at' => task.updated_at
}
)
end
end
The test fails on created_at and updated_at. response_json is a helper method that parses the JSON response.
Here's where the test fails:
expected: {"id"=>1, "title"=>"MyString", "note"=>"MyText", "due_date"=>Mon, 29 Dec 2014, "created_at"=>Mon, 29 Dec 2014 13:37:09 UTC +00:00, "updated_at"=>Mon, 29 Dec 2014 13:37:09 UTC +00:00}
got: {"id"=>1, "title"=>"MyString", "note"=>"MyText", "due_date"=>"2014-12-29", "created_at"=>"2014-12-29T13:37:09Z", "updated_at"=>"2014-12-29T13:37:09Z"}
The solution was embarrassing simple — I had specify that the dates should be formatted using ISO 8601 using the to_formatted_s() method. Below is the correct, passing version:
require 'spec_helper'
describe 'GET /v1/tasks/:id' do
it 'returns an task by :id' do
task = create(:task)
get "/v1/tasks/#{task.id}"
expect(response_json).to eq(
{
'id' => task.id,
'title' => task.title,
'note' => task.note,
'due_date' => task.due_date.to_formatted_s(:iso8601),
'created_at' => task.created_at.to_formatted_s(:iso8601),
'updated_at' => task.updated_at.to_formatted_s(:iso8601)
}
)
end
end
The problem is that the date columns are being 'jsonified' in a format that you may or may not have specified. This value is of a different format and a string; therefore matching it with a date value fails.
To resolve this you could either adjust how the created_at value is 'jsonified' (format it) or you could just do this in your test:
columns = %w{id title note due_date created_at updated_at}
# get task as json string:
task_json_string = task.to_json(only: columns)
# turn json into hash
task_json = ActiveSupport::JSON.decode(task_json_string)
json_attributes = task_json.values.first
expected_hash = columns.each_with_object({}) do |attr, hash|
# ensuring that the order of keys is as specified by 'columns'
hash[attr] = json_attributes[attr]
end
expect(response_json).to eq expected_hash
That should work (minor debugging might be required though).
To go the former route, add an instance method like the following to your Task model:
def as_json( *args )
super(*args).tap do |hash|
attributes_hash = hash.values.first
%w{created_at updated_at}.each do |attr|
if attributes_hash.has_key?(attr)
# Use strftime time instead of to_s to specify a format:
attributes_hash[attr] = self.send(attr).to_s
end
end
end
end
Then in your test your task json could simply be:
task_json = task.as_json.values.first
Or after adding the add_json method and keeping the to_s there, in your test all you will need to do is add a .to_s to your date method e.g. task.created_at.to_s; the values will match.
Thereby removing the object-to-json-to-hash step and doing object-to-hash directly.
The following Date and DateTime formatting should correct the problem.
{
'id' => task.id,
'title' => task.title,
'note' => task.note,
'due_date' => task.due_date.strftime("%Y-%m-%d %H:%M:%S %Z"),
'created_at' =>task.created_at.strftime("%Y-%m-%d %H:%M:%S %Z"),
'updated_at' =>task.updated_at.strftime("%Y-%m-%d %H:%M:%S %Z")
}
EDIT
Correction on date format.
As #Humza mentioned, the problem stems from how values are "jsonified".
tl;dr -- just change your spec to expect 'created_at' => task.created_at.as_json
The Long Version
In ActiveSupport (5.1.4), all values are run through ActiveSupport::JSON::Encoding#jsonify:
def jsonify(value)
case value
when String
EscapedString.new(value)
when Numeric, NilClass, TrueClass, FalseClass
value.as_json
when Hash
Hash[value.map { |k, v| [jsonify(k), jsonify(v)] }]
when Array
value.map { |v| jsonify(v) }
else
jsonify value.as_json
end
end
As you can see, there's no special case for ActiveSupport::TimeWithZone, it just calls ActiveSupport::TimeWithZone#as_json. So let's see what that looks like:
[1] pry(#<DataModelRowSerializer>)> show-source ActiveSupport::TimeWithZone#as_json
From: /Users/stevehull/.rbenv/versions/2.4.2/lib/ruby/gems/2.4.0/gems/activesupport-5.1.4/lib/active_support/time_with_zone.rb # line 165:
Owner: ActiveSupport::TimeWithZone
Visibility: public
Number of lines: 7
def as_json(options = nil)
if ActiveSupport::JSON::Encoding.use_standard_json_time_format
xmlschema(ActiveSupport::JSON::Encoding.time_precision)
else
%(#{time.strftime("%Y/%m/%d %H:%M:%S")} #{formatted_offset(false)})
end
end
So if you wanted to, you could expect 'created_at' => task.created_at.xmlschema(ActiveSupport::JSON::Encoding.time_precision), however it's much more compact to use as_json ;)
I'm using scaffolding to generate rspec controller tests. By default, it creates the test as:
let(:valid_attributes) {
skip("Add a hash of attributes valid for your model")
}
describe "PUT update" do
describe "with valid params" do
let(:new_attributes) {
skip("Add a hash of attributes valid for your model")
}
it "updates the requested doctor" do
company = Company.create! valid_attributes
put :update, {:id => company.to_param, :company => new_attributes}, valid_session
company.reload
skip("Add assertions for updated state")
end
Using FactoryGirl, I've filled this in with:
let(:valid_attributes) { FactoryGirl.build(:company).attributes.symbolize_keys }
describe "PUT update" do
describe "with valid params" do
let(:new_attributes) { FactoryGirl.build(:company, name: 'New Name').attributes.symbolize_keys }
it "updates the requested company", focus: true do
company = Company.create! valid_attributes
put :update, {:id => company.to_param, :company => new_attributes}, valid_session
company.reload
expect(assigns(:company).attributes.symbolize_keys[:name]).to eq(new_attributes[:name])
This works, but it seems like I should be able to test all attributes, instead of just testing the changed name. I tried changing the last line to:
class Hash
def delete_mutable_attributes
self.delete_if { |k, v| %w[id created_at updated_at].member?(k) }
end
end
expect(assigns(:company).attributes.delete_mutable_attributes.symbolize_keys).to eq(new_attributes)
That almost worked, but I'm getting the following error from rspec having to do with BigDecimal fields:
-:latitude => #<BigDecimal:7fe376b430c8,'0.8137713195 830835E2',27(27)>,
-:longitude => #<BigDecimal:7fe376b43078,'-0.1270954650 1027958E3',27(27)>,
+:latitude => #<BigDecimal:7fe3767eadb8,'0.8137713195 830835E2',27(27)>,
+:longitude => #<BigDecimal:7fe3767ead40,'-0.1270954650 1027958E3',27(27)>,
Using rspec, factory_girl, and scaffolding is incredibly common, so my questions are:
What is a good example of an rspec and factory_girl test for a PUT update with valid params?
Is it necessary to use attributes.symbolize_keys and to delete the mutable keys? How can I get those BigDecimal objects to evaluate as eq?
Ok so this is how I do, I don't pretend to strictly follow the best practices, but I focus on precision of my tests, clarity of my code, and fast execution of my suite.
So let take example of a UserController
1- I do not use FactoryGirl to define the attributes to post to my controller, because I want to keep control of those attributes. FactoryGirl is useful to create record, but you always should set manually the data involved in the operation you are testing, it's better for readability and consistency.
In this regard we will manually define the posted attributes
let(:valid_update_attributes) { {first_name: 'updated_first_name', last_name: 'updated_last_name'} }
2- Then I define the attributes I expect for the updated record, it can be an exact copy of the posted attributes, but it can be that the controller do some extra work and we also want to test that. So let's say for our example that once our user updated his personal information our controller automatically add a need_admin_validation flag
let(:expected_update_attributes) { valid_update_attributes.merge(need_admin_validation: true) }
That's also where you can add assertion for attribute that must remain unchanged. Example with the field age, but it can be anything
let(:expected_update_attributes) { valid_update_attributes.merge(age: 25, need_admin_validation: true) }
3- I define the action, in a let block. Together with the previous 2 let I find it makes my specs very readable. And it also make easy to write shared_examples
let(:action) { patch :update, format: :js, id: record.id, user: valid_update_attributes }
4- (from that point everything is in shared example and custom rspec matchers in my projects) Time to create the original record, for that we can use FactoryGirl
let!(:record) { FactoryGirl.create :user, :with_our_custom_traits, age: 25 }
As you can see we manually set the value for age as we want to verify it did not change during the update action. Also, even if the factory already set the age to 25 I always overwrite it so my test won't break if I change the factory.
Second thing to note: here we use let! with a bang. That is because sometimes you may want to test your controller's fail action, and the best way to do that is to stub valid? and return false. Once you stub valid? you can't create records for the same class anymore, therefor let! with a bang would create the record before the stub of valid?
5- The assertions itself (and finally the answer to your question)
before { action }
it {
assert_record_values record.reload, expected_update_attributes
is_expected.to redirect_to(record)
expect(controller.notice).to eq('User was successfully updated.')
}
Summarize So adding all the above, this is how the spec looks like
describe 'PATCH update' do
let(:valid_update_attributes) { {first_name: 'updated_first_name', last_name: 'updated_last_name'} }
let(:expected_update_attributes) { valid_update_attributes.merge(age: 25, need_admin_validation: true) }
let(:action) { patch :update, format: :js, id: record.id, user: valid_update_attributes }
let(:record) { FactoryGirl.create :user, :with_our_custom_traits, age: 25 }
before { action }
it {
assert_record_values record.reload, expected_update_attributes
is_expected.to redirect_to(record)
expect(controller.notice).to eq('User was successfully updated.')
}
end
assert_record_values is the helper that will make your rspec simpler.
def assert_record_values(record, values)
values.each do |field, value|
record_value = record.send field
record_value = record_value.to_s if (record_value.is_a? BigDecimal and value.is_a? String) or (record_value.is_a? Date and value.is_a? String)
expect(record_value).to eq(value)
end
end
As you can see with this simple helper when we expect for a BigDecimal, we can just write the following, and the helper do the rest
let(:expected_update_attributes) { {latitude: '0.8137713195'} }
So at the end, and to conclude, when you have written your shared_examples, helpers, and custom matchers, you can keep your specs super DRY. As soon as you start repeating the same thing in your controllers specs find how you can refactor this. It may take time at first, but when its done you can write the tests for a whole controller in few minutes
And a last word (I can't stop, I love Rspec) here is how my full helper look like. It is usable for anything in fact, not just models.
def assert_records_values(records, values)
expect(records.length).to eq(values.count), "Expected <#{values.count}> number of records, got <#{records.count}>\n\nRecords:\n#{records.to_a}"
records.each_with_index do |record, index|
assert_record_values record, values[index], index: index
end
end
def assert_record_values(record, values, index: nil)
values.each do |field, value|
record_value = [field].flatten.inject(record) { |object, method| object.try :send, method }
record_value = record_value.to_s if (record_value.is_a? BigDecimal and value.is_a? String) or (record_value.is_a? Date and value.is_a? String)
expect_string_or_regexp record_value, value,
"#{"(index #{index}) " if index}<#{field}> value expected to be <#{value.inspect}>. Got <#{record_value.inspect}>"
end
end
def expect_string_or_regexp(value, expected, message = nil)
if expected.is_a? String
expect(value).to eq(expected), message
else
expect(value).to match(expected), message
end
end
This is the questioner posting. I had to go down the rabbit hole a bit in understanding multiple, overlapping issues here, so I just wanted to report back on the solution I found.
tldr; It's too much trouble trying to confirm that every important attribute comes back unchanged from a PUT. Just check that the changed attribute is what you expect.
The issues I encountered:
FactoryGirl.attributes_for does not return all values, so FactoryGirl: attributes_for not giving me associated attributes suggests using (Factory.build :company).attributes.symbolize_keys, which winds up creating new problems.
Specifically, Rails 4.1 enums show as integers instead of enum values, as reported here: https://github.com/thoughtbot/factory_girl/issues/680
It turns out that the BigDecimal issue was a red herring, caused by a bug in the rspec matcher which produces incorrect diffs. This was established here: https://github.com/rspec/rspec-core/issues/1649
The actual matcher failure is caused by Date values that don't match. This is due to the time returned being different, but it doesn't show because Date.inspect does not show milliseconds.
I got around these problems with a monkey patched Hash method that symbolizes keys and stringifes values.
Here's the Hash method, which could go in rails_spec.rb:
class Hash
def symbolize_and_stringify
Hash[
self
.delete_if { |k, v| %w[id created_at updated_at].member?(k) }
.map { |k, v| [k.to_sym, v.to_s] }
]
end
end
Alternatively (and perhaps preferably) I could have written a custom rspec matcher than iterates through each attribute and compares their values individually, which would have worked around the date issue. That was the approach of the assert_records_values method at the bottom of the answer I selected by #Benjamin_Sinclaire (for which, thank you).
However, I decided instead to go back to the much, much simpler approach of sticking with attributes_for and just comparing the attribute I changed. Specifically:
let(:valid_attributes) { FactoryGirl.attributes_for(:company) }
let(:valid_session) { {} }
describe "PUT update" do
describe "with valid params" do
let(:new_attributes) { FactoryGirl.attributes_for(:company, name: 'New Name') }
it "updates the requested company" do
company = Company.create! valid_attributes
put :update, {:id => company.to_param, :company => new_attributes}, valid_session
company.reload
expect(assigns(:company).attributes['name']).to match(new_attributes[:name])
end
I hope this post allows others to avoid repeating my investigations.
Well, I did something that's quite simpler, I'm using Fabricator, but I'm pretty sure it's the same with FactoryGirl:
let(:new_attributes) ( { "phone" => 87276251 } )
it "updates the requested patient" do
patient = Fabricate :patient
put :update, id: patient.to_param, patient: new_attributes
patient.reload
# skip("Add assertions for updated state")
expect(patient.attributes).to include( { "phone" => 87276251 } )
end
Also, I'm not sure why you are building a new factory, PUT verb is supposed to add new stuff, right?. And what you are testing if what you added in the first place (new_attributes), happens to exist after the put in the same model.
This code can be used to solve your two issues:
it "updates the requested patient" do
patient = Patient.create! valid_attributes
patient_before = JSON.parse(patient.to_json).symbolize_keys
put :update, { :id => patient.to_param, :patient => new_attributes }, valid_session
patient.reload
patient_after = JSON.parse(patient.to_json).symbolize_keys
patient_after.delete(:updated_at)
patient_after.keys.each do |attribute_name|
if new_attributes.keys.include? attribute_name
# expect updated attributes to have changed:
expect(patient_after[attribute_name]).to eq new_attributes[attribute_name].to_s
else
# expect non-updated attributes to not have changed:
expect(patient_after[attribute_name]).to eq patient_before[attribute_name]
end
end
end
It solves the problem of comparing floating point numbers by converting the values to it string representation using JSON.
It also solves the problem of checking that the new values have been updated but the rest of the attributes have not changed.
In my experience, though, as the complexity grows, the usual thing to do is to check some specific object state instead of "expecting that the attributes I don't update won't change". Imagine, for instance, having some other attributes changing as the update is done in the controller, like "remaining items", "some status attributes"... You would like to check the specific expected changes, that may be more than the updated attributes.
Here is my way of testing PUT. That is a snippet from my notes_controller_spec, the main idea should be clear (tell me if not):
RSpec.describe NotesController, :type => :controller do
let(:note) { FactoryGirl.create(:note) }
let(:valid_note_params) { FactoryGirl.attributes_for(:note) }
let(:request_params) { {} }
...
describe "PUT 'update'" do
subject { put 'update', request_params }
before(:each) { request_params[:id] = note.id }
context 'with valid note params' do
before(:each) { request_params[:note] = valid_note_params }
it 'updates the note in database' do
expect{ subject }.to change{ Note.where(valid_note_params).count }.by(1)
end
end
end
end
Instead of FactoryGirl.build(:company).attributes.symbolize_keys, I'd write FactoryGirl.attributes_for(:company). It is shorter and contains only parameters that you specified in your factory.
Unfortunately that is all I can say about your questions.
P.S. Though if you lay BigDecimal equality check on database layer by writing in style like
expect{ subject }.to change{ Note.where(valid_note_params).count }.by(1)
this may work for you.
Testing the rails application with rspec-rails gem.
Created the scaffold of user.
Now you need to pass all the examples for the user_controller_spec.rb
This has already written by the scaffold generator. Just implement
let(:valid_attributes){ hash_of_your_attributes} .. like below
let(:valid_attributes) {{ first_name: "Virender", last_name: "Sehwag", gender: "Male"}
}
Now will pass many examples from this file.
For invalid_attributes be sure to add the validations on any of field and
let(:invalid_attributes) {{first_name: "br"}
}
In the users model .. validation for first_name is as =>
validates :first_name, length: {minimum: 5}, allow_blank: true
Now all the examples created by the generators will pass for this controller_spec
Lets set the stage for this question.
Here is our model:
class Deal < ActiveRecord::Base
belongs_to :retailer
has_many :content_locations, as: :locatable
has_many :stores, through: :content_locations
searchable do
text :title
text :store_name
text :store_sort_name
text :description
end
scope :by_keyword, ->(keyword, limit, offset) {
search {
fulltext keyword
paginate offset: offset, per_page: limit
}.results
}
end
Here is the section of our rspec test that hits SOLR:
describe 'searchable' do
before do
DatabaseCleaner.clean
end
target = 'foo'
let!(:deal_with_title) { create :deal, title: target }
let!(:deal_with_description) { create :deal, description: target }
let!(:deal_with_store_name) { create :deal, store_name: target }
let!(:deal_with_store_sort_name) { create :deal, store_sort_name: target }
let!(:deal_with_nothing) {
create :deal, title: 'bar', description: 'bar', store_name: 'bar', store_sort_name: 'bar'
}
it 'includes matches' do
sleep 1
results = Deal.by_keyword(target, 25, 0)
expect(results).to include(deal_with_title)
expect(results).to include(deal_with_description)
expect(results).to include(deal_with_store_name)
expect(results).to include(deal_with_store_sort_name)
expect(results).to_not include(deal_with_nothing)
end
end
What we are seeing is that our test fails randomly. I can execute this test over and over successfully for 1-3xs but the 4th will fail, or it will fail 4xs and the 5th will fail. The pattern is completely random. As you see we tried adding a sleep to this setup and that doesn't do anything but slow it down.
Any tips would be greatly appreciated here. This has been driving us nuts!
In the Sunspot Model Spec they're allways calling
Sunspot.commit
after data creation and before any check
Update:
You can also call model.index!, which will immediately sync the particular model into solr index. In one specific case i also had to call model.reload after saving and before indexing, because of some tag relationships attached to the model (however this was acts_as_taggable specific)
#simple
Factory.create(:model).tap { |m| m.index! }
#advanced
model = Factory.build :model_with_relationships
model.save
model.reload #optional
model.index!
We decided to go a completely different direction and honestly I would not recommend hitting SOLR in real time for anyone reading this post. It is a pain.
Our final solution was to with the sunspot_matchers gem instead.