What is the difference between the following?
DatabaseCleaner.clean_with(:truncation)
DatabaseCleaner.clean
What i'm trying to figure out is what is the best way to clean up a before(:all) hook in my tests (performance wise) using database cleaner. My before(:all) hook just creates a ton of factories and creates some associations between them. Currently, i'm just loading them all in a before(:each) not having to worry about the cleanup afterwards.
My current strategy looks like:
RSpec.configure do |config|
config.use_transactional_fixtures = false
config.before(:suite) do
DatabaseCleaner.clean_with(:truncation)
end
config.before(:each) do |example|
DatabaseCleaner.strategy = example.metadata[:js] ? :truncation : :transaction
DatabaseCleaner.start
end
config.after(:each) do
DatabaseCleaner.clean
end
So in addition to my previous question, what should my after(:all) look like?
What is the difference between the following?
DatabaseCleaner.clean_with(:truncation)
DatabaseCleaner.clean
The difference is pretty straightforward: in the first case you're telling DatabaseCleaner to clean your db now with truncation strategy, and in the second case DatabaseCleaner will clean your db using currently configured strategy.
I think your setup is pretty good already. Since creating a ton of factories (as you said) in before(:all) hook is quite rare, you just need to add to that specific test after(:all) hook to put the db back to stable state.
Cleaning with transaction won't work, since before(:all) is not wrapped in transaction.
You're left with 2 options here:
after(:all) { DatabaseCleaner.with(:truncation) }
after(:all) { DatabaseCleaner.with(:deletion) }
In order to choose between these two, as documentation clearly states, you have to measure and choose what's fastest for you, or just pick some if it doesn't matter.
Related
I am trying to test a dropdown with capybara. It works fine if I don't clean the database. I have to clean the database for other test to function properly.
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, {:except => %w[questions]}
end
config.before(:each) do
DatabaseCleaner.start
end
config.after(:each) do
DatabaseCleaner.clean
end
If I comment out the database cleaner the test must run atleast twice to pass. I can see it retry and always pass on the second try with the same criteria. Once the database cleaner is put into place the test paramenters change on each iteration so it doesn't get a chance to test it a second time.
context 'when there are questions and answers in the system' do
before do
allow_any_instance_of(Form).to receive(:create_questions)
question = create(:question)
people.map do |person|
create(:answer, question: question, person: person) if [true, false].sample
end
end
it 'should allow filtering by whether a person has answered a specific question', js: true do
#question = Question.first
select_from_dropdown #question.text, from: 'question'
click_on 'Search'
people.each do |person|
if person.questions.include? #question
expect(page).to have_content person.full_name
else
expect(page).to_not have_content person.full_name
end
end
end
end
This is the helper method which explores the drop down for capybara
def select_from_dropdown(item_text, options)
# find dropdown selector
dropdown = find_field(options[:from], visible: false).first(:xpath, './/..')
# click on dropdown
dropdown.trigger('click')
# click on menu item
dropdown.find('.menu .item', text: item_text, visible: false).trigger('click')
end
I have tried sleep throughout the select_from_dropdown thinking that maybe it wasn't loading fast enough but that is not the problem. Ideally this test should work on the first try, but at the very least it needs to pass with the database cleaner.
There is no visit shown in the test, which means you've visited the page before the questions are created and therefore the info expected is not shown on the page. The data would exist from the first run on the second run if you don't clear the DB which explains why it passes second time around. You should always visit the page after creating the test data.
Additionally as I mentioned in the comments you probably want to fix (remove if using Rails 5.1+) your DatabaseCleaner config and the use of trigger is a really bad idea in tests since it can do things a user never could.
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'm a little lost with RSpec having always stuck to xUnit-based testing frameworks, but I'm giving it a go.
The nested nature of the way specs are written is giving me some headaches with regards to where I'm supposed to do database setup/teardown though.
According the to DatabaseCleaner README:
Spec::Runner.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
Now I can't use transactions, because I use them in my code, so I'm just sticking to truncation, but that should be neither here nor there.
I have this:
RSpec.configure do |config|
config.mock_with :rspec
config.before(:suite) do
DatabaseCleaner.strategy = :truncation
end
config.before(:each) do
DatabaseCleaner.start
end
config.after(:each) do
DatabaseCleaner.clean
end
end
The problem here is that any fixtures I create in a subject or let block have already disappeared (from the database) when I try to use them in a following describe or it block.
For example (using Machinist to create the fixtures... but that shouldn't be relevant):
describe User do
describe "finding with login credentials" do
let(:user) { User.make(:username => "test", :password => "password") }
subject { User.get_by_login_credentials!("test", "password") }
it { should == user }
end
end
I'm struggling with how I'm supposed to be nesting these describe and subject and other blocks, so maybe that's my problem, but basically this fails because when it tries to get the user from the database, it's already been removed due to the after(:each) hook being invoked, presumably after the let?
If you're going to use subject and let together, you need to understand how/when they are invoked. In this case, subject is invoked before the user method generated by let. The problem is not that the object is removed from the db before subject is invoked, but that it is not even created at that point.
Your example would work if you use the let! method, which adds a before hook that implicitly invokes the user method before the example (and therefore before subject is invoked).
That said, I'd recommend you stop struggling and use simpler API's that RSpec already exposes:
describe User do
it "finds a user with login credentials" do
user = User.make(:username => "test", :password => "password")
User.get_by_login_credentials!("test", "password").should eq(user)
end
end
That seems much simpler to me.
You wrote:
The problem here is that any fixtures I create in a subject or let block have already disappeared (from the database) when I try to use them in a following describe or it block.
That's right, that's how it works. (And you're not using fixtures in the usual Rails sense, but factories -- just as well, since Rails fixtures suck.)
Every individual spec (that is, every it block) starts (or should start) from a pristine database. Otherwise your tests would leak state and lose atomicity. So you should create every record you need within the spec in which you need it (or, as David said, in a before block to cut down on repetition).
As for organizing your specs...do it any way that makes sense. Usually there will be an outer describe block for the whole class, with inner describe blocks for groups of related behavior or specs that need a common setup. Each describe context can have its own before and after blocks. These get nested as you would expect, so the order of execution is something like
outer before
inner before
spec
inner after
outer after
If you'd like to see a project with a large number of RSpec specs and Cucumber stories (though for slightly old versions of each), check out http://github.com/marnen/quorum2 .
How would go about writing proper unit testing (and integration testing for that matter) using MongoDB through Mongoid on Rails ?
I am asking, because to the opposite of using let's say SQLite3, even when running tests, everything I do does persists. So for the moment I am writing the creation test and then I manually delete everything I do. But it's getting annoying and even complicated to do for integration testing.
Sample of what I do:
before(:each) do
#user = User.create!(#attr)
end
after(:each) do
# MongoDB is not a transactional DB, so added objects (create) during tests can't be rollbacked
# checking for the existance of a similar object with exact :name and :email (regex make it case insensitive)
cleanup = User.where(:name => "Example User", :email => /^user#example.com/i)
cleanup.destroy unless cleanup.nil?
end
Any idea how to make MongoDB not persistent during Testing ? (I can't even run the console in sandbox mode because to use Mongoid I had to deactivate Active Record).
Ok thanks to Kyle who pointed me in the right direction, I found out how to make it work.
So basically the trick is to drop all your collections in mongodb for each test case that you will run. This is a bit radical, but it works. But keep in mind that you won't retain any data at all in you test db.
Finally I found that link: http://adventuresincoding.com/2010/07/how-to-configure-cucumber-and-rspec-to-work-with-mongoid
And basically what you need to do is simple:
add a block in you spec_helper.rb:
RSpec.configure do |config|
# blabla other confs
config.before :each do
Mongoid.master.collections.select {|c| c.name !~ /system/ }.each(&:drop)
end
# blabla other confs
end
For Mongoid 3:
Mongoid.default_session.collections.select {|c| c.name !~ /system/ }.each(&:drop
This effectively kills all the collection within the db allowing you to run your tests fresh every time.
Alex
Another way is to use database_cleaner. It supports multiple ORMs, so I think you could do something like this:
# spec/support/database_cleaner.rb
RSpec.configure do |config|
config.before(:suite) do
DatabaseCleaner[:mongoid].strategy = :truncation
DatabaseCleaner[:mongoid].clean_with(:truncation)
end
config.before(:each) do
DatabaseCleaner.start
end
config.after(:each) do
DatabaseCleaner.clean
end
end
There's no way to make MongoDB non-persistent. You simply have to delete data before or after each test. There's some documentation on that here:
http://www.mongodb.org/display/DOCS/Rails+-+Getting+Started#Rails-GettingStarted-Testing
Here's what I did (using Test::Unit and Mongoid 3)...
# test/test_helper.rb
setup {
Mongoid.default_session.collections.select {|c| c.name !~ /system/ }.each(&:drop)
}
This one worked for me.
In spec_helper.rb, under RSpec.configure do |config| I put:
config.before :each do
Mongoid.purge!
end
Reference.