I have a controller that calls the CSVCreator class when a get request is made. People is just a list of Persons that have some basic values like name and title.
def index
respond_to do |format|
format.html
format.csv { send_data CSVCreator.new(people).create }
end
My CSVCreator looks like this
class CSVCreator
HEADERS = ["Name", "Date", "Title"].freeze
def initialize(people)
@people = people
end
def generate
CSV.generate do |csv|
csv << HEADERS
@people.each do |person|
row = [person.name, person.date, person.title]
csv << row
end
end
end
end
I'm trying to figure out how I would go about testing this in Rspec? What I've tried is
it "calls CSVCreator when get request is made" do
csv_creator = double("CSVCreator")
people = double("People")
allow(csv_creator).to receive(:new).with(people)
allow(csv_creator).to receive(:create)
expect(csv_creator).to receive(:new).with(people)
expect(csv_creator).to receive(:create)
get :index, format: :csv
end
My thought process was to decouple the controller and the CSVCreator and People classes. So to test the controller, I wanted to see if it correctly calls the methods it needs to, so I created test doubles for those objects. I'm new to RSpec and testing in general, so please let me know if my approach was incorrect.
My issue is, I get a failure saying
Failure/Error: expect(csv_creator).to receive(:new).with(people)
(Double "CSVCreator").new(#<Double "People">)
expected: 1 time with arguments: (#<Double "People">)
received: 0 times
I know my class works and it creates the CSV, and that CSVCreator.new(people).create
being called in the controller. So I'm curious as to why I'm receiving this failure.
CodePudding user response:
Create a class method on your CSVCreator
that creates an instance and calls the generate method:
class CSVCreator
# ...
def self.perform(people)
new(people).generate
end
end
def index
respond_to do |format|
format.html
format.csv { send_data CSVCreator.perform(people) }
end
end
This creates a better API so that consumers don't have to have as much knowledge about the class and it makes it way easier to set expectations:
it "calls CSVCreator when get request is made" do
expect(CSVCreator).to receive(:perform)
get :index, format: :csv
end
However stubbing in people
is a bit more problematic and depends on how its actually implemented. If its a method on the controller it would be hard to stub without using any_instance
which is a code smell.
CodePudding user response:
i guess that the input param people
is created somewhere in your controller, but i'm sure it's not your double "People", you have to stub the method :new
of the People class to return an instance_double
of "People", Or in case you want to make sure that CSVCreator is created with a special "People", for example in your controller you call this to get people: PeopleService.some_logic(..) -> a_special_people
, then stub that method allow(PeopleService).to receive(:some_logic).and_return(instance_double_people)
beside that, you could also create an instance_double
of CSVCreator instead, then stub method :new
of CSVCreator to return that instance_double, now you only need to verify that instance_double has called :create
method since it makes sure that CSVCreator has called :new
and the new instance of CSVCreator has called :create
.
it "calls CSVCreator when get request is made" do
csv_creator = instance_double(CSVCreator)
people = instance_double(People)
allow(People).to receive(:new).and_return(people)
# allow(PeopleService).to receive(:some_logic).and_return(people)
allow(CSVCreator).to receive(:new).with(people).and_return(csv_creator)
allow(csv_creator).to receive(:create)
get :index, format: :csv
expect(csv_creator).to have_received(:create)
end