For some reason my Rspec Model tests are failing for one of my models due to a custom validation method that is being called, but not found within the test. At least that's what I think is happening. This validation is triggered when the form is submitted to create a new Dinosaur in the app. Can anyone tell me why this may be happening and what a potential fix may be?
This is the failure error for all 4 tests:
Failure/Error: if cage.at_capacity?
NoMethodError:
undefined method `at_capacity?' for nil:NilClass
Model/dinosaur.rb
class Dinosaur < ApplicationRecord
belongs_to :cage
validates :name, :species, :diet_type, :cage_id, presence: true
validates_uniqueness_of :name
validate :is_cage_at_capacity
validate :is_cage_powered_down
validate :cage_contains_diet_mismatch
=begin
def set_cage(c)
return false if c.at_capacity?
cage = c
end
def move_dino_to_powered_down_cage(c)
return false if c.is_powered_down?
cage = c
end
=end
def is_herbivore?
return diet_type == "Herbivore"
end
def is_carnivore?
return diet_type == "Carnivore"
end
def is_cage_powered_down
if cage.is_powered_down?
errors.add(:cage_id, "Chosen cage is powered down. Please choose another cage!")
end
end
def is_cage_at_capacity
if cage.at_capacity?
errors.add(:cage_id, "Chosen cage is full. Please choose another cage!")
end
end
def cage_contains_diet_mismatch
if cage.has_carnivore == true and is_herbivore?
errors.add(:cage_id, "Chosen cage contains carnivores! This dinosaur will be eaten!")
else
if cage.has_herbivore == true and is_carnivore?
errors.add(:cage_id, "Chosen cage contains herbivores! This dinosaur will eat the others!")
end
end
end
end
spec/models/dinosaur_spec.rb
require 'rails_helper'
describe Dinosaur, type: :model do
it "is valid with valid attributes" do
dinosaur = Dinosaur.new(name:"Yellow", species:"Tyrranosaurus", diet_type:"Carnivore", cage_id: 7)
expect(dinosaur).to be_valid
end
it "is not valid without a name" do
dinosaur = Dinosaur.new(name: nil)
expect(dinosaur).to_not be_valid
end
it "is not valid without a max capacity" do
dinosaur = Dinosaur.new(species: nil)
expect(dinosaur).to_not be_valid
end
it "is not valid without a power status" do
dinosaur = Dinosaur.new(diet_type: nil)
expect(dinosaur).to_not be_valid
end
end
Why this happening?
When you call method valid? all validations are triggered. At least, is_cage_at_capacity calls dinosaur.cage.at_capacity?. However, Dinosaur.new(diet_type: nil) does not have any cage so the exception is raised.
How can you fix it?
The simplest way would be to add a cage in your tests:
cage = Cage.new
dinosaur = cage.build_dinosaur(params)
It could be quite repetetive to build all these objects every time, so consider using FactoryBot with associations.
When testing validations it is more precise to test an expected error instead of the whole object state. Check this.
For built-in validations (e.g. presence, uniqueness) better use shoulda-matchers.
It seems that when the execution of the program reaches the is_cage_at_capacity method the cage is not set, actually the error has a clue: undefined method 'at_capacity?' for nil:NilClass this means that you are trying to send the method at_capacity? to a nil object.
So, I think some options to fix it will be to make sure a cage is associated with the Dinosaur when created or to add a guard clause to the is_cage_at_capacity method, something like this:
def is_cage_at_capacity
if cage.nil?
errors.add(:cage_id, "There is not a cage associated with the dinosaur")
end
if cage.at_capacity?
errors.add(:cage_id, "Chosen cage is full. Please choose another cage!")
end
end
Related
I've attempted to create a model, which needs to pass a series of validation tests in RSpec. However, I constantly get the error
expected #<Surveyor::Answer:0x0055db58e29260 #question=#<Double Surveyor::Question>, #value=5> to respond to `valid?`
My understanding (from here) was that 'valid?' checks that no errors were added to the model. I can't find any errors, however the message above persists.
This is my model
module Surveyor
class Answer
attr_accessor :question, :value
def initialize(params)
#question = params.fetch(:question)
#value = params.fetch(:value)
end
end
end
And the class Question
module Surveyor
class Question
attr_accessor :title, :type
def initialize(params)
#title = params.fetch(:title, nil)
#type = params.fetch(:type)
end
end
end
And this is the test I am attempting to pass
RSpec.describe Surveyor::Answer, '03: Answer validations' do
let(:question) { double(Surveyor::Question, type: 'rating') }
context "question validation" do
context "when the answer has a question" do
subject { described_class.new(question: question, value: 5) }
it { should be_valid }
end
end
Is my understanding of 'valid?' correct? Am I able to look at 'valid?' and perhaps see where I'm going wrong?
RSpec doesn't actually have a matcher called be_valid, instead it has some dynamic predicate matchers:
For any predicate method, RSpec gives you a corresponding matcher. Simply prefix the
method with be_ and remove the question mark. Examples:
expect(7).not_to be_zero # calls 7.zero?
expect([]).to be_empty # calls [].empty?
expect(x).to be_multiple_of(3) # calls x.multiple_of?(3)
so by calling it { should be_valid }, your subject has to respond to a valid? method. If you're testing an ActiveRecord model, those have a valid? method, but your model does not. So, if you want to test that your Answer is valid, you need to decide "what is a valid answer?" and write a method that checks for those conditions. If you want an API similar to Rails model, you might be interested in using ActiveModel::Validations
This simple method on a class just run the status method using the safe navigation operator.
def current_status
account&.status
end
But reek report this warning:
MyClass#current_status performs a nil-check [https://github.com/troessner/reek/blob/master/docs/Nil-Check.md]
How can I properly write methods like this to avoid Nil Check?
I've also verified this post from thoughtbot but it seem like "too much" for just a safe navigation operator.
Ruby 2.3.1
The advice from "Example 4" in the linked post is verbose but pretty good :
class MyClass
def initialize(with_account = nil)
#account = Account.new if with_account
end
def current_status
account.status
end
def account
#account || NilAccount.new
end
end
class Account
def status
"Up!"
end
end
class NilAccount
def status
"Down!"
end
end
puts MyClass.new(:with_account).current_status
#=> "Up!"
puts MyClass.new.current_status
#=> "Down!"
If it's "too much" for you, account&.status might be just fine.
Whatever you do : you'll need to test your code as much as possible!
well, tell-dont-ask looks pretty good, but Example 4 looks like an overkill to resolve this specific case.
#andredurao I think, we can use this workaround to pass checks, for some reason reek is fine with it:
def current_status
return unless account
account.status
end
I have the following code with two classes
class DeliveryService
attr_reader :cities
def initialize *cities
#cities = cities
end
end
class Product
attr_accessor :name
def initialize name
#name = name
end
def deliver_to delivery_service, city
if delivery_service.cities.include?(city)
puts "Product has been delivered"
else
puts "Сontact another delivery service"
end
end
end
I want to deliver_to method throw something like "Choose valid delivery service" when invalid delivery_service provided (which doesn't exist), but instead I get NameError
I tried to put this in different places in my code
rescue NameError
puts "Choose valid delivery service"
but it doesn't work
irb(main):001:0> require './DeliveryService.rb'
=> true
irb(main):002:0> fedex = DeliveryService.new "Moscow", "Saint-Petersburg"
=> #<DeliveryService:0x007f66ded31520 #cities=["Moscow", "Saint-Petersburg"]>
irb(main):003:0> product = Product.new "mug"
=> #<Product:0x007f66ded1e498 #name="mug">
irb(main):004:0> product.deliver_to fedex, "Moscow"
Product has been delivered
=> nil
irb(main):005:0> product.deliver_to dhl, "Moscow"
NameError: undefined local variable or method `dhl' for main:Object
from (irb):6
from /home/budkin/.rbenv/versions/2.1.2/bin/irb:11:in `<main>'
irb(main):006:0>
Use Dependency Injection on the Correct Object
This is a design problem, in that you aren't getting NameError from within your class; it's being raised when you call product.deliver_to dhl, "Moscow" from outside the class. Even if you have a rescue clause as part of your class definition, the NameError is going to be raised by the caller.
The right way to fix this is to pass a valid Product object to a valid DeliveryService object instead. A product shouldn't know anything about deliveries anyway; it's a violation of the single responsibility principle. So:
class DeliveryService
def deliver product
end
end
This will give you an instance method that takes a Product, and delivers it through the current delivery service. For example:
product = Product.new :widget
dhl = DeliveryService.new 'Moscow'
dhl.deliver product
Even if you perform this inversion, you will have problems if you pass invalid objects around, so don't do that. :)
dh1 does not exist, so you need to wrap your part of the code that attempts to dereference dh1.
begin
product.deliver_to dhl, "Moscow"
rescue NameError => e
puts e.message
end
Although this is quite a messy approach. A better way would be to create an array or set containing all your companies, and then check if the company is defined in that set/array.
Hi I made it to the lase exercise os Learn Ruby The Hard Way, and I come at the wall...
Here is the test code:
def test_gothon_map()
assert_equal(START.go('shoot!'), generic_death)
assert_equal(START.go('dodge!'), generic_death)
room = START.go("tell a joke")
assert_equal(room, laser_weapon_armory)
end
And here is the code of the file it should test:
class Room
attr_accessor :name, :description, :paths
def initialize(name, description)
#name = name
#description = description
#paths = {}
end
def ==(other)
self.name==other.name&&self.description==other.description&&self.paths==other.paths
end
def go(direction)
#paths[direction]
end
def add_paths(paths)
#paths.update(paths)
end
end
generic_death = Room.new("death", "You died.")
And when I try to launch the test file I get an error:
generic_death = Room.new("death", "You died.")
I tried to set the "generic_death = Room.new("death", "You died.")" in test_gothon_map method and it worked but the problem is that description of the next object is extremely long, so my questions are:
why assertion doesn't not respond to defined object?
can it be done different way then by putting whole object to testing method, since description of the next object is extremely long...
The nature of local variable is that they are, well, local. This means that they are not available outside the scope they were defined.
That's why ruby does not know what generic_death means in your test.
You can solve this in a couple of ways:
define rooms as constants in the Room class:
class Room
# ...
GENERIC_DEATH = Room.new("death", "You died.")
LASER_WEAPON_ARMORY = Room.new(...)
end
def test_gothon_map()
assert_equal(Room::START.go('shoot!'), Room::GENERIC_DEATH)
assert_equal(Room::START.go('dodge!'), Room::GENERIC_DEATH)
room = Room::START.go("tell a joke")
assert_equal(room, Room::LASER_WEAPON_ARMORY)
end
assert the room by its name, or some other identifier:
def test_gothon_map()
assert_equal(START.go('shoot!').name, "death")
assert_equal(START.go('dodge!').name, "death")
room = START.go("tell a joke")
assert_equal(room.name, "laser weapon armory")
end
My question is, why do I receive the following rspec error message? (code below) I've stubbed the :update_payment method on the StripeSubscription model. I've been at this for a couple of hours and am perplexed.
Failure/Error: #stripe_msub.should_receive(:update_payment).and_return(#stripe_msub)
(#<StripeSubscription:0xb879154>).update_payment(any args)
expected: 1 time
received: 0 times
###Rspec test###
describe "PUT 'update'" do
context "signed-in teacher" do
before(:each) do
#teacher = Factory(:teacher)
#teacher_upload = Factory(:teacher_upload,:teacher_id=>#teacher.id)
#stripe_mplan = Factory(:stripe_plan)
#new_stripe_card_token = 528
#stripe_msub = Factory(:stripe_subscription,:teacher_id=>#teacher.id,:stripe_plan_id=>#stripe_mplan.id, :email=>#teacher.email,:account_status=>Acemt::Application::STRIPE_SUBSCRIPTION_ACCOUNT_STATUS[:active])
#stripe_msub.stub!(:update_payment).and_return(#stripe_msub)
StripeSubscription.stub!(:update_payment).and_return(#stripe_msub)
StripeSubscription.stub!(:update_attributes).and_return(true)
#stripe_customer = mock('Stripe::Customer')
Stripe::Customer.stub!(:retrieve).with(#stripe_msub.stripe_customer_token).and_return(#stripe_customer)
#stripe_customer.stub(:card=).and_return(true)
#stripe_customer.stub(:save).and_return(true)
test_sign_in(#teacher)
end
it "should update credit card information" do
#stripe_msub.should_receive(:update_payment)
Stripe::Customer.should_receive(:retrieve).with(#stripe_msub.stripe_customer_token).and_return(#stripe_customer)
#stripe_customer.should_receive(:card=)
#stripe_customer.should_receive(:save).and_return(#stripe_customer)
put :update, :teacher_id=>#teacher.id, :stripe_subscription=>{:stripe_plan_id=>#stripe_msub.stripe_plan_id, :teacher_id=>#stripe_msub.teacher_id, :email=>#stripe_msub.email, :stripe_customer_token=>#stripe_msub.stripe_customer_token,:stripe_card_token=>#new_stripe_card_token,:account_status=>#stripe_msub.account_status}
#teacher.stripe_subscription.should == #stripe_msub
response.should redirect_to teacher_path(#teacher)
end
end #signed in teacher
end #PUT update
###controller###
class StripeSubscriptionsController < ApplicationController
before_filter :signed_in_teacher
before_filter :correct_teacher
:
def update
##stripe_subscription = StripeSubscription.find_by_id(params[:id])
#stripe_subscription = #teacher.stripe_subscription
if #stripe_subscription.update_payment(params[:stripe_subscription])
#handle successful update
flash[:success] = "Credit card updated"
sign_in #teacher
redirect_to #teacher
else
render 'edit'
end
end
:
end
###model###
class StripeSubscription < ActiveRecord::Base
#attr_accessible :email, :plan_id, :stripe_customer_token, :teacher_id, :account_status
validates_presence_of :stripe_plan_id
validates_presence_of :email
validates_presence_of :teacher_id
belongs_to :stripe_plan, :class_name=>"StripePlan"
belongs_to :teacher
attr_accessor :stripe_card_token
def save_with_payment
if valid?
customer = Stripe::Customer.create(description: email, plan: stripe_plan_id, card: stripe_card_token)
self.stripe_customer_token = customer.id
save!
end
rescue Stripe::InvalidRequestError => e
logger.error "Stripe error while creating customer: #{e.message}"
errors.add :base, "There was a problem with your credit card."
end
def update_payment(stripe_params)
if valid?
customer = Stripe::Customer.retrieve(self.stripe_customer_token)
customer.card = stripe_params[:stripe_card_token]
status = customer.save #update card info on Stripe
update_attributes(stripe_params) #save StripeSubscription object
end
rescue Stripe::InvalidRequestError => e
logger.error "Stripe error while updating your credit card: #{e.message}"
errors.add :base, "There was a problem with your credit card."
end
end
You are setting the expectation on an object in your spec (#stripe_msub). In the controller however, you probably load a teacher from the database and get its subscription (#stripe_subscription).
Due to the roundtrip through the database, this object is not the same one as the one you set your expectation on, therefore the one you set your expection on (#stripe_msub in the spec) never receives the method call and therefore rspec complains.
To fix this, you would have to stub away all database calls and make sure, that your object from the spec appears in the controller. I don't know where exactly #teacher is set in the controller (I guess in one of the filters) so I can't give you the exact solution, but it will be something like this:
# I assume this is the implentation of your filter in the controller
def signed_in_teacher
#teacher = Teacher.find_by_id(params[:teacher_id])
end
# Then you would have to add the following mocks/stubs to your spec
Teacher.should_receive(:find_by_id).once.with(#teacher.id).and_return(#teacher)