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.
Related
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 need to seed some geographical data for my test, and I'm not sure that I'm taking the right approach here, but here is how I've tried.
In my spec helper:
config.before(:each, location_data: true) do |example|
address = FactoryGirl.create(:address, atitude: 20.9223, longitude: -72.551)
end
A specific address point I created. Then I have these, which I think are ruining my data :
config.before(:suite) do
DatabaseCleaner.strategy = :truncation
DatabaseCleaner.clean_with(:truncation)
end
config.before(:each) do
DatabaseCleaner.start
end
config.after(:each) do
DatabaseCleaner.clean
end
Then in my test I have this:
context 'nearby', location_data: true do
context 'there should be 0 matches in radius' do
binding.pry
#When I debug here there are 0 addresses created
expect(Address.count).to eq(1)
end
end
When I look at the test log its like my test data setup is not even executed, what am I doing wrong here? I need this address for various tests, not just one but many complex scenarios and I would reuse the addresses in another tests, that's why I put them in a rspec config to make it more DRY
Changes as suggested in max answer :
module LocationData
extend ActiveSupport::Concern
included do
let!(:address) { FactoryGirl.create(:address, latitude: 20.9223, longitude: -72.551) }
end
end
Then in my test:
require 'support/location_data'
describe MyModel do
include LocationData
context 'nearby' do
context 'there should be 1 matches in radius' do
binding.pry
#When I debug here there are 0 addresses created
expect(Address.count).to eq(1)
end
end
end
Still get 0 address count when I count addresses. Not sure what am I doing wrong.
SOLUTION (thanks max):
I was missing it block in context block :
context 'there should be 1 matches in radius' do
binding.pry
#When I debug here there are 0 addresses created
it 'has one address before' do
expect(Address.count).to eq(1)
end
end
A good test suite will empty the database between each example.
Why? Stuffing a bunch of data into your database and then running some test on the same DB data sounds like a good idea at first. But if you tests alter that data than you soon end up with ordering issues which can cause flapping tests and serious headaches. Its an approach that has been tested and found lacking.
Instead you want a clean slate for each test. DatabaseCleaner does just that. It's not ruining your data - it's keeping your data from ruining your test suite and or sanity.
You never want to create test data in your rspec configuration. Use it to setup the tools you need to run your test. If you start creating a bunch of flags to set up data from your config it's going to get out of control quickly. And you don't really need the exact same data as often as you think.
Instead if you find yourself repeatedly setting up the same data in your specs you can dry it out with example groups. Or create named factories with FactoryGirl.
module GeocodedExampleGroup
extend ActiveSupport::Concern
included do
let(:address) { FactoryGirl.create(:address, latitude: 20.9223, longitude: -72.551) }
end
end
require 'rails_helper'
require 'support/example_groups/geocoded'
describe SomeModel do
include GeocodedExampleGroup
# ...
end
I have gem devise and gem apartment which I'm using to create separate schemas for each devise's user account.
Apartment's doc and advice in that issue suggest to use Rack middleware to switch between tenants. In that case it's not possible (as far as I know) as I have it user depended rather than request depended.
All works just great except my RSpec tests. The problem is that after every test database is not clean properly (it doesn't remove schema for new created user). All tests pass if I run a small set of them but if I run to many than Faker::Internet.first_name generates usernames that already was taken (which is not valid).
So this is how I did it:
app/controllers/application_controller.rb
def scope_tenant
Apartment::Database.switch(current_user.username)
end
app/controllers/albums_controller.rb (album model belong_to :user)
class AlbumsController < ApplicationController
before_action :authenticate_user! # devise magic
before_action :scope_tenant
app/model/user.rb
after_create :create_schema
private
def create_schema
Apartment::Database.create(self.username)
end
This is what I've added to my specs:
spec/factories/user.rb
FactoryGirl.define do
factory :user do
username { Faker::Name.first_name }
email { Faker::Internet.email("#{username}") }
password "login_as will not use it anyway"
end
end
spec/support/auth_helpers.rb
Warden.test_mode!
def login_and_switch_schema(user)
login_as(user)
Apartment::Database.switch(user.username) # for some reason `login_as()` didn't do that by itself
end
spec/features/albums_spec.rb
feature "Album Pages" do
given(:user) { create(:user) }
given(:album) { create(:album) }
around :each do
login_and_switch_schema user
end
scenario...
As I have some tests with js: true than I have that:
spec/support/database_cleaner.rb
RSpec.configure do |config|
config.before(:suite) do
DatabaseCleaner.clean_with(:truncation)
end
config.before(:each) do
DatabaseCleaner.strategy = :transaction
end
config.before(:each, js: true) do
DatabaseCleaner.strategy = :truncation
end
config.before(:each) do
DatabaseCleaner.start
end
config.after(:each) do
DatabaseCleaner.clean
end
end
Current commit for all sources are available at my github here.
So.. the main question is: how to clean database created schemas for each user after test ? I'll appreciate any other comment as well. Thank you in advance for help.
This isn't specific to Apartment in any way, it's more related to how DatabaseCleaner cleans your db. When using transactions, any schemas created within that transaction will be rolled back as well. Unfortunately however, you need to truncate for feature specs as transactions don't work (don't try the shared connection 'solution', it causes random failures due to a race condition). So, given that, for your feature specs, you need a way of ensuring any schemas created are deleted, since truncation only truncates tables and will NOT clean up schemas.
I'd suggest isolating your feature specs that test the multi-tenant behaviour specifically, to ensure it works the way you want it to, and manually clean up any created schemas in those specs. Then for the rest of the feature specs, assume you're testing within one tenant, or in your case one User.
We do this in our test suite, where a new Company model creates a new tenant. So we test that behaviour, for multiple tenants, then for the rest of our features we operate within 1 Company, so we don't have to worry about cleanup anymore and we can just truncate the tables within that 1 tenant. Truncate will always truncate the tables in the current tenant unless you have excluded_models.
Does that help?
Another way to deal with truncation and multi tenant applications by now is creating the tenant and deleting it on each test. Like this:
On your rails_helper.rb:
...
config.before(:suite) do
DatabaseCleaner.clean_with :truncation
DatabaseCleaner.strategy = :truncation
end
config.before(:each) do
Apartment::Tenant.drop('test') rescue nil
Company.create! name: "LittleCompany", subdomain: "test"
DatabaseCleaner.start
Apartment::Tenant.switch! "test"
end
config.after(:each) do
Apartment::Tenant.reset
DatabaseCleaner.clean
Apartment::Tenant.drop('test')
end
...
It's not very fast of course, but it's the only way i have found.
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'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.