How do I remove duplication in shoulda tests? - ruby-on-rails

Here is what I have:
context "Create ingredient from string" do
context "1 cups butter" do
setup do
#ingredient = Ingredient.create(:ingredient_string => "1 cups butter")
end
should "return unit" do
assert_equal #ingredient.unit, 'cups'
end
should "return amount" do
assert_equal #ingredient.amount, 1.0
end
should "return name" do
assert_equal #ingredient.name, 'butter'
end
end
context "1 (18.25 ounce) package devil's food cake mix with pudding" do
setup do
#ingredient = Ingredient.create(:ingredient_string => "1 (18.25 ounce) package devil's food cake mix with pudding")
end
should "return unit" do
assert_equal #ingredient.unit, '(18.25 ounce) package'
end
should "return amount" do
assert_equal #ingredient.amount, 1.0
end
should "return name" do
assert_equal #ingredient.name, 'devil\'s food cake mix with pudding'
end
end
end
Clearly there is a lot of duplication there. Any thoughts on how to remove it, if only at the very least the context and the string?

Here's a solution to your specific problem. The idea is to create a class method (like Shoulda's context, setup and should).
Encapsulate the repetition in a class method accepting all varying parts as arguments like this:
def self.should_get_unit_amount_and_name_from_string(unit, amount, name, string_to_analyze)
context string_to_analyze do
setup do
#ingredient = Ingredient.create(:ingredient_string => string_to_analyze)
end
should "return unit" do
assert_equal #ingredient.unit, unit
end
should "return amount" do
assert_equal #ingredient.amount, amount
end
should "return name" do
assert_equal #ingredient.name, name
end
end
end
Now you can call all these encapsulated tests with one liners (5-liners here for readability ;-)
context "Create ingredient from string" do
should_get_unit_amount_and_name_from_string(
'cups',
1.0,
'butter',
"1 cups butter")
should_get_unit_amount_and_name_from_string(
'(18.25 ounce) package',
1.0,
'devil\'s food cake mix with pudding',
"1 (18.25 ounce) package devil's food cake mix with pudding")
end
In some cases, you may want to accept a block which could serve as your Shoulda setup.

Duplication in tests is not necessarily a Bad Thing(tm)
I suggest you read the following articles from Jay Field
http://blog.jayfields.com/2007/06/testing-one-assertion-per-test.html
http://blog.jayfields.com/2008/05/testing-duplicate-code-in-your-tests.html
They make a convinving case for code duplication in the tests and keeping one assertion per test.

Tests/specs are not production code and so being dry is not a priority.
The principle is that the specs should be clear to read, even if it means there is duplication of text across tests.
Don't be too concerned about specs being dry. Overemphasis of dry tests tends to make things more difficult as you have to jump around to the definitions of things to understand what is happening.

Personally for this test, I wouldn't use Shoulda.
You can easily remove duplication by using dynamic method creation as follows:
class DefineMethodTest < Test::Unit::TestCase
[{:string => '1 cups butter', :unit => 'cups', :amount => 1.0, :name => 'butter'},{:string => '1 (18.25 ounce) package devil's food cake mix with pudding', :unit => '(18.25 ounce) package', :unit => 1.0, :name => "devil's food cake mix with pudding"}].each do |t|
define_method "test_create_ingredient_from_string_#{t[:string].downcase.gsub(/[^a-z0-9]+/, '_')}" do
#ingredient = Ingredient.create(:ingredient_string => t[:string])
assert_equal #ingredient.unit, t[:unit], "Should return unit #{t[:unit]}"
assert_equal #ingredient.amount, t[:amount], "Should return amount #{t[:amount]}"
assert_equal #ingredient.name, t[:name], "Should return name #{t[:name]}"
end
end
end

Related

Writing test for Spree, cant create variants for products

I'm trying to write rspec tests for my spree customizations and i need to create products with variants. i cant seem to do this even though i appear to be doing the exact same thing as the rspec tests that are part of spree core.
def build_option_type_with_values(name, values)
ot = create(:option_type, :name => name)
values.each do |val|
ot.option_values.create(:name => val.downcase, :presentation => val)
end
ot
end
let(:number_size_option_type) do
size = build_option_type_with_values("number sizes", %w(1 2 3 4))
end
let(:product1) { create(:product, name: 'product1') }
it "should have variants" do
hash = {number_size_option_type.id.to_s => number_size_option_type.option_value_ids}
product1.option_values_hash = hash
product1.save
product1.reload
expect(product1.variants.length).to eq(4)
end
no matter what i do, the number of variants for my product is always zero.
Turns out the product.option_values_hash needs to be added during product creation in order to invoke the variant creation code. here is the changed line and then i removed the hash from the test "should have variant"
let(:product1) { create(:product, name: 'product1', option_values_hash: {number_size_option_type.id.to_s => number_size_option_type.option_value_ids}) }
it "should have variants" do
product1.save
expect(product1.option_type_ids.length).to eq(1)
expect(product1.variants.length).to eq(4)
end

How to pass an instance variable to an RSpec shared example

I'm using RSpec (2.10.1) to test validations on a model and have extracted some code to share with other model validations. The validations were first written on the Companies table, so the code looks like this:
# support/shared_examples.rb
shared_examples "a text field" do |field, fill, length|
it "it should be long enough" do
#company.send("#{field}=", fill * length)
#company.should be_valid
end
etc...
end
and the usage is:
# company_spec.rb
describe Company do
before { #company = Company.new( init stuff here ) }
describe "when address2" do
it_behaves_like "a text field", "address2", "a", Company.address2.limit
end
etc...
end
I'd like to pass the #company as a parameter to the shared example so I can reuse the code for different models, something like this:
# support/shared_examples.rb
shared_examples "a text field" do |model, field, fill, length|
it "it should be long enough" do
model.send("#{field}=", fill * length)
model.should be_valid
end
etc...
end
and the usage is:
# company_spec.rb
describe Company do
before { #company = Company.new( init stuff here ) }
describe "when address2" do
it_behaves_like "a text field", #company, "address2", "a", Company.address2.limit
end
etc...
end
However, when I do this I get undefined method 'address2' for nil:NilClass. It appears #company is not being passed (not in scope?) How do I get something like this to work?
The problem is that self within the example group is different from self within a before hook, so it's not the same instance variable even though it has the same name.
I recommend you use let for cases like these:
# support/shared_examples.rb
shared_examples "a text field" do |field, fill, length|
it "it should be long enough" do
model.send("#{field}=", fill * length)
model.should be_valid
end
end
# company_spec.rb
describe Company do
describe "when address2" do
it_behaves_like "a text field", "address2", "a", Company.address2.limit do
let(:model) { Company.new( init stuff here ) }
end
end
end

Matching a complex data structure (an array of structs) with Rspec

How can I match the following array with Rspec ?
[#<struct Competitor html_url="https://github.com/assaf/vanity", description="Experiment Driven Development for Ruby", watchers=845, forks=146>,
#<struct Competitor html_url="https://github.com/andrew/split", description="Rack Based AB testing framework", watchers=359, forks=43>]
I need to check if a class method return an array of struct like the previous or a more extensive one which include the previous.
UPDATE:
I currently have this test which go green,
require 'spec_helper'
describe "Category" do
before :each do
#category = Category.find_by(name: "A/B Testing")
end
describe ".find_competitors_by_tags" do
it "returns a list of competitors for category" do
competitors = Category.find_competitors_by_tags(#category.tags_array).to_s
competitors.should match /"Experiment Driven Development for Ruby"/
end
end
end
end
but I'd like to know if it is the correct way to test the following method or you think it could be better :
class Category
...
Object.const_set :Competitor, Struct.new(:html_url, :description, :watchers, :forks)
def self.find_competitors_by_tags(tags_array)
competitors = []
User.all_in('watchlists.tags_array' => tags_array.map{|tag|/^#{tag}/i}).only(:watchlists).each do |u|
u.watchlists.all_in(:tags_array => tags_array.map{|tag|/^#{tag}/i}).desc(:watchers).each do |wl|
competitors << Competitor.new(wl.html_url, wl.description, wl.watchers, wl.forks)
end
end
return competitors
end
end
I would test the minimum needed to make sure that your find function works correctly. You probably don't need to check every field for the returned records for that. What you have does that. I'd modify it a bit, to just look at the description (or whatever other field is appropriate):
it "returns a list of competitors for category" do
competitors = Category.find_competitors_by_tags(#category.tags_array)
descriptions = competitors.map(&:description).sort
descriptions.should == [
"Experiment Driven Development for Ruby",
"Rack Based AB testing framework",
]
end

Unit Testing validates presence of odd behaviour

I'm trying out the whole TDD and I'm running into a problems with validate presence. I have a model called Event and I want to ensure that when an Event is created that a title a price and a summary exists.
Unit Test Code
class EventTest < ActiveSupport::TestCase
test "should not save without a Title" do
event = Event.new
event.title = nil
assert !event.save, "Save the Event without title"
end
test "should not save without a Price" do
event = Event.new
event.price = nil
assert !event.save, "Saved the Event without a Price"
end
test "should not save without a Summary" do
event = Event.new
event.summary = nil
assert !event.save, "Saved the Event without a Summary"
end
end
I run the test I get 3 FAILS. Which is Good.
Now I want to to just get the title test to pass first with the following code in the Event model.
class Event < ActiveRecord::Base
validates :title, :presence => true
end
When I re-run the test I get 3 PASSES where I would think I should have gotten 1 PASS and 2 FAILS. Why am I getting 3 PASSES?
I have two test helper methods that can make this sort of thing easier to diagnose:
def assert_created(model)
assert model, "Model was not defined"
assert_equal [ ], model.errors.full_messages
assert model.valid?, "Model failed to validate"
assert !model.new_record?, "Model is still a new record"
end
def assert_errors_on(model, *attrs)
found_attrs = [ ]
model.errors.each do |attr, error|
found_attrs << attr
end
assert_equal attrs.flatten.collect(&:to_s).sort, found_attrs.uniq.collect(&:to_s).sort
end
You'd use them in cases like this:
test "should save with a Title, Price or Summary" do
event = Event.create(
:title => 'Sample Title',
:price => 100,
:summary => 'Sample summary...'
)
assert_created event
end
test "should not save without a Title, Price or Summary" do
event = Event.create
assert_errors_on event, :title, :price, :summary
end
This should show if you're missing a validation that you expected and will also give you feedback on specific validations that have failed when not expected.
When you created the model with Event.new, all attributes initially have a value of nil. This means that all 3 attributes you are checking are already nil (so event.title = nil and event.price = nil don't actually do anything). Since title has been marked for validation to ensure its presence, unless you set title to something other than nil, you will not be able to save the model.
Perhaps try adding this to your test class:
setup do
#event_attributes = {:title => "A title", :price => 3.99, :summary => "A summary"}
end
Then instead of:
event = Event.new
event.title = nil
Use:
event = Event.new(#event_attributes.merge(:title => nil))
Do the same for all your tests (substituting :title with whatever attribute you are validating presence for)
Also, there's no reason to call save to test for a valid state. You can just call event.valid? to avoid trips to the database where not needed.

Simple syntax for testing Validation errors

I'm looking for clean and short code to test validations in Rails Unittests.
Currently I do something like this
test "create thing without name" do
assert_raise ActiveRecord::RecordInvalid do
Thing.create! :param1 => "Something", :param2 => 123
end
end
I guess there is a better way that also shows the validation message?
Solution:
My current solution without an additional frameworks is:
test "create thing without name" do
thing = Thing.new :param1 => "Something", :param2 => 123
assert thing.invalid?
assert thing.errors.on(:name).any?
end
You don't mention what testing framework that you're using. Many have macros that make testing activerecord a snap.
Here's the "long way" to do it without using any test helpers:
thing = Thing.new :param1 => "Something", :param2 => 123
assert !thing.valid?
assert_match /blank/, thing.errors.on(:name)
In newer versions of Rails (v5) with MiniTest
test "create thing without name" do
thing = Thing.new :param1 => "Something", :param2 => 123
assert thing.invalid?
assert thing.errors.added? :name, :blank
end
https://devdocs.io/rails~5.2/activemodel/errors
I'm using Rails 2.0.5, and when I want to assert that a model will fail validation, I check the errors.full_messages method, and compare it to an array of expected messages.
created = MyModel.new
created.field1 = "Some value"
created.field2 = 123.45
created.save
assert_equal(["Name can't be blank"], created.errors.full_messages)
To assert that validation succeeds, I just compare to an empty array. You can do something very similar to check that a Rails controller has no error messages after a create or update request.
assert_difference('MyModel.count') do
post :create, :my_model => {
:name => 'Some name'
}
end
assert_equal([], assigns(:my_model).errors.full_messages)
assert_redirected_to my_model_path(assigns(:my_model))
For those using Rails 3.2.1 and up, I prefer using the added? method:
assert record.errors.added? :name, :blank
I use a test helper that looks like this:
def assert_invalid(record, options)
assert_predicate record, :invalid?
options.each do |attribute, message|
assert record.errors.added?(attribute, message), "Expected #{attribute} to have the following error: #{message}"
end
end
Which allows me to write tests like this:
test "should be invalid without a name" do
user = User.new(name: '')
assert_invalid user, name: :blank
end
Try also accept_values_for gem.
It allows to do something like this:
describe User do
subject { User.new(#valid_attributes)}
it { should accept_values_for(:email, "john#example.com", "lambda#gusiev.com") }
it { should_not accept_values_for(:email, "invalid", nil, "a#b", "john#.com") }
end
In this way you can test really complicated validations easily
You could give the rspec-on-rails-matchers a try. Provides you with syntax like:
#thing.should validates_presence_of(:name)
Not sure when it was added but the #where method makes it easy to target a specific error without having to rely on the text of the message.
refute #thing.valid?
assert #thing.errors.where(:name, :invalid).present?

Resources