Readable test names with minitest - ruby-on-rails

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

Related

Rspec mocks and stubs confuse with expect

I have confuse when use mocks and stubs in rspec on rails. I have test like below
require 'rails_helper'
class Payment
attr_accessor :total_cents
def initialize(payment_gateway, logger)
#payment_gateway = payment_gateway
#logger = logger
end
def save
response = #payment_gateway.charge(total_cents)
#logger.record_payment(response[:payment_id])
end
end
class PaymentGateway
def charge(total_cents)
puts "THIS HITS THE PRODUCTION API AND ALTERS PRODUCTION DATA. THAT'S BAD!"
{ payment_id: rand(1000) }
end
end
class LoggerA
def record_payment(payment_id)
puts "Payment id: #{payment_id}"
end
end
describe Payment do
it 'records the payment' do
payment_gateway = double()
allow(payment_gateway).to receive(:charge).and_return(payment_id: 1234)
logger = double('LoggerA')
expect(logger).to receive(:record_payment).with(1234)
payment = Payment.new(payment_gateway, logger)
payment.total_cents = 1800
payment.save
end
end
Ok when I run rspec it works, no problem, but when I try to move expect to last line like below:
payment = Payment.new(payment_gateway, logger)
payment.total_cents = 1800
payment.save
expect(logger).to receive(:record_payment).with(1234)
and I try to run rpsec, it fail, I dont know why expect is last line will fail, I thought that expect always puts in last line before we run something to get result to test. Anyone can explain for me ?
expect(sth).to receive sets a message expectation which is to be satisfied between the call and end of the test, and that expectation is verified after the test finishes. When you move the expect to the last line, expectation is set just at the end of the test and no code is executed to satisfy it so it fails. Unfortunately it means breaking the prepare-execute-test order.
Which is why you should really rarely use expect.to receive and replace it with allow.to receive with expect.to have_received
# prepare
allow(logger).to receive(:record_payment)
# execute
..
# test
expect(logger).to have_received(:record_payment).with(1234)
allow.to receive sets up a mock proxy which starts tracing received messages which then can be explicitly verified by expect.to have_received. Some objects automatically sets their mock proxies, for example you don't need allow.to receive for doubles with predefined responses or spies. In your case, you could write the test like:
payment_gateway = double
allow(payment_gateway).to receive(:charge).and_return(payment_id: 1234)
logger = double('LoggerA', record_payment: nil)
payment = Payment.new(payment_gateway, logger)
payment.total_cents = 1800
payment.save
expect(logger).to have_received(:record_payment).with(1234)
Other notes
I strongly recommend using verifiable_doubles, which will protect you from false positives:
payment_gateway = instance_double(PaymentGateway)
allow(payment_gateway).to receive(:charge).and_return(payment_id: 1234)
This test will now raise an exception if there is no charge method defined on PaymentGateway class - protecting you from your tests passing even in case you rename that method but forgot to rename it in the test and implementation.

How to use CustomHealthCheck with health_check gem in ruby?

From the health_check official site, we know that it can add a config.add_custom_check block in the config file:
https://github.com/ianheggie/health_check
# Add one or more custom checks that return a blank string if ok, or an error message if there is an error
config.add_custom_check do
CustomHealthCheck.perform_check # any code that returns blank on success and non blank string upon failure
end
# Add another custom check with a name, so you can call just specific custom checks. This can also be run using
# the standard 'custom' check.
# You can define multiple tests under the same name - they will be run one after the other.
config.add_custom_check('sometest') do
CustomHealthCheck.perform_another_check # any code that returns blank on success and non blank string upon failure
end
But about the CustomHealthCheck class, how to define it?
For okcomputer gem, it offers a way like this:
https://github.com/sportngin/okcomputer
# config/initializers/okcomputer.rb
class MyCustomCheck < OkComputer::Check
def check
if rand(10).even?
mark_message "Even is great!"
else
mark_failure
mark_message "We don't like odd numbers"
end
end
end
OkComputer::Registry.register "check_for_odds", MyCustomCheck.new
Didn't find the usage about health_check gem.
Update
I have tried:
Add these source in the config/initializers/health_check.rb file:
class CustomHealthCheck
def perform_check
if rand(10).even?
p "Even is great!"
else
p "We don't like odd numbers"
end
end
end
HealthCheck.setup do |config|
...
Run curl -v localhost:3000/health_check.json, got:
{"healthy":false,"message":"health_check failed: undefined method `perform_check' for CustomHealthCheck:Class"}%
Update 2
Edited source in config/initializers/health_check.rb:
class CustomHealthCheck
def self.perform_check
p 'OK'
end
end
HealthCheck.setup do |config|
...
Got:
{"healthy":false,"message":"health_check failed: OK"}%
Success is defined by returning an empty or blank string. Right now your perform_check always returns the string "OK" which will be seen as failure.
Try this to get a passing health check:
class CustomHealthCheck
def self.perform_check
everything_is_good = true # or call some method to do more elaborate checking
return everything_is_good ? "" : "We've got Problems"
end
end

Ruby: Default values for define?

I have a question about define my main issue is I am a bit confused on how the parameters work for it.
This is my Methods
def repeat(repeated_word)
#repeated_word = repeated_word
"##repeated_word ##repeated_word"
end
This is my rspec test to make sure my method works.
describe "repeat" do
it "should repeat" do
repeat("hello").should == "hello hello"
end
# Wait a second! How can you make the "repeat" method
# take one *or* two arguments?
#
# Hint: *default values*
it "should repeat a number of times" do
repeat("hello", 3).should == "hello hello hello"
end
end
it passes the first test but fails the second. My confusion is if i add a second parameter meaning def repeat(repeat_word, times_repeated)
the first test then fails because it has the wrong number of arguments. Not sure how to set up default values?
def repeat(repeated_word, repeats=2)
repeats.times.map { repeated_word }.join(' ')
end

How to test the number of database calls in Rails

I am creating a REST API in rails. I'm using RSpec. I'd like to minimize the number of database calls, so I would like to add an automatic test that verifies the number of database calls being executed as part of a certain action.
Is there a simple way to add that to my test?
What I'm looking for is some way to monitor/record the calls that are being made to the database as a result of a single API call.
If this can't be done with RSpec but can be done with some other testing tool, that's also great.
The easiest thing in Rails 3 is probably to hook into the notifications api.
This subscriber
class SqlCounter< ActiveSupport::LogSubscriber
def self.count= value
Thread.current['query_count'] = value
end
def self.count
Thread.current['query_count'] || 0
end
def self.reset_count
result, self.count = self.count, 0
result
end
def sql(event)
self.class.count += 1
puts "logged #{event.payload[:sql]}"
end
end
SqlCounter.attach_to :active_record
will print every executed sql statement to the console and count them. You could then write specs such as
expect do
# do stuff
end.to change(SqlCounter, :count).by(2)
You'll probably want to filter out some statements, such as ones starting/committing transactions or the ones active record emits to determine the structures of tables.
You may be interested in using explain. But that won't be automatic. You will need to analyse each action manually. But maybe that is a good thing, since the important thing is not the number of db calls, but their nature. For example: Are they using indexes?
Check this:
http://weblog.rubyonrails.org/2011/12/6/what-s-new-in-edge-rails-explain/
Use the db-query-matchers gem.
expect { subject.make_one_query }.to make_database_queries(count: 1)
Fredrick's answer worked great for me, but in my case, I also wanted to know the number of calls for each ActiveRecord class individually. I made some modifications and ended up with this in case it's useful for others.
class SqlCounter< ActiveSupport::LogSubscriber
# Returns the number of database "Loads" for a given ActiveRecord class.
def self.count(clazz)
name = clazz.name + ' Load'
Thread.current['log'] ||= {}
Thread.current['log'][name] || 0
end
# Returns a list of ActiveRecord classes that were counted.
def self.counted_classes
log = Thread.current['log']
loads = log.keys.select {|key| key =~ /Load$/ }
loads.map { |key| Object.const_get(key.split.first) }
end
def self.reset_count
Thread.current['log'] = {}
end
def sql(event)
name = event.payload[:name]
Thread.current['log'] ||= {}
Thread.current['log'][name] ||= 0
Thread.current['log'][name] += 1
end
end
SqlCounter.attach_to :active_record
expect do
# do stuff
end.to change(SqlCounter, :count).by(2)

Passing a simple test

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.

Resources