I am trying to test #initialize of class Author
:
# lib/author.rb
class Author
attr_reader :name, :filename, :website
def initialize(name, filename, website)
@name = name
@filename = filename
set_filename_from_name if filename == :auto
@website = website
end
end
In reality there are 8-9 properties of Author
, not just 3 and I've decided to require them all in #initialize.
I am trying to dry up my specs, but I have lots of repetitions.
I have created a helper module:
#spec/helpers/create_author.rb
module CreateAuthor
# Creates author with default values except for those listed
# under params.
def create_author(params = {})
defaults = {
name: "Ford",
filename: "Ford",
website: 'www.ford.com'
}
opt = defaults.merge(params)
Author.new(opt[:name],
opt[:filename],
opt[:website])
end
end
But now I have untested code which I need to test. This is spec for that helper method:
# spec/authors_helper_spec.rb
require_relative 'helpers/create_author'
describe 'Create Author Helper' do
# subject do
# Class.new { include CreateAuthor }
# end
include CreateAuthor
let(:defaults) do
{
name: "Ford",
filename: "Ford",
website: 'www.ford.com'
}
end
describe '#create_author' do
it 'returns author with default values when called with no parameters' do
author = create_author()
expect(author.class).to eq(Author)
expect(author.name).to eq(defaults[:name])
expect(author.filename).to eq(defaults[:filename])
expect(author.website).to eq(defaults[:website])
end
it 'returns author with custom name when specified' do
author = create_author(name: 'New Name')
expect(author.class).to eq(Author)
expect(author.name).to eq('New Name')
expect(author.filename).to eq(defaults[:filename])
expect(author.website).to eq(defaults[:website])
end
end
end
I will need to do similar thing for other class, so I will just change create_author(options={})
to create_class(class, options={})
and test that.
How can I DRY this up and make it less verbose?
-
\$\begingroup\$ Is it possible to use keyword arguments for your contructor? \$\endgroup\$slowjack2k– slowjack2k2016年10月11日 11:45:47 +00:00Commented Oct 11, 2016 at 11:45
-
\$\begingroup\$ That would make testing easier, but calling it more difficult. There are so many params I can't even remember what they are. So I just copy paste parameter list of initialize method and then handle the code before it to initialize all params. I have purposely put it in constructor so that I do not forget to initalize some of them, as there are so many. \$\endgroup\$Marko Avlijaš– Marko Avlijaš2016年10月11日 12:20:03 +00:00Commented Oct 11, 2016 at 12:20
-
\$\begingroup\$ do you really need to test your test helpers? \$\endgroup\$max pleaner– max pleaner2016年10月23日 20:03:17 +00:00Commented Oct 23, 2016 at 20:03
3 Answers 3
You could use the following solution unless it's important to call initialize in "create_author".
# spec/helpers/create_author.rb
module CreateEntity
def create_entity(klass, params = {}, defaults = {})
opt = defaults.merge(params)
entity = klass.allocate
opt.each do |key, value|
entity.instance_variable_set("@#{key}", value)
end
entity
end
def create_author(params = {})
create_entity(Author, params, {
name: "Ford",
filename: "Ford",
website: 'www.ford.com'
})
end
end
create_author(name: "Ford 1")
-
\$\begingroup\$ Thank you. Calling initialize is important because validation logic is in there. \$\endgroup\$Marko Avlijaš– Marko Avlijaš2016年10月12日 14:36:41 +00:00Commented Oct 12, 2016 at 14:36
-
\$\begingroup\$ You have presented an alternative solution, but haven't reviewed the code. Please explain your reasoning (how your solution works and why it is better than the original) so that the author and other readers can learn from your thought process. \$\endgroup\$Ethan Bierlein– Ethan Bierlein2016年10月12日 14:40:44 +00:00Commented Oct 12, 2016 at 14:40
I would give shared examples a try. It's not tested, but it should work like this:
#spec/helpers/shares_examples/check_default_behaviour.rb
RSpec.shared_examples 'check default behaviour' do |class_to_instantiate|
def create_instance(params = {})
opt = defaults.merge(params)
class_to_instantiate.new(opt[:name],
opt[:filename],
opt[:website])
end
# let(:defaults) has to be set within your spec
describe "#create_#{class_to_instantiate}" do
it "returns #{class_to_instantiate} with default values when called with no parameters" do
instance = create_instance()
expect(instance.class).to eq(class_to_instantiate)
expect(instance.name).to eq(defaults[:name])
expect(instance.filename).to eq(defaults[:filename])
expect(instance.website).to eq(defaults[:website])
end
it "returns #{class_to_instantiate} with custom name when specified" do
instance = create_instance(name: 'New Name')
expect(instance.class).to eq(class_to_instantiate)
expect(instance.name).to eq('New Name')
expect(instance.filename).to eq(defaults[:filename])
expect(instance.website).to eq(defaults[:website])
end
end
end
# spec/...
describe 'Create Author Helper' do
let(:defaults) do
{
name: "Ford",
filename: "Ford",
website: 'www.ford.com'
}
end
include_examples 'check default behaviour', Author
end
You should work on making your test cases rely on shared code as much as possible.
It's part of a larger strategy to make more 'testable' code. In your main application, if you have three similar methods and want 100% unit test coverage, you'd have to test all three. But if you refactor the app code to use one method instead, then both the app code and the tests will be more terse.
To reiterate - just for the sake of sanity, if you're setting out to write 100% unit test coverage it's worth trying to write 'testable' code - there is no magic bullet to make a concise test suite if your code is sprawling.
I may be misunderstanding, but are you trying to test your helper in addition to your application code? To do so might be a little much - are people really testing their test suites?