Half of the time I'll be able to make my tests run. The other half they fail because of a uniqueness violation, the source of which I am unable to locate. Right now I am in the latter half. My error is this:
ItemTest#test_valid_setup:
ActiveRecord::RecordNotUnique: PG::UniqueViolation: ERROR: duplicate key value violates unique constraint "index_blobs_on_user_id_and_item_id"
DETAIL: Key (user_id, item_id)=(1, 1) already exists.
: INSERT INTO "blobs" ("user_id", "item_id", "amount", "active", "created_at", "updated_at") VALUES ($1, $2, $3, $4, $5, $6) RETURNING "id"
test/models/item_test.rb:15:in `setup'
I have a factories.rb:
FactoryBot.define do
factory :user, aliases: [:owner] do
email "test#test.com"
username "test"
end
factory :item do
owner
image { loads file etc... }
price 100
...
end
factory :blob, aliases: [:wanted, :collateral] do
user
item
amount 0
active true
end
end
My item_test.rb
require 'test_helper'
require 'support/database_cleaner'
class ItemTest < Minitest::Test
def setup
DatabaseCleaner.start
#create users
#user1 = FactoryBot.create(:user, email: "a#pear.com", username: "barnyard")
#user2 = FactoryBot.create(:user, email: "bo#ichi.com", username: "ponygrl")
#user3 = FactoryBot.create(:user, email: "ho#ho.com", username: "hon")
#create items
#item1 = FactoryBot.create(:item, owner: #user1)
#item2 = FactoryBot.create(:item, owner: #user2, price: 101)
#item3 = FactoryBot.create(:item, owner: #user3, price: 102)
#create blobs
#blob1 = FactoryBot.create(:blob, user: #user1, item: #item1, amount: #item1.price, active: false)
#blob2 = FactoryBot.create(:blob, user: #user2, item: #item2, amount: #item2.price, active: false)
#blob3 = FactoryBot.create(:blob, user: #user3, item: #item3, amount: #item3.price, active: false)
end
def teardown
DatabaseCleaner.clean
end
end
And then an item.rb
class Item < ApplicationRecord
after_create :create_blobs
private
def create_blobs
blob = Blob.new(user_id: self.owner.id, item_id: self.id, amount: self.price)
blob.save
end
end
A little background: A User creates an Item which in turn creates a Blob in an after_create with an amount parameter set to the value of Item's price. I cannot find out how to run an after_create in minitest, so I mocked up the Blob data in setup to inherit from an attribute of Item.
I can see that the error comes from line 15 of item_test.rb, but I'm not understanding why. I'm creating the Users, then the Items, and then ERROR the Blobs. I understand the why (I have a db level uniqueness constraint on a combination of user and item) but not the how (because from what I see, I haven't created those Blobs - there's no after_create called on Item when they're created in test), and I suspect that it has to do with the way I'm writing this.
It seems natural to me to conclude that DatabaseCleaner.start and DatabaseCleaner.clean both start and clean up old test data when the test is run and concluded, but this is obviously not the case. I started using it specifically to avoid this problem, which I was having previously. So I db:drop db:create, and db:schema:load, but once again, I have the same issue. And if it's not that, it's a uniqueness violation on a username, an email, etc......long story short, what is going on with that error?
Sorry if this is so confusing.
Edit: If I uncomment the after_create and replace all method references to the blob object created through that callback with blobs created in my test setup, the tests pass. But I really don't like doing that.
Either uncomment the after_create and reference the test objects, or remove the test objects and reference each blob by writing a method that returns the blob owned_by user and item, so that you can write #item.blob, and have it return the relevant blob.
Related
I am using an enum in Rails and PostgreSQL. In my model tests I usually verify that my Rails validations are backed by database constraints where appropriate (e.g. presence: true in Model and null: false in DB). I do this by deliberately making the model invalid, attempting to save it without validations and making sure it raises a ActiveRecord::StatementInvalid error.
How do I test a PostgreSQL enum from MiniTest? My usual approach isn't working as everything I try to do to set my ActiveRecord model to an invalid enum value raises an ArgumentError, even using write_attribute() directly.
Is there a way to deliberately bypass the enum restrictions in Rails? Do I need to drop down out of ActiveRecord and send an AREL or SQL query direct to the database? Is there some other approach?
# Model
class SisRecord < ApplicationRecord
enum record_type: {
student: "student",
staff: "staff",
contact: "contact"
}
validates :record_type, presence: true
end
# Migration
class CreateSisRecords < ActiveRecord::Migration[7.0]
def change
create_enum :sis_record_type, %w(student staff contact)
create_table :sis_records do |t|
t.enum :record_type, enum_type: :sis_record_type, null: false
t.timestamps
end
end
end
# Test
require "test_helper"
class SisRecordTest < ActiveSupport::TestCase
test "a record_type is required" do
record = sis_records(:valid_sis_record)
record.record_type = nil
assert_not record.save, "Saved the SIS Record without a record type"
end
test "a record_type is required by the database too" do
record = sis_records(:valid_sis_record)
record.record_type = nil
assert_raises(ActiveRecord::StatementInvalid) {
record.save(validate: false)
}
end
test "record_type is restricted to accepted values" do
accepted_values = %w(student staff contact)
record = sis_records(:valid_sis_record)
assert_nothing_raised {
record.record_type = accepted_values.sample
}
assert_raises(ArgumentError) {
record.record_type = "something else"
}
end
test "record_type is restricted to accepted values by the database too" do
accepted_values = %w(student staff contact)
record = sis_records(:valid_sis_record)
record.record_type = accepted_values.sample
assert record.save, "Record didn't save despite accepted type value '#{record.record_type}'"
record.write_attribute(:record_type, "nonsense") ### <-- ArgumentError
assert_raises(ActiveRecord::StatementInvalid) {
record.save(validate: false)
}
end
end
I have an answer to my own question, but I'm still open to better answers.
I found a comment on a gist that showed how to fairly simply insert a record with Arel so for now I am using this approach:
# Just the test in question
test "record_type is restricted to accepted values by the database too" do
accepted_values = %w(student staff contact)
table = Arel::Table.new(:sis_records)
manager = Arel::InsertManager.new
manager.insert [
[table[:record_type], accepted_values.sample],
[table[:created_at], Time.now],
[table[:updated_at], Time.now],
]
assert_nothing_raised {
SisRecord.connection.insert(manager.to_sql)
}
manager.insert [
[table[:record_type], "other type"],
[table[:created_at], Time.now],
[table[:updated_at], Time.now],
]
assert_raises(ActiveRecord::StatementInvalid) {
SisRecord.connection.insert(manager.to_sql)
}
end
created_at and updated_at are required fields so we have to add a value for those.
In my real case (not the simplified version I posted above), SisRecord belongs to Person so I had to provide a valid person ID (UUID) too. I did this by grabbing an ID from my people fixtures:
manager.insert [
[table[:record_type], "other type"],
[table[:person_id], people(:valid_person).id], # <--------
[table[:created_at], Time.now],
[table[:updated_at], Time.now],
]
On my ruby on Rails app, when creating a model Deal, I use an after_create to create prizes on the DealPrize table.
Deal and DealPrize have a belong to/has_many relations: a Deal has many Deal prizes and a Dealprize belongs to a Deal.
It works like this: on my admin panel (using activeadmin), inside a Deal, I have a column 'prize-number' and I use an after_create so that every time the admin creates a new deal, the app takes this prize_number column, and create this volume of prizes (inserting as many rows as necessary) inside the DealPrize table.
My tests using rspec and FactoryGirl are failing. It might be due to the fact rspec/factory girl do not play well with prepared_statements. I am not sure.
Here is my code
models/deal.rb
has_many :deal_prizes, dependent: :delete_all
after_create :create_dealprizes
# Constants
TIME_SET = Time.zone.now
CONNECTION = ActiveRecord::Base.connection.raw_connection
def create_dealprizes
begin
CONNECTION.describe_prepared('create_deal_prizes')
rescue PG::InvalidSqlStatementName
CONNECTION.prepare('create_deal_prizes', 'INSERT INTO deal_prizes (deal_id,created_at,updated_at,admin_user_id,prize_id) values ($1, $2, $3, $4, $5)')
end
Deal.transaction do
self.prizes_number.times do |i|
CONNECTION.exec_prepared('create_deal_prizes', [
{ value: self.id},
{ value: TIME_SET },
{ value: TIME_SET },
{ value: self.admin_user_id },
{ value: 5 }
])
end
end
end
Here is my test: I would like to be sure when a deal is created that if the deal's prizes_number is 340 then 340 rows are added on the table Dealprizes as it should be.
require 'spec_helper'
describe DealPrize do
describe "the right number of rows are created inside DealPrize table when a Deal is created" do
before do
#initial_prize_count = DealPrize.count
#deal_prize = FactoryGirl.create(:deal_prize)
#deal = FactoryGirl.create(:deal_prizes => [#deal_prize], :prizes_number => 277)
end
it "does create right nb of rows" do
expect(DealPrize.count).to eq( #initial_prize_count + 277 )
end
end
end
I use a factory for Deals:
FactoryGirl.define do
factory :deal do
country "France"
title "Neque porro quisquam est qui dolorem"
description "lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum"
factory :deal_skips_validate do
to_create {|instance| instance.save(validate: false) }
end
end
end
and here is the Factory for DealPrizes:
FactoryGirl.define do
factory :deal_prize do
end
end
Here is the error I am getting:
PG::UnableToSend:
server closed the connection unexpectedly
This probably means the server terminated abnormally
before or while processing the request.
If needed, here is how I deal with transaction in my spec_helper.rb
config.use_transactional_fixtures = false
config.before(:suite) do
DatabaseCleaner.clean_with(:truncation, :except => %w(roles))
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
You'll need to grab a new connection from the pool each time your method runs (and make sure you return it to the pool when you're done). Take a look at with_connection.
Something like this should work.
class Deal < ActiveRecord::Base
has_many :deal_prizes, dependent: :delete_all
after_create :create_dealprizes
# Constants
TIME_SET = Time.zone.now
def create_dealprizes
self.class.connection_pool.with_connection do |connection|
begin
connection.describe_prepared('create_deal_prizes')
rescue PG::InvalidSqlStatementName
connection.prepare('create_deal_prizes', 'INSERT INTO deal_prizes (deal_id,created_at,updated_at,admin_user_id,prize_id) values ($1, $2, $3, $4, $5)')
end
connection.execute("START TRANSACTION")
prizes_number.times do |i|
connection.exec_prepared('create_deal_prizes', [
{ value: self.id},
{ value: TIME_SET },
{ value: TIME_SET },
{ value: self.admin_user_id },
{ value: 5 }
])
end
connection.execute("COMMIT")
end
end
end
This should work, but in my opinion (for what it's worth) it's not very Ruby-ish. It sounds like you're concerned with performance, so I would add the data with a single INSERT statement. You could build that manually, but the activerecord-import gem makes it very easy.
Using that gem might look something like:
class Deal < ActiveRecord::Base
has_many :deal_prizes, dependent: :delete_all
after_create :create_dealprizes
def create_dealprizes
prizes = prizes_number.times.map do
deal_prizes.build(created_by: admin_user, prize_id: 5)
end
DealPrize.import(prizes)
end
end
Here's a sample app that uses ActiveRecord import.
You only retrieve CONNECTION from ActiveRecord::Base.connection.raw_connection once, as the class is being loaded. So the framework is free to close that connection and create a new one. You should retrieve it at the time you actually need it, then I imagine it might work.
Update: afaik the PostgreSQL adapter will create prepared statements, too and the database should be able to re-use them, so the performance gain from doing these things manually shouldn't be that big (especially since you're "only" talking about a few hundred objects). You lose compatibility to other adapters as well as maintainability due to non-idiomatic Rails code when you could just create DealPrize objects instead.
Can I suggest the following?
def create_deal_prizes
batch_size = 1000
deal_prizes = []
time = Time.zone.now
prize_id = 5 # wasn't sure where you got this id from
Deal.transaction do
deal_prizes = []
prizes_number.times do
deal_prizes << "(#{self.id}, #{self.admin_user_id}, #{prize_id}, '#{time}', '#{time}')"
if deal_prizes.size >= batch_size
sql = "INSERT INTO deal_prizes (deal_id, admin_user_id, prize_id, created_at, updated_at) VALUES #{deal_prizes.join(", ")}"
ActiveRecord::Base.connection.execute sql
deal_prizes = []
end
end
if deal_prizes.any?
sql = "INSERT INTO deal_prizes (deal_id, admin_user_id, prize_id, created_at, updated_at) VALUES #{deal_prizes.join(", ")}"
ActiveRecord::Base.connection.execute sql
end
end
end
This is more rails-esque, efficient, and won't have to be re-written when you switch to [INSERT DB HERE]. Feel free to play with the batch_size as well.
Another note, almost positive if you set the time to a constant it will not change, was that your intention? Instead I moved it to the method, so it will lock to the time it was invoked and not the time the class was initialized (boot).
I am using the railstutorial.org book.
I tried updating the user attributes as written in Chapter 7 of the book, but the email became nil. I have tried updating to no avail. This produces a NoMethodError in UsersController#show: undefined method `downcase' for nil:NilClass.
Here is the show.html.erb
<% provide(:title, #user.name) %>
<h1>
<%= gravatar_for #user %>
<%= #user.name %>
</h1>
Users helper
module UsersHelper
# Returns the Gravatar of the given user.
def gravatar_for(user)
gravatar_id = Digest::MD5::hexdigest(user.email.downcase)
gravatar_url = "https://secure.gravatar.com/avatar/#{gravatar_id}"
image_tag(gravatar_url, alt: user.name, class: "gravatar")
end
end
User Controller
class UsersController < ApplicationController
def new
end
def show
#user = User.find(params[:id])
end
end
User model
class User < ActiveRecord::Base
before_save { email.downcase! }
validates :name, presence: true, length: { maximum: 50 }
VALID_EMAIL_REGEX = /\A[\w+\-.]+#[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
validates :email, presence: true,
format: { with: VALID_EMAIL_REGEX },
uniqueness: { case_sensitive: false }
has_secure_password
validates :password, presence: true, length: { minimum: 6 }
end
Please I need help to fix this. Thanks.
I'm also following this tutorial, so I don't pretend to be a Rails expert.
That said, I just went back and reviewed the tutorial material surrounding the first introduction of the
before_save { email.downcase! }
syntax, and I see this at the end of Chapter 6 (listing 6.42).
I'm pretty sure this isn't working for you because your UsersController is missing a definition of the "New" method:
def new
#user = User.new
end
I'm wiling to bet that your #user object is Nil because you haven't create an instance of it yet. BTW, at this point in the tutorial, you should have also defined a Create method in UsersController.
EDIT: If your problems are limited to what is happening in the Rails console, I agree with the comments that you need to provide a complete transcript of your console session in order for folks to provide a complete answer.
Here's an example Rails console session from within my Rails Tutorial project:
Invoke the console and make it aware of my User model:
$rails console
Loading development environment (Rails 4.2.2)
require './app/models/user'
=> true
Create a User instance named "spong"
**spong = User.new**
=> <User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil, password_digest: nil, remember_digest: nil, admin: nil, activation_digest: nil, activated: false, activated_at: nil>
(Note: My User model has more attributes because I am toward the end of the Tutorial.)
Populate values for name and e-mail:
spong.name = "Yo Dawg!"
=> "Yo Dawg!"
spong.email = "YoDaWG#dawg.COM"
=> "YoDaWG#dawg.COM"
Note that my initial e-mail address is mixed case.
Invoke the downcase method:
spong.email.downcase
=> "yodawg#dawg.com"
This is working for me in the console. Now let's try the update_attributes method:
spong.update_attributes(name: "The Dude", email: "dude#AbideS.org")
This is straight out of the tutorial, but it doesn't work for me, because at this point in my journey, I have implemented features that prevent this kind of update:
(6.5ms) begin transaction
User Exists (0.5ms) SELECT 1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER('dude#AbideS.org') LIMIT 1
User Exists (0.2ms) SELECT 1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER('dude#AbideS.org') LIMIT 1
(0.1ms) rollback transaction
=> false
As Hartl says:
Note that if any of the validations fail, such as when a password is required to save a record (as implemented in Section 6.3), the call to update_attributes will fail.
So let me try the singular version of this command:
spong.update_attribute( :email, "dude#AbideS.org")
(3.7ms) begin transaction
SQL (4.0ms) INSERT INTO "users" ("name", "email", "created_at", "updated_at", "activation_digest") VALUES (?, ?, ?, ?, ?) [["name", "The Dude"], ["email", "dude#abides.org"], ... ]
(1.2ms) commit transaction
==> true
spong.email
=> "dude#abides.org"
Not that the e-mail address is already converted to lower case in the INSERT command--exactly as expected, thanks to that
before_save { email.downcase! }
we have in our User model.
But what's with all the DB activity? This is because update_attributes updates a single attribute and saves the record without going through the normal validation procedure (which is why I am able to do this). While research this, I found this excellent discussion about update_attribute and update_attributes. Great stuff!
OK, so what happens if we try to call update_attribute when the (existing) e-mail address is blank? Let's see:
newUser = User.new
=> #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil, password_digest: nil, remember_digest: nil, admin: nil, activation_digest: nil, activated: false, activated_at: nil>
Everything in newUser is nil. Let's try to update the e-mail address:
newUser.update_attribute(:email, "cOnFuSed#MixecCase.com")**
(1.2ms) begin transaction
SQL (3.9ms) INSERT INTO "users" ("email", "created_at", "updated_at", "activation_digest") VALUES (?, ?, ?, ?) [["email", "confused#mixeccase.com"], ...]
(0.9ms) commit transaction
=> true
Again, because of the behavior of update_attribute/update_attributes, my database is updated; somewhat counterintuitively, a record is inserted during this "update" process, but this is because I had not yet saved this (or the first) record to the DB.
I hope all this helps. At a minimum, I have demonstrated that this DOES work via the console--even with previously 'nil' values (and I learned a ton while doing the research to attempt an answer).
I have abstracted out my models to be able to test multiple models at the same time. The issue is that some of the models have different parameters. Take this sample schema below.
Schemas (simplified)
# Table name: cars
#
# id :integer not null, primary key
# hp :integer
# wheels :integer
# Table name: trucks
#
# id :integer not null, primary key
# hp :integer
# wheels :integer
# Table name: boats
#
# id :integer not null, primary key
# motors :integer
# hp :integer
Test
setup do
#models = ['cars', 'trucks', 'boats']
end
test 'something awesome' do
#models.each do |model|
# This works for cars and trucks, not for boats
exemplar = FactoryGirl.create(model, id: 1, hp: 600, wheels: 4)
# A bunch of assertions
end
end
I can assign id and hp to all models but while cars and trucks have wheels, boats have motors. Is there a way to in the create call essentially say "If this method is defined, then use it, if not then ignore it"
What I would like to be able to do is call exemplar = FactoryGirl.create(model, id: 1, hp: 600, wheels: 4, motors: 2) and have it work across the board creating 3 objects:
car: id = 1, hp = 600, wheels = 4
truck: id = 1, hp = 600, wheels = 4
boat: id = 1, hp = 600, motors = 2
If you use rspec as a test framework,use shared examples in the current context.
This will allow you to build each object as you want, and have them all go through the same tests. Example:
groupe_example 'object' do
it 'has a valid factory' do
expect(object).to be_valid
end
end
describe Car do
let(:object){ create(:car_with_some_options) }
include_examples 'object'
end
describe Truck do
let(:object){ create(:truck_with_other_options) }
include_examples 'object'
end
Otherwise, you should go for a solution like:
setup do
#models = {:car => {hp: 600}, :truck => { wheels: 8, hp: 1000} }
end
test 'something awesome' do
#models.each do |model, params|
# This works for cars and trucks, not for boats
exemplar = FactoryGirl.create(model, params)
# A bunch of assertions
end
end
which can be reformated better with different factories. For instance, if you create :default_car, :default_truck, etc factories for each of your model, you can then set whatever parameters you want there and then plain call them through FactoryGirl.create without having to worry about the parameters in your test.
================== EDIT ========================
If you really want to test whether the parameter is defined, you can either use attributes. A more comprehensive answer is here
Or, more simple, you can check whether there is a writer operator:
model.public_send(:wheels=, 4) if model.respond_to? :wheels=
I need to create an enumeration that I will need to initialize from a value from the querystring.
Example of what I have and what I need to do:
class UserType
NONE = 0
MEMBER = 1
ADMIN = 2
SUPER = 3
end
Now in my querystring I will have:
/users/load_by_type?type=2
Now in my controller I will get the value 2 from the querystring, I then need to have a UserType object which has the value 'MEMBER'.
How can I do this?
If my class isn't really a good enumeration hack, please advise.
How about something like this.
require 'active_record'
# set up db
ActiveRecord::Base.establish_connection adapter: 'sqlite3', database: ':memory:'
# define schema
ActiveRecord::Schema.define do
suppress_messages do
create_table :users do |t|
t.string :name
t.string :role
end
end
end
# define class
class User < ActiveRecord::Base
Roles = %w[none member admin super].map(&:freeze)
validates_inclusion_of :role, in: Roles
end
# specification
describe User do
before { User.delete_all }
let(:valid_role) { User::Roles.first }
let(:invalid_role) { valid_role.reverse }
it 'is valid if its role is in the Roles list' do
User.new.should_not be_valid
User.new(role: valid_role).should be_valid
User.new(role: invalid_role).should_not be_valid
end
let(:role) { User::Roles.first }
let(:other_role) { User::Roles.last }
it 'can find users by role' do
user_with_role = User.create! role: role
user_with_other_role = User.create! role: other_role
User.find_all_by_role(role).should == [user_with_role]
end
end
It does have the disadvantage of using an entire string (255 chars) for the enumeration method, but it also has the advantage of readability and ease of use (it would probably come in as "/users/load_by_role?role=admin"). Besides, if at some point it winds up costing too much, it should be easy to update to use a small integer.
I think I'd rather use hashes for this kind of thing, but just for fun:
class Foo
BAR = 1
STAN = 2
class << self
def [](digit)
constants.find { |const| const_get(const) == digit }
end
end
end
puts Foo[1] # BAR
puts Foo[2] # STAN