Passing a simple test - ruby-on-rails

I'm using Rails 3.2's rake tests function. I'm trying to pass a test but it's giving me errors. Btw, when see you how I write, I'm a noob. It's a hacked way of testing, but at least I want to try passing it first.
test "product title must have at least 10 characters" do
ok = %w{ aaaaaaaaaa aaaaaaaaaaa }
bad = %w{ a aa aaa aaaa aaaaa aaaaaa aaaaaaa aaaaaaaa aaaaaaaaa}
ok.each do |name|
assert new_product_title(name).valid?, "#{name} shouldn't be invalid"
end
bad.each do |name|
assert new_product_title(name).invalid?, "#{name} shouldn't be valid"
end
end
with the function
def new_product_title(title)
Product.new(title: title,
description: "yyy",
price: 1,
image_url: "fred.gif")
end
somehow it's not passing.
What's the reason here? And is there a better way to write it?

I'm more concerned about the method. I'm assuming this method is in a product model? It seems what you are trying to do should definitely be controlled by the model, but I don't think you can call a class's method inside the class's definition. I also don't see much utility in a method that creates a new product with specified title, but static description, price, and image_url. If you need default values for specific attributes, you can set those in an initialize method and overwrite them later if needed. Some people frown on setting defaults in initialize so instead you can set them in an after_initialize callback like this:
class Product < ActiveRecord::Base
after_initialize :init
def init
self.description ||= 'yyy'
self.price ||= 1
self.image_url ||= "fred.gif"
end
end
Then whenever you needed to create a new product with a title and the default attributes you can just use
Product.new(:title => "some title")
And if you don't want all the defaults you can just pass the values into new like usual
Product.new(:title => "some other title", :price => 400) # desc & url are still default
About your tests. I always test in RSpec. Since you are using Test Unit (or Mini Test or whatever it is now), my advice my not be correct. But first I would make the variable names more descriptive. Secondly, there are some commas at the end of your assertions that shouldn't be there.
test "product title must have at least 10 characters" do
valid_name = "a" * 10
short_name = "a" * 9
valid_product = Product.new(:name => valid_name)
assert valid_product.valid?
invalid_product = Product.new(:name => short_name)
assert invalid_product.invalid?
end
If you get that working you may want to verify that the product is invalid for the correct reason using an assert equals method on invalid_product.errors.full_messages and the expected string from the error.

Related

Ensure Capitalization for Double-Barrel Names

In a Rails model, I am trying to override the setter to ensure that the first_name and last_name fields are always properly capitalized -- the use-case is for a CRM, so you'd expect to have humanized names.
Is this the best practise to ensure capitalization on the first and last names? Currently, it is failing the tests.
# app/models/concerns/personable.rb
def first_name=(value)
write_attribute(:first_name, value.humanize.gsub(/\b('?[a-z])/) { $1.capitalize })
end
def last_name=(value)
write_attribute(:last_name, value.humanize.gsub(/\b('?[a-z])/) { $1.capitalize })
end
# test/models/customer_test.rb
test "should capitalize first and last names" do
customer = create_customer(first_name: "MARRIE-DAY", last_name: "kennedy")
assert_equal customer.name, "Marrie-Day Kennedy"
end
test "#name returns a affixed surname" do
customer = create_customer(first_name: "Test", last_name: "McKay")
assert_equal "Test McKay", customer.name
end
Failure:
CustomerTest#test_#name_returns_a_affixed_surname:
Expected: "Test McKay"
Actual: "Test Mckay"
humanize will turn McKay into Mckay so the last replacing logic will not applied in this case.
you could try add a mark for those capital characters before humanize, such as McKay -> Mc|K|ay, so that we still know where need to turn capital, as below code:
class Helper
def self.format_name(name)
name.gsub(/([^A-Z])\B([A-Z])/) {"#{$1}{#{$2&.capitalize}}"}
.humanize
.gsub(/\{([a-z])\}?/){$1.capitalize}
.gsub(/\b([a-z])/) {$1.capitalize}
end
end
Helper.format_name("Kennedy") # Kennedy
Helper.format_name("McKay") # McKay
Helper.format_name("MARRIE-DAY") # Marrie-Day

the right way to change the associated object in rspec

I recently started to test with rspec, so I can strongly be mistaken, correct me if there is a better way
I create two related models
let(:user) {FactoryGirl.create :user}
let!(:participation) {FactoryGirl.create :participation, user: user}
and before one of the tests change one of the related objects
context "when" do
before {participation.prize = 100}
it "" do
binding.pry
end
end
But inside it
participation.prize => 100
user.participatons.select(:prize) => nil
what am I doing wrong ? and how to fix it?
When you say user.participations.select(:prize), you're making a query to the db to get values in the user's participations' prize columns. But when you say before {participation.prize = 100} you're only setting the prize attribute on the participation object. Try saving the participation before the select line:
participation.prize # => 100
participation.save
user.participatons.select(:prize) # => nil
Another possible issue is that user.participations has been memoized by a previous call. Ensure that user.participations.first == participation. If it doesn't, check
1) puts participation.user_id and
2) puts user.participations, user.reload.participations
Lastly, a better way of setting up the test so that you run into this issue less often is something along the lines of:
# let(:price) { 0 } # default price. Optional so that tests won't throw errors if you forget to set it in a context/describe block.
let(:user) {FactoryGirl.create :user}
let!(:participation) {FactoryGirl.create :participation, user: user, price: price}
# ...
context "when ..." do
let(:price) { 100 }
it "" do
binding.pry
end
end
This way, the price is set when you create the model. Following this pattern generally means running into this problem less.

How to delete an entire array in Ruby and test with RSpec

I'm fairly new to Ruby and am currently taking a full stack course. For one of my projects we are building an addressbook. I have set up how to add an entry to the addressbook, however, I can't seem to figure out how to delete an entry (I make an attempt with the remove_entry method in the AddressBook class below but am not having any luck). We are also supposed to test first with RSpec, have the test fail and then write some code to get it to pass. If I didn't include all the info needed for this question let me know (rookie here). Anyway, here is what I have so far:
RSpec
context ".remove_entry" do
it "removes only one entry from the address book" do
book = AddressBook.new
entry = book.add_entry('Ada Lovelace', '010.012.1815', 'augusta.king#lovelace.com')
book.remove_entry(entry)
expect(entry).to eq nil
end
end
AddressBook class
require_relative "entry.rb"
class AddressBook
attr_accessor :entries
def initialize
#entries = []
end
def add_entry(name, phone, email)
index = 0
#entries.each do |entry|
if name < entry.name
break
end
index += 1
end
#entries.insert(index, Entry.new(name, phone, email))
end
def remove_entry(entry)
#entries.delete(entry)
end
end
Entry class
class Entry
attr_accessor :name, :phone_number, :email
def initialize(name, phone_number, email)
#name = name
#phone_number = phone_number
#email = email
end
def to_s
"Name: #{#name}\nPhone Number: #{#phone_number}\nEmail: #{#email}"
end
end
When testing my code with RSpec I receive the following error message:
.....F
Failures:
1) AddressBook.remove_entry removes only one entry from the address book
Failure/Error: expect(entry).to eq nil
expected: nil
got: [#<Entry:0x00000101bc82f0 #name="Ada Lovelace", #phone_number="010.012.1815", #email="augusta.king#lovelace.com">]
(compared using ==)
# ./spec/address_book_spec.rb:49:in `block (3 levels) in <top (required)>'
Finished in 0.02075 seconds (files took 0.14221 seconds to load)
6 examples, 1 failure
Failed examples:
rspec ./spec/address_book_spec.rb:44 # AddressBook.remove_entry removes only one entry from the address book
Just test that the book.entries association is empty:
expect(book.entries).to be_empty
As book is a local variable in your test, you will not get a false negative result if you keep your test atomic. Some best practices on rspec.
Edit:
You can also check the entry was not in the set:
expect(book.entries.index(entry)).to be_nil
or test the change of the array length with:
expect { book.remove_entry(entry) }.to change{book.entries.count}.by(-1)
If you wonder for the be_xxx syntax sugar, if the object respond to xxx?, then you can use be_xxx in your tests (predicate matchers)
I think your expect has an issue. The entry variable is not set to nil, but the entry inside book would be nil.
I think something like this would work better:
expect(book.entries.find { |e| e.name == "Ada Lovelace" }).to eq nil
Better still, your AddressBook could have its own find method, which would make the expect param much nicer, like book.find(:name => "Ada Lovelace").
Finally, I would also put an expect call before the remove_entry call, to make sure its result equals entry.

Readable test names with minitest

I'm using MiniTest on a new Rails project and this is my first time really doing testing. When a test fails the message looks like this
1) Failure:
Category::when created#test_0002_must have a unique name [/home/caleb/workspace/buzz/test/models/category_test.rb:10]:
Expected: true
Actual: false
Can you change #test_0002_ to another string to make the error more readable? I know it's a minor issue, but this seems like something that should be supported.
# Example test
require 'test_helper'
describe Category do
describe 'when created' do
unique = false
it 'must not have a unique name' do
unique.must_equal false
end
it 'must have a unique name' do
unique.must_equal true
end
end
end
Well, there is a lot here to cover, so bear with me.
First, the test names are readable. And they are 100% accurate. When you use the spec DSL you are still creating test classes and test methods. In your case, you class is Category::when created and your test method is test_0002_must have a unique name. The # in between them is a very common Ruby idiom for an instance method on a class, which is what your test method is. When you use class or def you can't create classes or methods with spaces in them, but when you create them programmatically you can. When running your code Ruby doesn't care if there are spaces in them or not.
Second, we can affect the display of test class and method. That text comes from a call to Minitest::Test#to_s. Here is what that looks like:
def to_s # :nodoc:
return location if passed? and not skipped?
failures.map { |failure|
"#{failure.result_label}:\n#{self.location}:\n#{failure.message}\n"
}.join "\n"
end
When the test fails then more info is returned, including the reason for the failure. But the piece we care about is the location. Here is what that looks like:
def location
loc = " [#{self.failure.location}]" unless passed? or error?
"#{self.class}##{self.name}#{loc}"
end
Ah, better. On the last line you can clearly see it is printing the class and the method name. If the test is failing the location also includes the filename where the method is defined. Let's break those values out so they aren't inline:
def location
loc = " [#{self.failure.location}]" unless passed? or error?
test_class = self.class
test_name = self.name
"#{test_class}##{test_name}#{loc}"
end
Okay, a bit clearer. First the test class, then the #, then the test name, then the location if the test is not passing. Now that we have them broken out we can modify them a bit. Let's use / to separate the class namespaces and the test method:
def location
loc = " [#{self.failure.location}]" unless passed? or error?
test_class = self.class.to_s.gsub "::", " / "
test_name = self.name
"#{test_class} / #{test_name}#{loc}"
end
Great. Now let's remove the test_0002_ from the beginning of the test method. That is added by the spec DSL, and by removing it we can make it match the string passed to the it block:
def location
loc = " [#{self.failure.location}]" unless passed? or error?
test_class = self.class.to_s.gsub "::", " / "
test_name = self.name.to_s.gsub /\Atest_\d{4,}_/, ""
"#{test_class} / #{test_name}#{loc}"
end
Now, your test output will look like this:
1) Failure:
Category / when created / must have a unique name [/home/caleb/workspace/buzz/test/models/category_test.rb:10]:
Expected: true
Actual: false
Minitest is no different than any other Ruby library. The spec DSL is simply a thin wrapper for creating test classes and methods. You can alter the behavior of your test objects to work the way you want them to.
TL;DR Add the following to your test/test_helper.rb file:
class Minitest::Test
def location
loc = " [#{self.failure.location}]" unless passed? or error?
test_class = self.class.to_s.gsub "::", " / "
test_name = self.name.to_s.gsub /\Atest_\d{4,}_/, ""
"#{test_class} / #{test_name}#{loc}"
end
end

Moching rails association methods

Here is my helper method which I want to test.
def posts_correlation(name)
if name.present?
author = User.find_by_name(name)
author.posts.count * 100 / Post.count if author
end
end
A factory for user.
factory :user do
email 'user#example.com'
password 'secret'
password_confirmation { password }
name 'Brian'
end
And finally a test which permanently fails.
test "should calculate posts count correlation" do
#author = FactoryGirl.create(:user, name: 'Jason')
#author.posts.expects(:count).returns(40)
Post.expects(:count).returns(100)
assert_equal 40, posts_correlation('Jason')
end
Like this.
UsersHelperTest:
FAIL should calculate posts count correlation (0.42s)
<40> expected but was <0>.
test/unit/helpers/users_helper_test.rb:11:in `block in <class:UsersHelperTest>'
And the whole problem is that mocha doesn't really mock the count value of author's posts, and it returns 0 instead of 40.
Are there any better ways of doing this: #author.posts.expects(:count).returns(40) ?
When your helper method runs, it's retrieving its own object reference to your author, not the #author defined in the test. If you were to puts #author.object_id and puts author.object_id in the helper method, you would see this problem.
A better way is to pass the setup data for the author in to your mocked record as opposed to setting up expectations on the test object.
It's been a while since I used FactoryGirl, but I think something like this should work:
#author = FactoryGirl.create(:user, name: 'Jason')
(1..40).each { |i| FactoryGirl.create(:post, user_id: #author.id ) }
Not terribly efficient, but should at least get the desired result in that the data will actually be attached to the record.

Resources