Search code examples
ruby-on-railsrspeccapybara

RSpec system test can't visit newly created record from a factory


I'm trying to login a user and visit a page for a record that this user owns.

RSpec.describe 'User matters', type: :system do
  context 'urgent matter flow' do
    let!(:matter) { create(:matter, :with_order) }

    before :each do
      login_as(matter.patient.user) # warden helper
    end

    it 'can submit matter info' do
      visit users_matter_path(matter)
    end
  end
end

The controller is a simple find

def show
  @matter = current_user.patient.matters.find(params[:id])
  @order = @matter.orders.joins(:order_items).first
end

Error in the template:

<%= @matter.id # works %> 
<%= @order.id # undefined method `id' for nil:NilClass %>

Factories:

FactoryBot.define do
  factory :matter do
    overview_image { Rack::Test::UploadedFile.new('spec/fixtures/images/bandit.jpg', 'image/jpeg') }
    overview2_image { Rack::Test::UploadedFile.new('spec/fixtures/images/bandit.jpg', 'image/jpeg') }
    patient
    doctor

    trait :with_order do
      after(:create) do |matter|
        create_list(:order, 1, matter: matter)
      end
    end
  end

  # -- Order ------------------
  factory :order do
    matter

    status { :placed }
    currency_code { 'SEK' }
  end

  # -- User -------------------
  sequence :user_email do |index|
    "patient-#{Random.hex(4)}#{index}@gmail.com"
  end

  factory :user do
    email { generate(:user_email) }
    locale { 'en' }
    country_alpha2_code { 'SE' }
    mobile_number { '+46701234567' }
    password { Faker::Internet.password(min_length: 8, max_length: 20) }
    password_confirmation { "#{password}" }
    after(:create) do |user|
      user.patient ||= create(:patient, user: user)
    end
  end

  # -- Patient -----------------
  sequence :patient_id_number do |index|
    year = index.to_s.rjust(2, '0')
    "19#{year}01019876"
  end

  factory :patient do
    id_number { generate(:patient_id_number) }
    date_of_birth { '1990-01-01' }
    first_name { 'John' }
    last_name { 'Doe' }

    user
  end
end

rails_helper.rb (omitted some unnecessary stuff)

require 'spec_helper'
ENV['RAILS_ENV'] ||= 'test'
require_relative '../config/environment'

require 'rspec/rails'

Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f }

RSpec.configure do |config|
  config.include Warden::Test::Helpers

  config.use_transactional_fixtures = false

  config.before(:suite) do
    DatabaseCleaner.clean_with(:truncation)
  end

  config.before(:each) do
    DatabaseCleaner.strategy = :transaction
  end

  config.before(:each, type: :system) do
    DatabaseCleaner.strategy = :truncation
  end

  config.before(:each) do
    DatabaseCleaner.start
  end

  config.append_after(:each) do
    DatabaseCleaner.clean
  end

  config.infer_spec_type_from_file_location!
  config.filter_rails_from_backtrace!
end

and the spec_helper.rb (ommited some unnecessary stuff)

RSpec.configure do |config|
  config.expect_with :rspec do |expectations|
    expectations.include_chain_clauses_in_custom_matcher_descriptions = true
    expectations.syntax = :expect
  end

  config.before(:each, type: :system) do
    driven_by :selenium
  end

  config.mock_with :rspec do |mocks|
    mocks.verify_partial_doubles = true
  end
  config.shared_context_metadata_behavior = :apply_to_host_groups
  config.filter_run_when_matching :focus
  config.profile_examples = 10
  config.order = :random
end

Why does this happen? Isn't setting use_transactional_fixtures to false exactly what this is for?

PS: I'm want to test a whole user flow, with multiple steps. Being able to create records with certain states and their relationships up front, would be quite much faster.

----------- Update:

When running rspec, i tail the test.log and see this output:

Matter Create (0.3ms)  INSERT INTO "matters" ("patient_id", "doctor_id", "reference", "symptoms", "tags", "status", "diagnosis", "planned_action", "orders_count", "matter_comments_count", "matter_responses_count", "created_at", "updated_at") VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) RETURNING "id"  [["patient_id", "ed1c0576-78a4-4383-ac33-291ca44083ca"], ["doctor_id", "cdab5e14-3641-4a78-8de6-1a0056635e73"], ["reference", "pohmb"], ["symptoms", nil], ["tags", nil], ["status", "created"], ["diagnosis", nil], ["planned_action", nil], ["orders_count", 0], ["matter_comments_count", 0], ["matter_responses_count", 0], ["created_at", "2023-11-18 09:08:57.796995"], ["updated_at", "2023-11-18 09:08:57.796995"]]


  Order Create (0.3ms)  INSERT INTO "orders" ("matter_id", "reference", "currency_code", "status", "order_items_count", "created_at", "updated_at") VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING "id"  [["matter_id", "8533289a-4087-4664-9fe3-df4bc7a9e241"], ["reference", "evt0o"], ["currency_code", "SEK"], ["status", 0], ["order_items_count", 0], ["created_at", "2023-11-18 09:08:57.841842"], ["updated_at", "2023-11-18 09:08:57.841842"]]

So the records are getting created correctly. I also verified that the matter_id (8533289a-4087-4664-9fe3-df4bc7a9e241) is the same.

So I think this has to do with rspec/system tests and transactions not being committed maybe?


Solution

  • A look at your factories and logs shows you're not creating any OrderItems, which means when you try and join it with orders, nothing is returned:

    @matter.orders.joins(:order_items).first
    

    Also, I think you're confusing joins with includes, as I don't see why you would need to join instead of just:

    @matter.orders.first
    
    # which should give the same result as
    @matter.orders.left_joins(:order_items).first
    

    Examples to illustrate:

    >> Order.all
    => [#<Order:0x00007f6795badfc0 id: 1>, #<Order:0x00007f6795bade80 id: 2>]
    
    >> Order.joins(:order_items)
    => [#<Order:0x00007f67985cb690 id: 1>, #<Order:0x00007f67985cb410 id: 1>]
    # NOTE: the dup orders             ^                                  ^
    #       first Order has two OrderItems
    #       second Order has no OrderItems so it is not in the result
    
    >> Order.left_joins(:order_items)
    => [#<Order:0x00007f6795af6a50 id: 1>, #<Order:0x00007f6795af6910 id: 1>, #<Order:0x00007f6795af67d0 id: 2>]
    

    and first is the same:

    >> Order.first
    => #<Order:0x00007f6798891410 id: 1>
    
    >> Order.joins(:order_items).first
    => #<Order:0x00007f6798891410 id: 1>
    
    >> Order.left_joins(:order_items).first
    => #<Order:0x00007f6798891410 id: 1>
    

    If you want to preload order_items use includes:

    >> Order.includes(:order_items)
    => [#<Order:0x00007f6795d9c598 id: 1>, #<Order:0x00007f6795d9c458 id: 2>]
    # NOTE: no duplicates, all orders are loaded
    
    >> Order.includes(:order_items).to_a.first.instance_variable_get("@association_cache")
    => 
    {:order_items=>
      #<ActiveRecord::Associations::HasManyAssociation:0x00007f679799d710
    ...
       @stale_state=nil,
       @target=[#<OrderItem:0x00007f6795acd5d8 id: 1, order_id: 1>, #<OrderItem:0x00007f6795acd498 id: 2, order_id: 1>]>}
    # NOTE: ^ order_items are preloaded into each Order object to avoid n+1 issues