I've got an Angular 1.5 app with a Rails 4.0 backend and I've got this problem where in my tests once a Staff member logs in 3 queries are sent to the backend at the same time. In testing (using Capybara + selenium + chromeheadless) this causes these requests result in errors on the backend. In development it works fine.
An example of the error;
NoMethodError at /entities/get_current_entity
=============================================
> undefined method `fields' for nil:NilClass
app/controllers/application_controller.rb, line 79
--------------------------------------------------
``` ruby
74
75 private
76
77 def set_locale
78 #I18n.locale = params[:locale] if params[:locale].present?
> 79 if current_staff.nil? or current_staff.locale.nil?
80 I18n.locale = "en"
81 else
82 I18n.locale = current_staff.locale
83 end
84 end
```
App backtrace
-------------
- app/controllers/application_controller.rb:79:in `set_locale'
<Snip>
The test which fails
require "rails_helper"
RSpec.describe "Login", :type => :feature do
describe "login with existing staff member" do
before do
create_admin
end
it "logs in as admin" do
visit "#/login"
fill_in "Email", with: "admin#test.com.au"
fill_in "Password", with: "password"
click_button "Login"
within "#staff-status" do
expect(page).to have_text('LOGOUT')
end
within("#flash-messages") do
expect(page).to have_text('Successful login')
end
# Check session variables
within("#selected-entity") do
expect(page).to have_text("LOGIN DEFAULT COMPANY")
end
within("#selected-ledger") do
expect(page).to have_text("My Ledger")
end
within("#selected-division") do
expect(page).to have_text("1-General")
end
end
end
end
Here is the code which causes the pending calls
$scope.$on('devise:new-session', (event, currentStaff) ->
# staff logged in by Auth.login({...})
Auth.currentUser().then((staff) ->
FirmService.current_entity($rootScope)
DivisionService.current_division($rootScope)
LedgerService.current_ledger($rootScope)
$rootScope.current_staff = staff
, (error) ->
console.log("fail")
)
$location.path "/dashboard"
)
This happens all through the application and didn't happen on Rails 3.2. It's only since I'm going through this upgrade to Rails 4 that this has started to happen.
If I use promises then this problem doesn't happen. I cannot always use promises to get the data and this slows down the application also.
Here is the workaround for the login;
Auth.currentUser().then((staff) ->
FirmService.current_entity($rootScope).then ->
DivisionService.current_division($rootScope).then ->
LedgerService.current_ledger($rootScope)
How can I get Rails 4 to handle multiple requests like this in testing mode?
Update: Run server in test environment
I ran my server in the test environment to try and replicate the problem manually but it worked when doing it this way. So it must have something to do with either the speed of the test or capybara itself?
Update: Updated Gems
I updated to capybara 2.15.2, selenium-webdriver 3.6.0 & chromedriver-helper 1.1.0. This didn't help.
Update: Errors
Sometimes the error changes, I got an error 'Staff not logged in' on one of my requests even though the user is successfully logged in.
Update: Debugging
Inserting 'require "pry";binding.pry' into the Rails controllers where the AngularJS calls end up doesn't work as expected. I have three calls in the AngularJS code and I expect that Rails will hit the debug lines three times. The first time I ran it with the debug lines it only hit one of them, the second time it didn't hit any of the debug lines. Instead I got that NoMethodError at /entities/get_current_entity and the same for division and ledger.
Update: Looking into pry hanging
If I put a binding.pry into the set_locale method and call current_staff.locale.nil? I have found that sometimes pry will hang there forever.
After remarking out the set_locale contents it continues on and still fails and in the test.log file I get these errors;
Completed 500 Internal Server Error in 4690ms
Completed 500 Internal Server Error in 4688ms
Completed 500 Internal Server Error in 4686ms
NoMethodError - undefined method `fields' for nil:NilClass:
<snip>
NoMethodError - undefined method `fields' for nil:NilClass:
<snip>
NoMethodError - undefined method `collect' for nil:NilClass:
<snip>
(1.3ms) ALTER TABLE "indirect_taxes" DISABLE TRIGGER ALL;ALTER TABLE "ledger_defaults" DISABLE TRIGGER ALL;ALTER TABLE "ledgers" DISABLE TRIGGER ALL;ALTER TABLE "levels" DISABLE TRIGGER ALL;ALTER TABLE "locations" DISABLE TRIGGER ALL;ALTER TABLE "phones" DISABLE TRIGGER ALL;ALTER TABLE "sessions" DISABLE TRIGGER ALL;ALTER TABLE "transactions" DISABLE TRIGGER ALL;ALTER TABLE "versions" DISABLE TRIGGER ALL;ALTER TABLE "schema_migrations" DISABLE TRIGGER ALL;ALTER TABLE "staff" DISABLE TRIGGER ALL;ALTER TABLE "tickets" DISABLE TRIGGER ALL;ALTER TABLE "timesheets" DISABLE TRIGGER ALL;ALTER TABLE "websites" DISABLE TRIGGER ALL;ALTER TABLE "customers" DISABLE TRIGGER ALL;ALTER TABLE "costings" DISABLE TRIGGER ALL;ALTER TABLE "disbursements" DISABLE TRIGGER ALL;ALTER TABLE "employees" DISABLE TRIGGER ALL;ALTER TABLE "entities" DISABLE TRIGGER ALL;ALTER TABLE "folios" DISABLE TRIGGER ALL;ALTER TABLE "work_descs" DISABLE TRIGGER ALL;ALTER TABLE "account_groups" DISABLE TRIGGER ALL;ALTER TABLE "accounts" DISABLE TRIGGER ALL;ALTER TABLE "addresses" DISABLE TRIGGER ALL;ALTER TABLE "business_types" DISABLE TRIGGER ALL;ALTER TABLE "charges" DISABLE TRIGGER ALL;ALTER TABLE "comments" DISABLE TRIGGER ALL;ALTER TABLE "invoices" DISABLE TRIGGER ALL;ALTER TABLE "jobs" DISABLE TRIGGER ALL;ALTER TABLE "divisions" DISABLE TRIGGER ALL;ALTER TABLE "emails" DISABLE TRIGGER ALL
(411.5ms) TRUNCATE TABLE "public"."indirect_taxes", "public"."ledger_defaults", "public"."ledgers", "public"."levels", "public"."locations", "public"."phones", "public"."sessions", "public"."transactions", "public"."versions", "public"."staff", "public"."tickets", "public"."timesheets", "public"."websites", "public"."customers", "public"."costings", "public"."disbursements", "public"."employees", "public"."entities", "public"."folios", "public"."work_descs", "public"."account_groups", "public"."accounts", "public"."addresses", "public"."business_types", "public"."charges", "public"."comments", "public"."invoices", "public"."jobs", "public"."divisions", "public"."emails" RESTART IDENTITY CASCADE;
(1.9ms) ALTER TABLE "indirect_taxes" ENABLE TRIGGER ALL;ALTER TABLE "ledger_defaults" ENABLE TRIGGER ALL;ALTER TABLE "ledgers" ENABLE TRIGGER ALL;ALTER TABLE "levels" ENABLE TRIGGER ALL;ALTER TABLE "locations" ENABLE TRIGGER ALL;ALTER TABLE "phones" ENABLE TRIGGER ALL;ALTER TABLE "sessions" ENABLE TRIGGER ALL;ALTER TABLE "transactions" ENABLE TRIGGER ALL;ALTER TABLE "versions" ENABLE TRIGGER ALL;ALTER TABLE "staff" ENABLE TRIGGER ALL;ALTER TABLE "tickets" ENABLE TRIGGER ALL;ALTER TABLE "timesheets" ENABLE TRIGGER ALL;ALTER TABLE "schema_migrations" ENABLE TRIGGER ALL;ALTER TABLE "websites" ENABLE TRIGGER ALL;ALTER TABLE "customers" ENABLE TRIGGER ALL;ALTER TABLE "costings" ENABLE TRIGGER ALL;ALTER TABLE "disbursements" ENABLE TRIGGER ALL;ALTER TABLE "employees" ENABLE TRIGGER ALL;ALTER TABLE "entities" ENABLE TRIGGER ALL;ALTER TABLE "folios" ENABLE TRIGGER ALL;ALTER TABLE "work_descs" ENABLE TRIGGER ALL;ALTER TABLE "account_groups" ENABLE TRIGGER ALL;ALTER TABLE "accounts" ENABLE TRIGGER ALL;ALTER TABLE "addresses" ENABLE TRIGGER ALL;ALTER TABLE "business_types" ENABLE TRIGGER ALL;ALTER TABLE "charges" ENABLE TRIGGER ALL;ALTER TABLE "comments" ENABLE TRIGGER ALL;ALTER TABLE "invoices" ENABLE TRIGGER ALL;ALTER TABLE "jobs" ENABLE TRIGGER ALL;ALTER TABLE "divisions" ENABLE TRIGGER ALL;ALTER TABLE "emails" ENABLE TRIGGER ALL
I think that last bit with the Truncate table is the database cleaner trying to clean up the mess. So those three Server Errors I get seem to be timing out. Why would the database / Rails stop listening to the queries?
pry will also hang within the set_locale method when I type a simple query
Staff.all
Update: strace output
I've taken the strace output from when pry hangs;
https://gist.github.com/map7/f25f75457f1a6ae6995934e2c2744660
After chatting for a bit it turns out that a patch to share the DB connection between multiple threads was being used. The problem is, that can create all sorts of issue with multiple simultaneous requests, and generally lead to potential flakiness in tests. Removing the patch fixed the tests.
Once you migrate your way all the way up to Rails 5.1, then Rails handles the sharing of the connection in the test environment through mutexes, allowing transactional feature testing, and obviating the need for database_cleaner
Related
I am trying to test a feature in my Rails app using RSpec + Polgergeist.
My feature spec is very basic right now:
RSpec.feature 'Listing owners can create new listings' do
let(:owner) { create(:user, :owner) }
before do
login_as owner
# I've also tried this with as recommended by Devise when using Capybara-Webkit:
# login_as(user, scope: :user, run_callbacks: false)
visit new_listing_path
end
scenario 'successfully', js: true do
fill_in 'Field 1', with: 'Example'
click_link 'Create'
# expectations here...
end
end
I have the following in my rails_helper.rb
require 'capybara/poltergeist'
RSpec.configure do |config|
# ...
config.include Warden::Test::Helpers, type: :feature
config.after(type: :feature) { Warden.test_reset! }
# ...
end
and in a support/poltergeist.rb file
RSpec.configure do |config|
Capybara.javascript_driver = :poltergeist
# Increase timeout in case asset compilation
# takes longer than anticipated.
Capybara.register_driver :poltergeist do |app|
Capybara::Poltergeist::Driver.new(app, {timeout: 60})
end
end
The precise location that is failing is this, where I set the CSRF token using Vue.js:
import Vue from 'vue/dist/vue.esm'
import VueResource from 'vue-resource'
Vue.use(VueResource)
document.addEventListener('DOMContentLoaded', () => {
if(document.getElementById('listing-multistep') !== null) {
Vue.http.headers.common['X-CSRF-Token'] = document.querySelector('input[name="authenticity_token"]').getAttribute('value');
var listingForm = document.getElementById('listing_form');
var listing = JSON.parse(listingForm.dataset.listing);
// do magic....
}
});
The authenticity token is set from the Rails form partial, _form.html.erb:
<%= form_for(
listing,
html: { multipart: true, id: 'listing_form',
data: {
listing: listing.to_json(except: [:user_id, :created_at, :updated_at])
}
}) do |f| %>
<fieldset class="listing-step" v-show="activeStep === 0">
<%= render 'listings/form/basics', f: f, listing: listing %>
</fieldset>
<fieldset class="listing-step" v-show="activeStep === 1">
<%= render 'listings/form/location', f: f, listing: listing %>
</fieldset>
<fieldset class="listing-step" v-show="activeStep === 2">
<%= render 'listings/form/amenities', f: f, listing: listing %>
</fieldset>
<fieldset class="listing-step" v-show="activeStep === 3">
<%= render 'listings/form/description', f: f, listing: listing %>
</fieldset>
<% end %>
Setting the CSRF token this way seems to work in development, but when I run the spec, I am seeing the following error:
Failure/Error: visit new_listing_path
Capybara::Poltergeist::JavascriptError:
One or more errors were raised in the Javascript code on the page. If you don't care about these errors, you can ignore them by setting js_errors: false in your Poltergeist configuration (see documentation for details).
TypeError: null is not an object (evaluating 'document.querySelector('input[name="authenticity_token"]').getAttribute')
TypeError: null is not an object (evaluating 'document.querySelector('input[name="authenticity_token"]').getAttribute')
at http://127.0.0.1:54450/packs-test/dashboard-ca7eeed2cc4b22399ade.js:13086
All I can think of is that poltergeist isn't waiting for the DOM to load fully, but that would be strange because I have explicitly written the vue code within document.addEventListener('DOMContentLoaded', () => {
As an aside, I also have a support/database_cleaner.rb file with the following contents:
RSpec.configure do |conf|
conf.before(:suite) do
DatabaseCleaner.clean_with :truncation
end
conf.before(:each) do
DatabaseCleaner.strategy = :transaction
end
conf.before(:each, js: true) do
DatabaseCleaner.strategy = :truncation
end
conf.before(:each) do
DatabaseCleaner.start
end
conf.after(:each) do
DatabaseCleaner.clean
end
end
Any idea what may be causing the aforementioned error? Any help is much appreciated!
Thanks in advance
UPDATE
Here is a copy of the test.log file that is generated when I run the spec:
(1.3ms) SELECT "schema_migrations"."version" FROM "schema_migrations" ORDER BY "schema_migrations"."version" ASC
(0.2ms) BEGIN
(2.1ms) ALTER TABLE "schema_migrations" DISABLE TRIGGER ALL;ALTER TABLE "ar_internal_metadata" DISABLE TRIGGER ALL;ALTER TABLE "subscriptions" DISABLE TRIGGER ALL;ALTER TABLE "listings" DISABLE TRIGGER ALL;ALTER TABLE "advertisements" DISABLE TRIGGER ALL;ALTER TABLE "chatroom_users" DISABLE TRIGGER ALL;ALTER TABLE "chatrooms" DISABLE TRIGGER ALL;ALTER TABLE "messages" DISABLE TRIGGER ALL;ALTER TABLE "pictures" DISABLE TRIGGER ALL;ALTER TABLE "friendly_id_slugs" DISABLE TRIGGER ALL;ALTER TABLE "contact_messages" DISABLE TRIGGER ALL;ALTER TABLE "users" DISABLE TRIGGER ALL
(0.7ms) COMMIT
(3.9ms) SELECT schemaname || '.' || tablename
FROM pg_tables
WHERE
tablename !~ '_prt_' AND
tablename <> 'schema_migrations' AND tablename <> 'ar_internal_metadata' AND
schemaname = ANY (current_schemas(false))
(2.5ms) select table_name from information_schema.views where table_schema = 'salayo_test'
(48.9ms) TRUNCATE TABLE "public"."subscriptions", "public"."listings", "public"."advertisements", "public"."chatroom_users", "public"."chatrooms", "public"."messages", "public"."pictures", "public"."friendly_id_slugs", "public"."contact_messages", "public"."users" RESTART IDENTITY CASCADE;
(1.3ms) BEGIN
(1.7ms) ALTER TABLE "schema_migrations" ENABLE TRIGGER ALL;ALTER TABLE "ar_internal_metadata" ENABLE TRIGGER ALL;ALTER TABLE "subscriptions" ENABLE TRIGGER ALL;ALTER TABLE "listings" ENABLE TRIGGER ALL;ALTER TABLE "advertisements" ENABLE TRIGGER ALL;ALTER TABLE "chatroom_users" ENABLE TRIGGER ALL;ALTER TABLE "chatrooms" ENABLE TRIGGER ALL;ALTER TABLE "messages" ENABLE TRIGGER ALL;ALTER TABLE "pictures" ENABLE TRIGGER ALL;ALTER TABLE "friendly_id_slugs" ENABLE TRIGGER ALL;ALTER TABLE "contact_messages" ENABLE TRIGGER ALL;ALTER TABLE "users" ENABLE TRIGGER ALL
(0.6ms) COMMIT
(0.3ms) BEGIN
User Exists (2.0ms) SELECT 1 AS one FROM "users" WHERE "users"."email" = $1 LIMIT $2 [["email", "dianna_kilback#okeefemccullough.org"], ["LIMIT", 1]]
SQL (1.1ms) INSERT INTO "users" ("first_name", "last_name", "email", "encrypted_password", "created_at", "updated_at", "role") VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING "id" [["first_name", "Hermann"], ["last_name", "VonRueden"], ["email", "dianna_kilback#okeefemccullough.org"], ["encrypted_password", "$2a$04$sSAdYthaHRhqjUqaing/weltoDcyHyR5Fyr2Pw/8xOtvz4mMPwWYm"], ["created_at", "2018-04-01 05:32:07.693349"], ["updated_at", "2018-04-01 05:32:07.693349"], ["role", 2]]
(0.6ms) COMMIT
Started GET "/en/listings/new" for 127.0.0.1 at 2018-04-01 14:32:09 +0900
(0.2ms) BEGIN
SQL (2.5ms) UPDATE "users" SET "sign_in_count" = $1, "current_sign_in_at" = $2, "last_sign_in_at" = $3, "current_sign_in_ip" = $4, "last_sign_in_ip" = $5, "updated_at" = $6 WHERE "users"."id" = $7 [["sign_in_count", 1], ["current_sign_in_at", "2018-04-01 05:32:09.833321"], ["last_sign_in_at", "2018-04-01 05:32:09.833321"], ["current_sign_in_ip", "127.0.0.1/32"], ["last_sign_in_ip", "127.0.0.1/32"], ["updated_at", "2018-04-01 05:32:09.866550"], ["id", 1]]
(0.6ms) COMMIT
Processing by ListingsController#new as HTML
Parameters: {"locale"=>"en"}
Rendering listings/new.html.erb within layouts/dashboard
Rendered listings/_prev_next_buttons.html.erb (3.6ms)
Rendered listings/form/_basics.html.erb (30.6ms)
Rendered listings/form/_location.html.erb (2.9ms)
Rendered listings/form/_amenities.html.erb (0.5ms)
Rendered listings/form/_description.html.erb (3.5ms)
Rendered listings/_form.html.erb (79.4ms)
Rendered listings/new.html.erb within layouts/dashboard (127.9ms)
Rendered common/_meta_tags.html.erb (1.2ms)
Rendered dashboard/_sidebar.html.erb (23.4ms)
Rendered dashboard/_topbar.html.erb (4.7ms)
Rendered common/_notices.html.erb (2.0ms)
Rendered common/_footer.html.erb (4.9ms)
Completed 200 OK in 980ms (Views: 877.4ms | ActiveRecord: 19.7ms)
Started GET "/assets/application-a2d7fced50ec51954f27ed1fab4792ab0cd3b9b88ff496fc07bd2489660a7c42.css" for 127.0.0.1 at 2018-04-01 14:32:10 +0900
Started GET "/assets/application-e7598b8619f2ba6c7b08ce9a5471f5e4b5223c384a09f8826ae19a864a9bc6b4.js" for 127.0.0.1 at 2018-04-01 14:32:10 +0900
Started GET "/cable" for 127.0.0.1 at 2018-04-01 14:32:11 +0900
Started GET "/assets/bg_logo-db2dd881e08b614d8971e623eb0a14df67c93a5615c04ec2d7c2967893d44b2c.svg" for 127.0.0.1 at 2018-04-01 14:32:11 +0900
Started GET "/cable/" [WebSocket] for 127.0.0.1 at 2018-04-01 14:32:11 +0900
Successfully upgraded to WebSocket (REQUEST_METHOD: GET, HTTP_CONNECTION: Upgrade, HTTP_UPGRADE: websocket)
Started GET "/assets/fontawesome-webfont-ba0c59deb5450f5cb41b3f93609ee2d0d995415877ddfa223e8a8a7533474f07.woff" for 127.0.0.1 at 2018-04-01 14:32:11 +0900
(0.3ms) BEGIN
(1.6ms) ALTER TABLE "schema_migrations" DISABLE TRIGGER ALL;ALTER TABLE "ar_internal_metadata" DISABLE TRIGGER ALL;ALTER TABLE "subscriptions" DISABLE TRIGGER ALL;ALTER TABLE "listings" DISABLE TRIGGER ALL;ALTER TABLE "advertisements" DISABLE TRIGGER ALL;ALTER TABLE "chatroom_users" DISABLE TRIGGER ALL;ALTER TABLE "chatrooms" DISABLE TRIGGER ALL;ALTER TABLE "messages" DISABLE TRIGGER ALL;ALTER TABLE "pictures" DISABLE TRIGGER ALL;ALTER TABLE "friendly_id_slugs" DISABLE TRIGGER ALL;ALTER TABLE "contact_messages" DISABLE TRIGGER ALL;ALTER TABLE "users" DISABLE TRIGGER ALL
(0.5ms) COMMIT
(56.7ms) TRUNCATE TABLE "public"."subscriptions", "public"."listings", "public"."advertisements", "public"."chatroom_users", "public"."chatrooms", "public"."messages", "public"."pictures", "public"."friendly_id_slugs", "public"."contact_messages", "public"."users" RESTART IDENTITY CASCADE;
(1.7ms) BEGIN
(2.7ms) ALTER TABLE "schema_migrations" ENABLE TRIGGER ALL;ALTER TABLE "ar_internal_metadata" ENABLE TRIGGER ALL;ALTER TABLE "subscriptions" ENABLE TRIGGER ALL;ALTER TABLE "listings" ENABLE TRIGGER ALL;ALTER TABLE "advertisements" ENABLE TRIGGER ALL;ALTER TABLE "chatroom_users" ENABLE TRIGGER ALL;ALTER TABLE "chatrooms" ENABLE TRIGGER ALL;ALTER TABLE "messages" ENABLE TRIGGER ALL;ALTER TABLE "pictures" ENABLE TRIGGER ALL;ALTER TABLE "friendly_id_slugs" ENABLE TRIGGER ALL;ALTER TABLE "contact_messages" ENABLE TRIGGER ALL;ALTER TABLE "users" ENABLE TRIGGER ALL
(0.5ms) COMMIT
Here I'm testing the changes in current_user.messages.count after the current user sends a valid message. Here's my code:
spec
scenario 'adds to their messages', js: true do
expect { find('#message_content').send_keys(:enter) }.to \
change(current_user.messages, :count).by(1)
end
test.log
# ...
ConversationChannel is transmitting the subscription confirmation
ConversationChannel is streaming from conversation_channel_1
(0.6ms) SELECT COUNT(*) FROM "messages" WHERE "messages"."user_id" = $1 [["user_id", 1]]
ConversationChannel#send_message({"content"=>"foobar\n", "conversation_id"=>"1"})
(0.3ms) BEGIN
(0.9ms) SELECT COUNT(*) FROM "messages" WHERE "messages"."user_id" = $1 [["user_id", 1]]
Conversation Load (1.6ms) SELECT "conversations".* FROM "conversations" WHERE "conversations"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
(0.7ms) SELECT "users"."id" FROM "users" INNER JOIN "user_conversations" ON "users"."id" = "user_conversations"."user_id" WHERE "user_conversations"."conversation_id" = $1 [["conversation_id", 1]]
SQL (1.0ms) INSERT INTO "messages" ("content", "user_id", "conversation_id", "created_at", "updated_at") VALUES ($1, $2, $3, $4, $5) RETURNING "id" [["content", "foobar\n"], ["user_id", 1], ["conversation_id", 1], ["created_at", "2018-01-29 11:27:13.095277"], ["updated_at", "2018-01-29 11:27:13.095277"]]
Finished "/cable/" [WebSocket] for 127.0.0.1 at 2018-01-29 19:27:13 +0800
ConversationChannel stopped streaming from conversation_channel_1
(0.2ms) BEGIN
(58.8ms) COMMIT
(16.7ms) ALTER TABLE "schema_migrations" DISABLE TRIGGER ALL;ALTER TABLE "ar_internal_metadata" DISABLE TRIGGER ALL;ALTER TABLE "conversations" DISABLE TRIGGER ALL;ALTER TABLE "messages" DISABLE TRIGGER ALL;ALTER TABLE "user_conversations" DISABLE TRIGGER ALL;ALTER TABLE "users" DISABLE TRIGGER ALL
Rendered messages/_message.html.erb (0.6ms)
[ActionCable] Broadcasting to conversation_channel_1: {:message=>"<p>User 1: foobar\n</p>\n"}
# ...
The spec fails expected #count to have changed by 1, but was changed by 0 even though in the log shows INSERT INTO actually happen.
This doesn't work because you're not waiting long enough for the message addition to actually occur. send_keys returns as soon as the browser has been sent the key event, but knows nothing at all about any request/action triggered by that key press in the browser. This is why direct DB access tests are generally a bad idea in feature/system tests (which should generally just test user visible changes/interactions) and make more sense as request or controller.
That being said you could fix this by just sleeping after sending the key, but a better solution is to use one of the Capybara provided matchers (have waiting/retrying behavior) to synchronize the test.
scenario 'adds to their messages', js: true do
expect do
find('#message_content').send_keys(:enter) }
expect(page).to have_css(...) # check for whatever visible change on the page indicates the action triggered by send_keys has completed
end.to change { current_user.reload.messages.count }.by(1)
end
Note: This test is also very simple for a feature test. It's okay to have multiple expectations in a feature test since it's really meant to test a whole user interaction with a specific feature of your app. You might want to look at combining this test with other tests of the same part of your app.
Try to write :
change{current_user.messages, :count}.by(1)
with {}
So I have a slightly unconventional Rails 4.2 application. There are no migrations associated with the models you see in the below statement(s). Instead to create the database tables, a db/car.sql was used.
But now when I run bundle exec rspec spec/models I get the below output. What is causing this output to appear? As far as I can tell the schema.rb file looks the same as if I had built it up using rails migration generator. There's a few database indexes but that's it.
rspec output
ActiveRecord::SchemaMigration Load (0.2ms) SELECT "schema_migrations".* FROM "schema_migrations"
(0.4ms) ALTER TABLE "kites" DISABLE TRIGGER ALL;ALTER TABLE "schema_migrations" DISABLE TRIGGER ALL;ALTER TABLE "tails" DISABLE TRIGGER ALL;ALTER TABLE "cars" DISABLE TRIGGER ALL;ALTER TABLE "car_washes" DISABLE TRIGGER ALL;ALTER TABLE "cars_engines" DISABLE TRIGGER ALL;ALTER TABLE "cities" DISABLE TRIGGER ALL;ALTER TABLE "engines" DISABLE TRIGGER ALL;ALTER TABLE "wheels" DISABLE TRIGGER ALL;ALTER TABLE "cars_wheels" DISABLE TRIGGER ALL
(0.4ms) ALTER TABLE "kites" DISABLE TRIGGER USER;ALTER TABLE "schema_migrations" DISABLE TRIGGER USER;ALTER TABLE "tails" DISABLE TRIGGER USER;ALTER TABLE "cars" DISABLE TRIGGER USER;ALTER TABLE "car_washes" DISABLE TRIGGER USER;ALTER TABLE "cars_engines" DISABLE TRIGGER USER;ALTER TABLE "cities" DISABLE TRIGGER USER;ALTER TABLE "engines" DISABLE TRIGGER USER;ALTER TABLE "wheels" DISABLE TRIGGER USER;ALTER TABLE "cars_wheels" DISABLE TRIGGER USER
(1.0ms) SELECT schemaname || '.' || tablename
FROM pg_tables
WHERE
tablename !~ '_prt_' AND
tablename <> 'schema_migrations' AND
schemaname = ANY (current_schemas(false))
(0.8ms) select table_name from information_schema.views where table_schema = 'automobile_test'
(758.2ms) TRUNCATE TABLE "public"."kites", "public"."tails", "public"."cars", "public"."car_washes", "public"."cars_engines", "public"."cities", "public"."engines", "public"."wheels", "public"."cars_wheels" RESTART IDENTITY CASCADE;
(0.4ms) ALTER TABLE "kites" ENABLE TRIGGER ALL;ALTER TABLE "schema_migrations" ENABLE TRIGGER ALL;ALTER TABLE "tails" ENABLE TRIGGER ALL;ALTER TABLE "car_washes" ENABLE TRIGGER ALL;ALTER TABLE "cars_engines" ENABLE TRIGGER ALL;ALTER TABLE "cities" ENABLE TRIGGER ALL;ALTER TABLE "engines" ENABLE TRIGGER ALL;ALTER TABLE "cars" ENABLE TRIGGER ALL;ALTER TABLE "wheels" ENABLE TRIGGER ALL;ALTER TABLE "cars_wheels" ENABLE TRIGGER ALL
(0.5ms) ALTER TABLE "kites" ENABLE TRIGGER USER;ALTER TABLE "schema_migrations" ENABLE TRIGGER USER;ALTER TABLE "tails" ENABLE TRIGGER USER;ALTER TABLE "car_washes" ENABLE TRIGGER USER;ALTER TABLE "cars_engines" ENABLE TRIGGER USER;ALTER TABLE "cities" ENABLE TRIGGER USER;ALTER TABLE "engines" ENABLE TRIGGER USER;ALTER TABLE "cars" ENABLE TRIGGER USER;ALTER TABLE "wheels" ENABLE TRIGGER USER;ALTER TABLE "cars_wheels" ENABLE TRIGGER USER
(0.1ms) BEGIN
(0.1ms) SAVEPOINT active_record_1
(0.1ms) RELEASE SAVEPOINT active_record_1
(0.1ms) SAVEPOINT active_record_1
(0.2ms) ROLLBACK TO SAVEPOINT active_record_1
(0.1ms) ROLLBACK
I have a Rails 4 app with simple specs. When I run a spec, it takes 26 seconds. If I look at the log, I can see this :
(38.5ms) ALTER TABLE "schema_migrations" DISABLE TRIGGER ALL;ALTER TABLE .............# tons of tables
(10.3ms) SELECT schemaname || '.' || tablename
FROM pg_tables
WHERE
tablename !~ '_prt_' AND
tablename <> 'schema_migrations' AND
schemaname = ANY (current_schemas(false))
(2.3ms) select table_name from information_schema.views where table_schema = 'lap-test'
(10708.2ms) TRUNCATE TABLE "public"."bounded_facets_service_boutiques", "public"."versions", ..... # Tons of tables
(10823.0ms) TRUNCATE TABLE "public"."bounded_facets_service_boutiques", "public"."versions", .... #Tons of tables
(26.2ms) ALTER TABLE "schema_migrations" ENABLE TRIGGER ALL;ALTER TABLE "bounded_facets_service_boutiques" ENABLE TRIGGER ALL;ALTER TABLE "versions" .... #Tons of tables
(0.6ms) BEGIN
(0.3ms) COMMIT
(0.2ms) BEGIN
(0.2ms) SAVEPOINT active_record_1
(0.2ms) RELEASE SAVEPOINT active_record_1
(0.2ms) SAVEPOINT active_record_1
(0.5ms) SAVEPOINT active_record_2
Administrator Exists (1.9ms) SELECT 1 AS one FROM "administrators" WHERE "administrators"."email" = 'person1#labandprocess.com' LIMIT 1
SQL (2.3ms) INSERT INTO "administrators" .....
(0.6ms) RELEASE SAVEPOINT active_record_2
(0.4ms) ROLLBACK TO SAVEPOINT active_record_1
(0.6ms) ROLLBACK
As you can see, The truncation takes 10 seconds 2 times.
I use database cleaner with this :
RSpec.configure do |config|
config.include Devise::TestHelpers, type: :controller
config.before(:suite) do
DatabaseCleaner.strategy = :transaction
DatabaseCleaner.clean_with(:truncation)
end
config.around(:each) do |example|
DatabaseCleaner.cleaning do
example.run
end
end
end
I tested with other strategies without result.
There is a lot of seeds in development mod but it's on another database. There is 107 models in this project.
Is there a solution ?
Setting use_transactional_fixtures=false in my spec_helper.rb file, though when I look at the output of my test log, it appears a transaction is still being used? Why is that the case and how do I eliminate the transaction?
rails_helper.rb...
# This file is copied to spec/ when you run 'rails generate rspec:install'
ENV["RAILS_ENV"] ||= 'test'
require 'spec_helper'
require File.expand_path("../../config/environment", __FILE__)
require 'rspec/rails'
# Add additional requires below this line. Rails is not loaded until this point!
ActiveRecord::Migration.maintain_test_schema!
RSpec.configure do |config|
# Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
config.fixture_path = "#{::Rails.root}/spec/fixtures"
config.use_transactional_fixtures = false
config.infer_spec_type_from_file_location!
config.include FactoryGirl::Syntax::Methods
end
spec_helper.rb...
require 'devise'
require "paperclip/matchers"
require 'simplecov'
require 'database_cleaner'
require 'capybara/rspec'
SimpleCov.start 'rails'
module SphinxHelpers
def index
ThinkingSphinx::Test.index
# Wait for Sphinx to finish loading in the new index files.
sleep 0.25 until index_finished?
end
def index_finished?
Dir[Rails.root.join(ThinkingSphinx::Test.config.indices_location, '*.{new,tmp}*')].empty?
end
end
RSpec.configure do |config|
config.include SphinxHelpers
config.before(:suite) do
# Ensure sphinx directories exist for the test environment
ThinkingSphinx::Test.init
# Configure and start Sphinx, and automatically
# stop Sphinx at the end of the test suite.
ThinkingSphinx::Test.start_with_autostop
end
config.before(:each) do |example|
index
end
config.include Paperclip::Shoulda::Matchers
config.include Devise::TestHelpers, :type => :controller
config.raise_errors_for_deprecations!
config.before(:suite) do
DatabaseCleaner.strategy = :transaction
DatabaseCleaner.clean_with(:truncation)
end
config.around(:each) do |example|
DatabaseCleaner.cleaning do
example.run
end
end
config.expect_with :rspec do |expectations|
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
end
config.mock_with :rspec do |mocks|
mocks.verify_partial_doubles = true
end
end
RSpec::Matchers.define :accept_nested_attributes_for do |association|
match do |model|
#model = model
#nested_att_present = model.respond_to?("#{association}_attributes=".to_sym)
if #nested_att_present && #reject
model.send("#{association}_attributes=".to_sym,[#reject])
#reject_success = model.send("#{association}").empty?
end
if #nested_att_present && #accept
model.send("#{association}_attributes=".to_sym,[#accept])
#accept_success = ! (model.send("#{association}").empty?)
end
#nested_att_present && ( #reject.nil? || #reject_success ) && ( #accept.nil? || #accept_success )
end
failure_message_for_should do
messages = []
messages << "expected #{#model.class} to accept nested attributes for #{association}" unless #nested_att_present
messages << "expected #{#model.class} to reject values #{#reject.inspect} for association #{association}" unless #reject_success
messages << "expected #{#model.class} to accept values #{#accept.inspect} for association #{association}" unless #accept_success
messages.join(", ")
end
description do
desc = "accept nested attributes for #{expected}"
if #reject
desc << ", but reject if attributes are #{#reject.inspect}"
end
end
chain :but_reject do |reject|
#reject = reject
end
chain :and_accept do |accept|
#accept = accept
end
end
a spec that displays the problem....
require 'rails_helper'
RSpec.describe "The homepage", :type => :feature do
it "displays articles" do
article = create(:article)
visit "/"
assert page.has_css?(".article")
end
end
the test log output when running the spec (note the transaction)...
(0.2ms) ALTER TABLE "annotations" DISABLE TRIGGER ALL;ALTER TABLE "categories" DISABLE TRIGGER ALL;ALTER TABLE "schema_migrations" DISABLE TRIGGER ALL;ALTER TABLE "articles" DISABLE TRIGGER ALL;ALTER TABLE "comments" DISABLE TRIGGER ALL;ALTER TABLE "contents" DISABLE TRIGGER ALL;ALTER TABLE "galleries" DISABLE TRIGGER ALL;ALTER TABLE "gallery_images" DISABLE TRIGGER ALL;ALTER TABLE "users" DISABLE TRIGGER ALL
(13.5ms) TRUNCATE TABLE "annotations", "categories", "articles", "comments", "contents", "galleries", "gallery_images", "users" RESTART IDENTITY CASCADE;
(0.7ms) ALTER TABLE "annotations" ENABLE TRIGGER ALL;ALTER TABLE "categories" ENABLE TRIGGER ALL;ALTER TABLE "schema_migrations" ENABLE TRIGGER ALL;ALTER TABLE "articles" ENABLE TRIGGER ALL;ALTER TABLE "comments" ENABLE TRIGGER ALL;ALTER TABLE "contents" ENABLE TRIGGER ALL;ALTER TABLE "galleries" ENABLE TRIGGER ALL;ALTER TABLE "gallery_images" ENABLE TRIGGER ALL;ALTER TABLE "users" ENABLE TRIGGER ALL
ActiveRecord::SchemaMigration Load (0.3ms) SELECT "schema_migrations".* FROM "schema_migrations"
(0.6ms) ALTER TABLE "annotations" DISABLE TRIGGER ALL;ALTER TABLE "categories" DISABLE TRIGGER ALL;ALTER TABLE "schema_migrations" DISABLE TRIGGER ALL;ALTER TABLE "articles" DISABLE TRIGGER ALL;ALTER TABLE "comments" DISABLE TRIGGER ALL;ALTER TABLE "contents" DISABLE TRIGGER ALL;ALTER TABLE "galleries" DISABLE TRIGGER ALL;ALTER TABLE "gallery_images" DISABLE TRIGGER ALL;ALTER TABLE "users" DISABLE TRIGGER ALL
(1.0ms) select table_name from information_schema.views where table_schema = 'site_test'
(23.8ms) TRUNCATE TABLE "annotations", "categories", "articles", "comments", "contents", "galleries", "gallery_images", "users" RESTART IDENTITY CASCADE;
(0.7ms) ALTER TABLE "annotations" ENABLE TRIGGER ALL;ALTER TABLE "categories" ENABLE TRIGGER ALL;ALTER TABLE "schema_migrations" ENABLE TRIGGER ALL;ALTER TABLE "articles" ENABLE TRIGGER ALL;ALTER TABLE "comments" ENABLE TRIGGER ALL;ALTER TABLE "contents" ENABLE TRIGGER ALL;ALTER TABLE "galleries" ENABLE TRIGGER ALL;ALTER TABLE "gallery_images" ENABLE TRIGGER ALL;ALTER TABLE "users" ENABLE TRIGGER ALL
(0.1ms) BEGIN
(0.1ms) COMMIT
(0.1ms) BEGIN
(0.1ms) SAVEPOINT active_record_1
User Exists (0.5ms) SELECT 1 AS one FROM "users" WHERE "users"."username" = 'testuser' LIMIT 1
User Exists (0.2ms) SELECT 1 AS one FROM "users" WHERE "users"."email" = 'testuser#miscia.net' LIMIT 1
User Exists (0.2ms) SELECT 1 AS one FROM "users" WHERE "users"."email" = 'testuser#miscia.net' LIMIT 1
SQL (0.7ms) INSERT INTO "users" ("created_at", "email", "encrypted_password", "updated_at", "username") VALUES ($1, $2, $3, $4, $5) RETURNING "id" [["created_at", "2015-03-08 20:48:58.017027"], ["email", "testuser#miscia.net"], ["encrypted_password", "$2a$04$vHQdcMEuRC2mdrNebemS5e9zMOUaloIhcIBEHlu6z7ms8Rh2NJ0F."], ["updated_at", "2015-03-08 20:48:58.017027"], ["username", "testuser"]]
(0.2ms) RELEASE SAVEPOINT active_record_1
Command :: file -b --mime '/var/folders/px/vlftvj1j43j7369cy2_sz_j40000gn/T/aacb8db341826e828273a80db5d60d5220150308-5197-hsarwm.png'
Command :: identify -format '%wx%h,%[exif:orientation]' '/var/folders/px/vlftvj1j43j7369cy2_sz_j40000gn/T/aacb8db341826e828273a80db5d60d5220150308-5197-hlhno1.png[0]' 2>/dev/null
Command :: identify -format %m '/var/folders/px/vlftvj1j43j7369cy2_sz_j40000gn/T/aacb8db341826e828273a80db5d60d5220150308-5197-hlhno1.png[0]'
Command :: convert '/var/folders/px/vlftvj1j43j7369cy2_sz_j40000gn/T/aacb8db341826e828273a80db5d60d5220150308-5197-hlhno1.png[0]' -auto-orient -resize "100%" '/var/folders/px/vlftvj1j43j7369cy2_sz_j40000gn/T/aacb8db341826e828273a80db5d60d5220150308-5197-hlhno120150308-5197-1j86yzw'
Command :: file -b --mime '/var/folders/px/vlftvj1j43j7369cy2_sz_j40000gn/T/aacb8db341826e828273a80db5d60d5220150308-5197-hlhno120150308-5197-1j86yzw'
(0.1ms) SAVEPOINT active_record_1
Command :: file -b --mime '/var/folders/px/vlftvj1j43j7369cy2_sz_j40000gn/T/aacb8db341826e828273a80db5d60d5220150308-5197-h4xfii.png'
Article Exists (0.5ms) SELECT 1 AS one FROM "articles" WHERE "articles"."title" = 'Article Title' LIMIT 1
SQL (6.4ms) INSERT INTO "articles" ("body", "created_at", "description", "header_one", "header_two", "photo_content_type", "photo_file_name", "photo_file_size", "photo_updated_at", "posted_at", "title", "updated_at", "url", "user_id") VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) RETURNING "id" [["body", "This is the article body."], ["created_at", "2015-03-08 20:48:58.218394"], ["description", "This is a test article!"], ["header_one", "Article Header One"], ["header_two", "Article Header Two"], ["photo_content_type", "image/png"], ["photo_file_name", "test_image.png"], ["photo_file_size", 96884], ["photo_updated_at", "2015-03-08 20:48:58.036313"], ["posted_at", "2015-03-08 20:48:51.398651"], ["title", "Article Title"], ["updated_at", "2015-03-08 20:48:58.218394"], ["url", "http://www.testurl.com"], ["user_id", 1]]
(0.2ms) RELEASE SAVEPOINT active_record_1
Started GET "/" for 127.0.0.1 at 2015-03-08 16:48:58 -0400
Processing by SiteController#index as HTML
Sphinx Query (9.3ms) SELECT * FROM `article_core` WHERE `sphinx_deleted` = 0 ORDER BY `created_at` DESC LIMIT 0, 5
Sphinx Found 0 results
Rendered articles/_pagination.html.erb (0.7ms)
Rendered articles/_list.html.erb (13.0ms)
Category Load (0.3ms) SELECT "categories".* FROM "categories"
Rendered site/homepage/_filters.html.erb (1.3ms)
Rendered site/homepage/_search.html.erb (57.1ms)
Rendered site/homepage/_twitter.html.erb (0.5ms)
Rendered site/homepage/_sidebar.html.erb (61.2ms)
Rendered site/index.html.erb within layouts/application (76.3ms)
Rendered site/_head.html.erb (19.8ms)
Rendered site/homepage/_social_media.html.erb (0.5ms)
Rendered site/homepage/_social_media.html.erb (0.1ms)
Rendered site/homepage/_social_media.html.erb (0.1ms)
Rendered site/homepage/_social_media.html.erb (0.1ms)
Rendered site/homepage/_menu.html.erb (8.0ms)
Content Load (0.4ms) SELECT "contents".* FROM "contents" WHERE "contents"."name_slug" = 'footer_left' ORDER BY "contents"."id" ASC LIMIT 1
Content Load (0.3ms) SELECT "contents".* FROM "contents" WHERE "contents"."name_slug" = 'footer_middle' ORDER BY "contents"."id" ASC LIMIT 1
Rendered site/_footer.html.erb (8.6ms)
Rendered site/_ga.html.erb (0.4ms)
Completed 200 OK in 128ms (Views: 116.4ms | ActiveRecord: 2.2ms)
(0.2ms) ROLLBACK
When you set config.use_transactional_fixtures = false you are telling RSpec to not bother cleaning the database each time it starts a new example. Sometimes I set this to false because I instead use database_cleaner (GitHub) to handle it.
So, based on your logs, I believe RSpec is doing exactly what it is supposed to be doing.
You can read about this setting in the RSpec manual