I use minitest on Ruby on Rails. Below is my model.
require 'mongoid'
class Person
include Mongoid::Document
index({ pin: 1 }, { unique: true, name: "pin_index" })
field :first_name
field :last_name
field :pin
validates :pin, presence: true, uniqueness: true
validates :first_name, presence: true
validates :last_name, presence: true
end
I try to write model test.I want to write a test that controls whether pin field is unique or not. How can i do this? Any idea?
I try to write a test like below:
it 'must not be valid' do
person_copy = person.dup
person.save
person_copy.save
end
You can write the test like this:
it 'must have unique pin' do
person_copy = person.dup
proc { person_copy.save! }.must_raise(Mongoid::Errors::Validations)
person_copy.errors.must_include(:pin)
end
You can use assert_includes and assert_same to test the error is the right one (about uniqueness):
it 'must not be valid' do
person_copy = person.dup
person.save
person_copy.save
assert_includes person.errors, :pin
assert_same person.errors[:pin], "pin is not unique (replace with actual error message)"
end
Considering you have a fixture already set, you can just do this:
test 'pin must be unique' do
new_person = Person.new(#person.attributes)
refute new_person.valid?
end
Related
I'm using Rails 7 and Ruby 3.1, and Shoulda Matchers for tests, but not Active Record, for I do not need a database.
I want to validate numericality. However, validations do not work. It looks like input is transformed into integer, instead of being validated. I do not understand why that happens.
My model:
# app/models/grid.rb
class Grid
include ActiveModel::Model
include ActiveModel::Attributes
attribute :rows, :integer
validates :rows, inclusion: { in: 1..50 }, numericality: { only_integer: true }
# Some other code...
end
My test:
# spec/models/grid_spec.rb
RSpec.describe Grid, type: :model do
describe 'validations' do
shared_examples 'validates' do |field, range|
it { is_expected.to validate_numericality_of(field).only_integer }
it { is_expected.to validate_inclusion_of(field).in_range(range) }
end
include_examples 'validates', 'rows', 1..50
end
# Some other tests...
end
Nonetheless, my test fails:
Grid validations is expected to validate that :rows looks like an integer
Failure/Error: it { is_expected.to validate_numericality_of(field).only_integer }
Expected Grid to validate that :rows looks like an integer, but this
could not be proved.
After setting :rows to ‹"0.1"› -- which was read back as ‹0› -- the
matcher expected the Grid to be invalid and to produce the validation
error "must be an integer" on :rows. The record was indeed invalid,
but it produced these validation errors instead:
* rows: ["is not included in the list"]
As indicated in the message above, :rows seems to be changing certain
values as they are set, and this could have something to do with why
this test is failing. If you've overridden the writer method for this
attribute, then you may need to change it to make this test pass, or
do something else entirely.
Update
Worse than before, because tests are working but code is not actually working.
# app/models/grid.rb
class Grid
include ActiveModel::Model
include ActiveModel::Attributes
attribute :rows, :integer
validates :rows, presence: true, numericality: { only_integer: true, in: 1..50 }
# Some other code...
end
# spec/models/grid_spec.rb
RSpec.describe Grid, type: :model do
describe 'validations' do
shared_examples 'validates' do |field, type, range|
it { is_expected.to validate_presence_of(field) }
it do
validate = validate_numericality_of(field)
.is_greater_than_or_equal_to(range.min)
.is_less_than_or_equal_to(range.max)
.with_message("must be in #{range}")
is_expected.to type == :integer ? validate.only_integer : validate
end
end
include_examples 'validates', 'rows', :integer, 1..50
end
# Some other tests...
end
The underlying problem (or maybe not a problem) is that you're typecasting rows attribute to integer.
>> g = Grid.new(rows: "num"); g.validate
>> g.errors.as_json
=> {:rows=>["is not included in the list"]}
# NOTE: only inclusion errors shows up, making numericality test fail.
To make it more obvious, let's remove inclusion validation:
class Grid
include ActiveModel::Model
include ActiveModel::Attributes
attribute :rows, :integer
validates :rows, numericality: { only_integer: true }
end
Still, this does not fix numericality test:
# NOTE: still valid
>> Grid.new(rows: "I'm number, trust me").valid?
=> true
# NOTE: because `rows` is typecasted to integer, it will
# return `0` which is numerical.
>> Grid.new(rows: "I'm number, trust me").rows
=> 0
>> Grid.new(rows: 0.1).rows
=> 0
# NOTE: keep in mind, this is the current behavior, which
# might be unexpected.
In the test validate_numericality_of, first of all, expects an invalid record with "0.1", but grid is still valid, which is why it fails.
Besides replacing the underlying validations, like you did, there are a few other options:
You could replace numericality test:
it { expect(Grid.new(rows: "number").valid?).to eq true }
it { expect(Grid.new(rows: "number").rows).to eq 0 }
# give it something not typecastable, like a class.
it { expect(Grid.new(rows: Grid).valid?).to eq false }
Or remove typecast:
attribute :rows
Update
Seems like you're trying to overdo it with validations and typecasting. From what I can see the only issue is just one test, everything else works fine. Anyway, I've came up with a few more workarounds:
class Grid
include ActiveModel::Model
include ActiveModel::Attributes
attribute :rows, :integer
validates :rows, inclusion: 1..50, numericality: { only_integer: true }
def rows= arg
# NOTE: you might want to raise an error instead,
# because this validation will go away if you run
# validations again.
errors.add(:rows, "invalid") if (/\d+/ !~ arg.to_s)
super
end
end
class Grid
include ActiveModel::Model
include ActiveModel::Attributes
attribute :rows, :integer
validates :rows, inclusion: 1..50, numericality: { only_integer: true }
validate :validate_rows_before_type_cast
def validate_rows_before_type_cast
rows = #attributes.values_before_type_cast["rows"]
errors.add(:rows, :not_a_number) if rows.is_a?(String) && rows !~ /^\d+$/
end
end
class Grid
include ActiveModel::Model
include ActiveModel::Attributes
include ActiveRecord::AttributeMethods::BeforeTypeCast
attribute :rows, :integer
validates :rows, inclusion: 1..50
# NOTE: this does show "Rows before type cast is not a number"
# maybe you'd want to customize the error message.
validates :rows_before_type_cast, numericality: { only_integer: true }
end
https://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods/BeforeTypeCast.html
My solution was dividing validations and typecasts into models.
# app/models/grid.rb
class Grid
include ActiveModel::Model
include ActiveModel::Attributes
attribute :rows, :integer
end
# app/models/grid_data.rb
class GridData
include ActiveModel::Model
include ActiveModel::Attributes
Grid.attribute_names.each { |name| attribute name }
validates(*attribute_names, presence: true)
validates :rows, numericality: { only_integer: true, in: 1..50 }
end
Specs
# spec/models
RSpec.describe Grid, type: :model do
let(:grid) { described_class.new(**attributes) }
describe 'type cast' do
let(:attributes) { default(rows: '2') }
it 'parses string valid arguments to integer or float' do
expect(grid.rows).to eq 2
end
end
end
RSpec.describe GridData, type: :model do
it 'has same attributes as Grid model' do
expect(described_class.attribute_names).to eq Grid.attribute_names
end
describe 'validations' do
shared_examples 'validates' do |field, type, range|
it { is_expected.to validate_presence_of(field) }
it do
validate = validate_numericality_of(field)
validate = validate.only_integer if type == :integer
expect(subject).to validate
end
it do
expect(subject).to validate_inclusion_of(field)
.in_range(range)
.with_message("must be in #{range}")
end
end
include_examples 'validates', 'rows', :integer, 1..50
end
end
Controller
# app/controller/grids_controller.rb
class GridsController < ApplicationController
def create
#grid_data = GridData.new(**grid_params)
if #grid_data.valid?
play
else
render :new, status: :unprocessable_entity
end
end
private
def grid_params
params.require(:grid_data).permit(*Grid.attribute_names)
end
def play
render :play, status: :created
Grid.new(**#grid_data.attributes).play
end
end
I'm super new to testing my app using RSpec and I'm trying to test the validation of a comment without a user and keep getting syntax errors.
Here is the comment model code.
class Comment < ApplicationRecord
belongs_to :user
belongs_to :product
scope :rating_desc, -> { order(rating: :desc) }
validates :body, presence: true
validates :user, presence: true
validates :product, presence: true
validates :rating, numericality: { only_integer: true }
after_create_commit { CommentUpdateJob.perform_later(self, user) }
end
and here is the comment spec:
require 'rails_helper'
describe Comment do
before do
#product = Product.create!(name: "race bike", description: "fast race bike")
#user = User.create!(email: "jerryhoglen#me.com", password: "Maggie1!")
#product.comments.create!(rating: 1, user: #user, body: "Awful bike!")
end
it "is invalid without a user"
expect(build(:comment, user:nil)).to_not be_valid
end
end
What you're doing here is good - building objects and using the be_valid matcher. But if you use shoulda-matchers there's a one-liner to test a model validation:
describe Comment do
it { is_expected.to validate_presence_of :user }
end
You can do this for other validations such as uniqueness, numericality, etc, though you'd have to look up the syntax.
you missed a do, do it like:
it "is invalid without a user" do
expect(build(:comment, user: nil)).to_not be_valid
end
But that's not a very clear test when it fails, I suggest you check the actual expected validation error.
That's what it may look like:
expect(ValidatingWidget.new.errors_on(:name)).to include("can't be blank")
expect(ValidatingWidget.new(:name => "liquid nitrogen")).to have(0).errors_on(:name)
See rspec-rails errors_on # relishapp
I try to add a validation from a blog category only limited at 1 word.
But I try this length: { maximum: 1 }
I doesn't work. Is there a validation to validaes only one word and not uniqueness?
Thank you for your answers
You can make a custom validation:
validates :category, uniqueness: true
validate :category_in_1_word
private
def category_in_1_word
if category.to_s.squish.split.size != 1
errors.add(:category, 'must be 1 word')
end
end
you can try:
validates :category, :format => { :with => /^[A-Za-z]+$/, :message => "Must be single word" }
No Rails doesn't have the validation you need, but you can easily create a custom one:
Try something like this:
class Post < ActiveRecord::Base
validate do
if ... # any custom logic goes here
errors.add :title, "is wrong"
end
end
end
I'm starting out with Rails 4.2, and I'm try to test uniqueness for the Item models I'm making, I ran this code:
item.rb:
class Item < ActiveRecord::Base
attr_accessor :name
validates :name, uniqueness: true #, other validations...
end
item_test.rb:
require 'test_helper'
class ItemTest < ActiveSupport::TestCase
def setup
#item = Item.new(name: "Example Item")
end
test "name should be unique" do
duplicate_item = #item.dup
#item.save
assert_not duplicate_item.valid?
end
end
but the test didn't pass, saying that the assert_not line is coming out true when it should be nil or false. I basically got this code from a tutorial but cannot figure out why it's not passing. Any help?
Edit: I found the solution, by not defining the other members (specifically :price ) of #item that I defined in the setup action, the test passed. However now I don't know how to make it pass with the the :price member. Below is the full implementation of item.rb & item_test.rb.
item.rb:
class Item < ActiveRecord::Base
attr_accessor :name, :description, :price
validates :name, presence: true, uniqueness: true, length: { maximum: 100 }
validates :description, presence: true,
length: { maximum: 1000 }
VALID_PRICE_REGEX = /\A\d+(?:\.\d{0,2})?\z/
validates :price, presence: true,
:format => { with: VALID_PRICE_REGEX },
:numericality => {:greater_than => 0}
end
item_test.rb:
require 'test_helper'
class ItemTest < ActiveSupport::TestCase
def setup
#item = Item.new(name: "Example Item", description: "Some kind of item.", price: 1.00)
end
test "name should be unique" do
duplicate_item = #item.dup
#item.save
assert_not duplicate_item.valid?
end
end
Almaron's answer above is correct and should be the accepted answer.
I am adding this answer to elaborate on it.
The test would be as follows:
require 'test_helper'
class ItemTest < ActiveSupport::TestCase
def setup
#item = Item.create(name: "Example Item")
end
test "name should be unique" do
duplicate_item = #item.dup
assert_not duplicate_item.valid?
end
end
Note: duplicate_item need not be saved before validating it.
The uniqueness validation is performed against the records already existing in the database. And your Item.new(name: "Example Item") is not in the database untill it is saved. So if you use Item.create(name: "Example Item") instead, the test should pass.
You've identified at least some of the problem in your edit.
The problem isn't that you're using Item.new instead of Item.create the problem is that when you do #item.save the #item record isn't being saved because it has other validation issues.
You could try...
#item.save(validate: false)
... which will force #item to be written to the database, but the test doesn't really determine why the duplicate_item record is invalid.
Better might be to test that you have an error relating to name...
require 'test_helper'
class ItemTest < ActiveSupport::TestCase
def setup
#item = Item.new(name: "Example Item")
end
test "name should be unique" do
duplicate_item = #item.dup
#item.save(validate: false)
duplicate_item.valid? # need this to populate errors
assert duplicate_item.errors
assert duplicate_item.errors[:name]
end
end
I fixed it, I got rid of the attr_accessor line, the test was then able to access the attributes and was able to detect the duplication.
I have this simplified model:
class Contract < ActiveRecord::Base
belongs_to :user belongs_to :plan
before_validation :set_default_is_voided
before_validation :set_default_expiration
validates :user, presence: true
validates :plan, presence: true
validates :contract_date, presence: true
validates :is_voided, presence: true
validates :expiration, presence: true
protected
def set_default_is_voided
if self.is_voided.nil?
self.is_voided = false
ap self.is_voided.present?
ap self.is_voided
end
end
def set_default_expiration
if self.contract_date.present?
self.expiration = self.contract_date+1.month
end
end
end
And this rspec simplified test:
context "Should create default values" do
it "Have to create is_voided" do
user = FactoryGirl.create(:user)
plan = FactoryGirl.create(:planContract)
ap "HERE"
contractDefault = FactoryGirl.create(:contractDefault, plan: plan, user: user)
ap contractDefault
expect(contractDefault.is_voided).to eq(false)
end
it "Have to create expiration" do
#expect(contract.expiration).should eq(Date.today()+1.month)
end
end
FactoryGirl:
FactoryGirl.define do
factory :contractVoid, class:Contract do
end
factory :contractDefault, class:Contract do
contract_date Date.today
end
end
This test fail with an 'is_voided can't be blank'.
And the question is:
Why the method "set_default_is_voided" in before_validation don't pass the presence true validation? Moreover, the self.is_voided.present? return false, why is it happing?
You answered your own question as to why set_default_is_voided doesn't pass the the presence: true validation, namely that self.is_voided.present? returns false, which is how presence: true is determined.
self.is_voided.present? returns false because false.present? == false per A concise explanation of nil v. empty v. blank in Ruby on Rails
See Rails: how do I validate that something is a boolean? for one way to validate that a boolean field is not nil.
See http://www.quora.com/Why-does-Rails-make-false-blank-true for a Q&A on the motivation behind the definition of blank?.