I'm new to using RSpec for writing tests in a Rails application which uses a MySQL database. I have defined my fixtures and am loading them in my spec as follows:
before(:all) do
fixtures :student
end
Does this declaration save the data defined in my fixtures in the students table or does it just load the data in the table while the tests are running and remove it from the table after all the tests are run?
If you want to use fixtures with RSpec, specify your fixtures in the describe block, not within a before block:
describe StudentsController do
fixtures :students
before do
# more test setup
end
end
Your student fixtures will get loaded into the students table and then rolled back at the end of each test using database transactions.
First of all: You cannot use method fixtures in :all / :context / :suite hook. Do not try to use fixtures in these hooks (like post(:my_post)).
You can prepare fixtures only in describe/context block as Infuse write earlier.
Call
fixtures :students, :teachers
do not load any data into DB! Just prepares helper methods students and teachers.
Demanded records are loaded lazily in the moment when You first try to access them. Right before
dan=students(:dan)
This will load students and teachers in delete all from table + insert fixtures way.
So if you prepare some students in before(:context) hook, they will be gone now!!
Insert of records is done just once in test suite.
Records from fixtures are not deleted at the end of test suite. They are deleted and re-inserted on next test suite run.
example:
#students.yml
dan:
name: Dan
paul:
name: Paul
#teachers.yml
snape:
name: Severus
describe Student do
fixtures :students, :teachers
before(:context) do
#james=Student.create!(name: "James")
end
it "have name" do
expect(Student.find(#james.id)).to be_present
expect(Student.count).to eq 1
expect(Teacher.count).to eq 0
students(:dan)
expect(Student.find_by_name(#james.name)).to be_blank
expect(Student.count).to eq 2
expect(Teacher.count).to eq 1
end
end
#but when fixtures are in DB (after first call), all works as expected (by me)
describe Teacher do
fixtures :teachers # was loaded in previous tests
before(:context) do
#james=Student.create!(name: "James")
#thomas=Teacher.create!(name: "Thomas")
end
it "have name" do
expect(Teacher.find(#thomas.id)).to be_present
expect(Student.count).to eq 3 # :dan, :paul, #james
expect(Teacher.count).to eq 2 # :snape, #thomas
students(:dan)
expect(Teacher.find_by_name(#thomas.name)).to be_present
expect(Student.count).to eq 3
expect(Teacher.count).to eq 2
end
end
All expectations in tests above will pass
If these test are run again (in next suite) and in this order, than expectation
expect(Student.count).to eq 1
will be NOT met! There will be 3 students (:dan, :paul and fresh new #james). All of them will be deleted before students(:dan) and only :paul and :dan will be inserted again.
before(:all) keeps the exact data around, as it's loaded/created once. You do your thing, and at the end of the test it stays. That's why bui's link has after(:all) to destroy or use before(:each); #var.reload!;end to get the latest data from the tests before. I can see using this approach in nested rspec describe blocks.
Related
I'm using Rails unit testing and fixtures framework. Unless there's a configuration I haven't seen, I can't use the "Advanced Fixtures" (no id objects) from fixtures in sub-directories:
fixtures/people.yml
_fixture:
model_class: Person
myself:
first_name: Me
last_name: Myself
The following call will pass as expected:
fixtures :people
assert(people(:myself)))
while this one will not (after I move people.yml to subdir):
fixtures "subdir/people"
assert(people(:myself)))
In the later case, the error I get is this:
NoMethodError: undefined method `people'
Using Advanced Fixtures seem valuable but having all of my fixture files in the root of /fixtures seems missing something. I have a few test files and I'd like to have various tests use different fixtures directories.
Any input will be appreciated.
I would try saving people.yml in /fixtures/subdir/people.yml. The documentation is here https://apidock.com/rails/ActiveRecord/TestFixtures/ClassMethods/set_fixture_class .
test_helper.rb
set_fixture_class :people => 'Subdir::People'
in your test file.
before do
myself = subdir_people(:myself)
register(myself)
end
it "should test myself" do
assert(people(:myself)))
end
Here is my final solution:
In the fixture file, I kept:
_fixture:
model_class: Person
In the test file, I got:
def people(sym)
subdir_people(sym)
end
def test_myself
assert(people(:myself))
end
That way, it saves me from refactoring my numerous people() calls. Or I can use subdir_people() if I want.
I don't understand why my record in 2 spec is not rollback:
I have 2 tables, prefecture has_many hospitals
require "rails_helper"
RSpec.describe Hospital, type: :model, focus: true do
context "created along with new hospital created" do
it "should create new hospital" do
#prefecture = FactoryGirl.create(:prefecture)
hospital = FactoryGirl.build(:hospital, id: 10, prefecture: #prefecture)
expect { hospital.save }.to change { Hospital.count }.by 1
end
it "should save" do
#prefecture = FactoryGirl.create(:prefecture)
hospital = FactoryGirl.build(:hospital, id: 10, prefecture: #prefecture)
hospital.save
end
end
end
If I run it will show error "id=10 is existed in db"
Can anyone explain where I am mistake?
Thank you.
It is not the responsibility of FactoryGirl. You have 2 options:
Use transactions in tests: https://relishapp.com/rspec/rspec-rails/docs/transactions
Use a gem which cleans DB after after test such as: https://github.com/DatabaseCleaner/database_cleaner
Usually your models test will use transctions and database cleaner is used for integration/acceptance tests which cannot operate within transaction because a separate process (browser) needs to read the data created in tests.
By default, the rspec will not reset your database for each test. You need to configure it. Is because that your second test is falling, already exists a Hospital with id = 10.
When you remove the parameter id for the second test and runs with success, FactoryGirl will generate automatically a new id.
The method build from FactoryGirl will not fill the attribute id, just the rest of the attributes. Is because that when you call save after build method, the FactoryGirll generate a new id and your tests pass successfully.
after :build do |offer|
offer.save
end
I met a very strange issue when writing test using RSpec. Assume that I have 2 models: Company and Item with association company has_many items. I also set up database_cleaner with strategy transaction. My RSpec version is 2.13.0, database_cleaner version is 1.0.1, rails version is 3.2.15, factory_girl version is 4.2.0. Here is my test:
let(:company) { RSpec.configuration.company }
context "has accounts" do
it "returns true" do
company.items << FactoryGirl.create(:item)
company.items.count.should > 0
end
end
context "does not have accounts" do
it "returns false" do
company.items.count.should == 0
end
end
end
I set up an initial company to rspec configuration for using in every test because I don't want to recreate it in every tests because creating a company takes a lot of time(due to its callbacks and validations). The second test fails because item is not cleaned from the database after the first test. I don't understand why. If I change line company.items << FactoryGirl.create(:item) to FactoryGirl.create(:item, company: company), the it passes. So can any body explain for me why transaction isn't rollbacked in the first situation. I'm really messed up
Thanks. I really appreciate.
I think the problem is not in the rollback and I'm wondering if company.items can store it's value between contexts but I'm not sure.
I'm unable to reproduce it quickly so I want to ask you to:
check log/test.log when the rollback is performed
how many INSERTs was made for company.items << FactoryGirl.create(:item)
than change on the first test > to < that way: company.items.count.should < 0 it'll make test to fail but you'll get count value. Is it 1 or 2 ?
If you have relation between Company and Item model like has_many/belongs_to than I would suggest to just use build(:item) which should create company for it as well:
for example:
let(:item) { FactoryGirl.build(:item) }
context "has accounts"
it "returns true" do
item.save
Company.items.count.should == 1
end
don't forget to include association :company line at :item's factory
Hint:
add to spec_helper.rb:
RSpec.configure do |config|
# most omitted
config.include FactoryGirl::Syntax::Methods
and you can call any FactoryGirl method directly like this:
let(:item) { build(:item) }
I am new to Rspec and Factory girl and would like my test to run on a specific database state. I understand I can get Factory girl to create these records, and the objects will be destroyed after the test run, but what happens if I have data in the database.
For example: I want my test to run when there are 3 records in the database that I created through Factory Girl. However, I currently already have 1 model record in the database, and I don't want to delete it just for the test. Having that 1 model in there ruins my test.
Database Content
[#<Leaderboard id: 1, score: 500, name: "Trudy">]
leaderboard_spec.rb
require 'spec_helper'
describe Rom::Leaderboard do
describe "poll leaderboard" do
it "should say 'Successful Run' when it returns" do
FactoryGirl.create(:leaderboard, score: 400, name: "Alice")
FactoryGirl.create(:leaderboard, score: 300, name: "Bob")
FactoryGirl.create(:leaderboard, score: 200, name: "John")
Leaderboard.highest_scorer.name.should == "Alice"
end
end
end
Now my test will fail because it will incorrectly assume that Trudy is the highest scorer, since the test have run in an incorrect state.
Does factory girl offer anyway to delete records from the database then rollback this delete? Similar to how it creates records in the database and rollsback
Its popular to use the database_cleaner gem. You can find it here:
https://github.com/bmabey/database_cleaner
The documentation recommends the following configuration for rspec:
RSpec.configure do |config|
config.before(:suite) do
DatabaseCleaner.strategy = :transaction
DatabaseCleaner.clean_with(:truncation)
end
config.before(:each) do
DatabaseCleaner.start
end
config.after(:each) do
DatabaseCleaner.clean
end
end
This will you make sure you have a clean database for each test.
To answer your rollback question as directly as possible: no there isn't a way to rollback a delete within a test.
Following test conventions your goal usually is to start with a clean slate, and use factory_girl to efficiently build the scenario in the database you need to test for.
You can accomplish what you want by, for instance, adding a this to your leaderboards.rb factory file:
factory :trudy do
id 1
score 500
name "Trudy"
end
Or you could create a simple helper function in your test file that regenerates that record when its needed for tests:
def create_trudy
FactoryGirl.create :leaderboard,
id: 1,
score: 500,
name: "Trudy"
end
end
Or you could place all this in a before(:suite) within a describe block, like so:
describe "with a leaderboard record existing" do
before(:each) do
FactoryGirl.create :leaderboard, id: 1, score: 500, name: "Trudy"
end
# Tests with an initial leaderboard record
end
describe "with no leaderboard records initially" do
# Your test above would go here
end
In this final suggestion, your tests become very descriptive and when viewing the output, you will know exactly what state your database was in at the beginning of each test.
In my model, I have to choose an asset, saved in a editorial_asset table.
include ActionDispatch::TestProcess
FactoryGirl.define do
factory :editorial_asset do
editorial_asset { fixture_file_upload("#{Rails.root}/spec/fixtures/files/fakeUp.png", "image/png") }
end
end
so I have attached in my model factory an association on :editorial_asset
Upload work great, but take too much time (1s per example)
I'm wonder if it's possible to create uploads one time before each examples, and say in the factory: "find instead of create"
But the problem with database_cleaner, I cannot except tables with :transaction, truncation take 25sec instead of 40ms !
EDIT
The factory that need an asset
FactoryGirl.define do
factory :actu do
sequence(:title) {|n| "Actu #{n}"}
sequence(:subtitle) {|n| "Sous-sitre #{n}"}
body Lipsum.paragraphs[3]
# Associations
user
# editorial_asset
end
end
The model spec
require 'spec_helper'
describe Actu do
before(:all) do
#asset = create(:editorial_asset)
end
after(:all) do
EditorialAsset.destroy_all
end
it "has a valid factory" do
create(:actu).should be_valid
end
end
So a working way is
it "has a valid factory" do
create(:actu, editorial_asset: #asset).should be_valid
end
but there's no way to inject automatically association ?
Since you're using RSpec, you could use a before(:all) block to set up these records once. However, anything done in a before-all block is NOT considered part of the transaction, so you will have to delete anything from the DB yourself in an after-all block.
Your factory for the model that has an association to the editorial asset could then, yes, try to first find one before creating it. Instead of doing something like association :editorial_asset you could do:
editorial_asset { EditorialAsset.first || Factory.create(:editorial_asset) }
Your rspec tests could then look like this:
before(:all) do
#editorial = Factory.create :editorial_asset
end
after(:all) do
EditorialAsset.destroy_all
end
it "already has an editorial asset." do
model = Factory.create :model_with_editorial_asset
model.editorial_asset.should == #editorial
end
Read more about before and after blocks on the Rspec GitHub wiki page or on the Relish documentation:
https://github.com/rspec/rspec-rails
https://www.relishapp.com/rspec