Hashes and expectations with RSpec (RED)
Today I wrote a pair of tests for my controller and found a couple of things I would like to understand in deep and begin to practice them more. While testing the #delete from my controller in my controller tests, I needed to test if at the end the count was changing or not. In order to achieve this I found that the expectation change was the one to go with.
I wrote my test like this:
describe '#delete' do
context 'authenticated admin' do
before do
@user = FactoryBot.create(:user, :admin)
@item = FactoryBot.create(:meetings_item)
@list = FactoryBot.create(:meetings_list)
end
it 'deletes an item' do
sign_in(@user)
expect do
delete(:destroy, params: { id: @item.id, meetings_list_id: @item.id })
end.to change(MeetingsItem, :count).by(-1)
end
end
What I tried to achieve is to proof that the MeetingsItem
, :count, was changing by -1, which in this example passed on green. First of all I would like to understand or how can I precisely tell which are the params I need to introduce and use. I got confused first by this line: delete(:destroy, params: { id: @item.id, meetings_list_id: @item.id })
it makes sense to me that the meetings_list_id: is the ID of a LIST not from an ITEM. So then I took a look back at my model, controller and my factories:
My model:
class MeetingsItem < ApplicationRecord
validates :date, :reason, presence: true
belongs_to :meetings_list
monetize :amount_cents, allow_nil: true, default: nil
def completed?
!completed_at.blank?
end
end
here I noticed the relationship of meetings_list
. Next I check the controller:
class MeetingsItemsController < ApplicationController
before_action :set_meeting_list
before_action :set_meetings_item, except: %i[create]
def create
@meetings_item = @meetings_list.meetings_items.create(meeting_item_params)
if @meetings_item.valid?
@meetings_item.save
redirect_to @meetings_list, notice: 'Neuer Eintrag hinzugefügt'
else
render :new
end
end
def new
@meetings_item = MeetingsItem.new
end
def destroy
flash[:notice] = if @meetings_item.destroy
'Listenelement gelöscht.'
else
'Listenelement kann nicht gelöscht werden.'
end
redirect_to @meetings_list
end
def complete
@meetings_item.update_attribute(:completed_at, Time.now)
redirect_to @meetings_list, notice: 'Aufgabe erledigt'
end
private
def set_meeting_list
@meetings_list = MeetingsList.find(params[:meetings_list_id])
end
def set_meetings_item
@meetings_item = @meetings_list.meetings_items.find_by(id: params[:id])
end
def meeting_item_params
params[:meetings_item].permit(:date, :reason, :amount)
end
end
My factory:
FactoryBot.define do
factory :meetings_list do
title { Faker::Team.name }
responsible { Faker::TvShows::SouthPark.character }
end
factory :invalid_meeting, parent: :meetings_list do
title { nil }
end
factory :meetings_item do
association :meetings_list
date { Date.today }
reason { Faker::Dessert.variety }
amount { Faker::Number.number(digits: 2) }
trait :invalid_item do
reason { nil }
end
end
end
I am assuming both: delete(:destroy, params: { id: *@item.id*, meetings_list_id: *@item.id* })
are using the item id from the factory id and for the meetings_list_id: through the association :meetings_list if taking the id from the list. Am I'm missing something? Because it is not clear to me.
Next I tried to test with the same hash an unauthenticated user with this test:
context 'unauthenticated user' do
before do
@user = FactoryBot.create(:user)
@item = FactoryBot.create(:meetings_item)
end
it 'fails to delete' do
expect do
delete(:destroy, params: { id: @item.id,
meetings_list_id: @item.id })
end.to_not change(MeetingsItem, :count)
end
end
and it fails with:
Failures:
1) MeetingsItemsController#delete unauthenticated user fails to delete
Failure/Error:
expect do
delete(:destroy, params: { id: @item.id,
meetings_list_id: @item })
end.to_not change(MeetingsItem, :count)
expected `MeetingsItem.count` not to have changed, but did change from 1 to 0
This is not what I expected since, this method first of all shouldn't be available without sign_in and should only be available to administrators. In the test none are present, and it simply deletes an item. Why?
What I'm trying to achieve is hit all the lines from my controller:
def destroy
flash[:notice] = if @meetings_item.destroy
'Listenelement gelöscht.'
else
'Listenelement kann nicht gelöscht werden.'
end
redirect_to @meetings_list
end
I do not get to hit the else statement. Maybe I'm testing in the wrong direction but I would like to know what I'm doing wrong.