I'm in a situation where for our projects, there is a desire for 100% code coverage in our unit testing.
In order to support some dereference operations, I had to utilize the ability to chain dynamic method calls. So I ended up with this open class addition defined on one of my source files:
class Object
def call_method_chain(method_chain, arg=nil)
return self if method_chain.empty?
method_chain.split('.').inject(self) { |o,m|
if arg.nil?
o.send(m.intern)
else
o.send(m.intern, arg)
end
}
end
end
It works wonderfully. The problem is it's the only part of my application that isn't showing up as covered by unit tests, even though the area that calls the above method is covered.
I can't figure out how to cover the above with some unit tests.
I have no idea how much information is too much information here, but just to give an idea, here is the code that actually uses the above (in a file called data_setter.rb):
module Symbiont
module DataSetter
# #param data [Hash] the data to use
def using(data)
data.each do |key, value|
use_data_with(key, value) if object_enabled_for(key)
end
end
def use_data_with(key, value)
element = self.send("#{key}")
self.call_method_chain("#{key}.set", value) if element.class == Watir::TextField
self.call_method_chain("#{key}.set") if element.class == Watir::Radio
self.call_method_chain("#{key}.select", value) if element.class == Watir::Select
return self.call_method_chain("#{key}.check") if element.class == Watir::CheckBox and value
return self.call_method_chain("#{key}.uncheck") if element.class == Watir::CheckBox
end
private
def object_enabled_for(key)
web_element = self.send("#{key}")
web_element.enabled? and web_element.visible?
end
end
end
You can see using() calls use_data_with() and it's that latter method that relies on the call_method_chain() that I defined on Object.
Further, I have a unit test that exercises the using() method, which looks like this:
require 'spec_helper'
describe Symbiont::DataSetter do
include_context :page
include_context :element
it 'will set data in a text field' do
expect(watir_element).to receive(:visible?).and_return(true)
expect(watir_element).to receive(:enabled?).and_return(true)
expect(watir_browser).to receive(:text_field).with({:id => 'text_field'}).exactly(2).times.and_return(watir_element)
watir_definition.using(:text_field => 'works')
end
end
That test shows my logic calling the using() method. That test passes. When looking at my code coverage, the coverage of data_setter.rb is 100%. Yet the coverage report also shows that my call_method_chain() method is never being executed.
This is one of those cases where I fear strict adherence to unit testing coverage is causing more trouble than it's worth since I know the above logic is working.
EDIT (based on response from Neil):
I'm using SimpleCov for the coverage.
Regarding the coverage report and object, taking this:
1 class Object
2 def call_method_chain(method_chain, arg=nil)
3 return self if method_chain.empty?
4 method_chain.split('.').inject(self) { |o,m| #o.send(m.intern, arg) }
5 if arg.nil?
6 o.send(m.intern)
7 else
8 o.send(m.intern, arg)
9 end
10 }
11 end
12 end
The report says that lines 1 and 2 are covered. They show up as green. Starting from 3 down to 9, it's all red.
Regarding my spec_helper.rb file, this is what it looks like, at the top:
require 'simplecov'
require 'coveralls'
Coveralls.wear!
SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
SimpleCov::Formatter::HTMLFormatter,
Coveralls::SimpleCov::Formatter
]
SimpleCov.start do
add_filter '/spec'
coverage_dir "#{SimpleCov.root}/spec/reports/coverage"
minimum_coverage 90
maximum_coverage_drop 5
end
require 'symbiont'
I have found that if I make a simple test like this:
it 'allows methods to be chained' do
methods = 'testing'.call_method_chain("reverse.capitalize")
end
That covers up to line 6 of my Object insertion. That leaves line 8, which I can't quite get to work. I tried this:
methods = 'testing'.call_method_chain("reverse.capitalize.eql?", 'GNITSET')
To no avail, however. So I can actually get it down to only one line not being covered, assuming I just use those little point tests like that. I'm not sure if that's in the spirit of good unit testing.
Related
I'm using the AfterStep hooks inside calabash-ios/cucumber.
I want to know the last executed step inside my hook.
AfterStep do |scenario|
puts "Step: #{scenario.name} #{scenario.title} #{scenario.gherkin_statement}"
end
I can see that the scenario is passed in, but how do I access the currently running step? I don't see any information inside the scenario docs regarding this.
I would assume that the step would be passed into the AfterStep hook. Any clues?
You may refer to this example code, which works with step indexes within the AfterStep hook.
Example:
CALABASH_COUNT = {:step_index => 0, :step_line => nil}
#TODO change this approach as it breaks scenario outlines
Before do |scenario|
begin
CALABASH_COUNT[:step_index] = 0
CALABASH_COUNT[:step_line] = scenario.raw_steps[CALABASH_COUNT[:step_index]].line
rescue Exception => e
puts "#{Time.now} - Exception:#{e}"
end
end
AfterStep do |scenario|
CALABASH_COUNT[:step_index] = CALABASH_COUNT[:step_index] + 1
raw = scenario.raw_steps[CALABASH_COUNT[:step_index]]
CALABASH_COUNT[:step_line] = raw.line unless raw.nil?
end
The Behave BDD framework, in Python allows a more simple step.name type accessor, but others seem more difficult, requiring the above technique of counting the current step and then using the index to find the name from the raw steps text.
I'm trying to write a custom parser for my cucumber results. In doing so, I want to write rspec tests around it. What I currently have is as follows:
describe 'determine_test_results' do
it 'returns a scenario name as the key of the scenario results, with the scenario_line attached' do
pcr = ParseCucumberJsonReport.new
expected_results = {"I can login successfully"=>{"status"=>"passed", "scenario_line"=>4}}
cucumber_results = JSON.parse(IO.read('example_json_reports/json_passing.json'))
pcr.determine_test_results(cucumber_results[0]).should == expected_results
end
end
The problem is, determine_test_results has a sub method called determine_step_results, which means this is really an integration test between the 2 methods and not a unit test for determine_test_results.
How would I mock out the "response" from determine_step_results?
Assume determine_step_results returns {"status"=>"passed", "scenario_line"=>4}
what I have tried:
pcr.stub(:determine_step_results).and_return({"status"=>"passed", "scenario_line"=>6})
and
allow(pcr).to receive(:determine_step_results).and_return({"status"=>"passed", "scenario_line"=>6})
You could utilize stubs for what you're trying to accomplish. Project: RSpec Mocks 2.3 would be good reading regarding this particular case. I have added some code below as a suggestion.
describe 'determine_test_results' do
it 'returns a scenario name as the key of the scenario results, with the scenario_line attached' do
pcr = ParseCucumberJsonReport.new
expected_results = {"I can login successfully"=>{"status"=>"passed", "scenario_line"=>4}}
# calls on pcr will return expected results every time determine_step_results is called in any method on your pcr object.
pcr.stub!(:determine_step_results).and_return(expected_results)
cucumber_results = JSON.parse(IO.read('example_json_reports/json_passing.json'))
pcr.determine_test_results(cucumber_results[0]).should == expected_results
end
end
If all what determine_test_results does is call determine_step_results, you should not really test it, since it is trivial...
If you do decide to test it, all you need to test is that it calls the delegate function, and returns whatever is passed to it:
describe ParseCucumberJsonReport do
describe '#determine_test_results' do
it 'calls determine_step_results' do
result = double(:result)
input = double(:input)
expect(subject).to receive(:determine_step_results).with(input).and_return(result)
subject.determine_test_results(input).should == result
end
end
end
If it is doing anything more (like adding the result to a larger hash) you can describe it too:
describe ParseCucumberJsonReport do
describe '#determine_test_results' do
it 'calls determine_step_results' do
result = double(:result)
input = double(:input)
expect(subject).to receive(:determine_step_results).with(input).and_return(result)
expect(subject.larger_hash).to receive(:merge).with(result)
subject.determine_test_results(input).should == result
end
end
end
I have a 1.6gb xml file, and when I parse it with Sax Machine it does not seem to be streaming or eating the file in chunks - rather it appears to be loading the whole file into memory (or maybe there is a memory leak somewhere?) because my ruby process climbs upwards of 2.5gb of ram. I don't know where it stops growing because I ran out of memory.
On a smaller file (50mb) it also appears to be loading the whole file. My task iterates over the records in the xml file and saves each record to a database. It takes about 30 seconds of "idling" and then all of a sudden the database queries start executing.
I thought SAX was supposed to allow you to work with large files like this without loading the whole thing in memory.
Is there something I am overlooking?
Many thanks
Update to add code sample
class FeedImporter
class FeedListing
include ::SAXMachine
element :id
element :title
element :description
element :url
def to_hash
{}.tap do |hash|
self.class.column_names.each do |key|
hash[key] = send(key)
end
end
end
end
class Feed
include ::SAXMachine
elements :listing, :as => :listings, :class => FeedListing
end
def perform
open('~/feeds/large_feed.xml') do |file|
# I think that SAXMachine is trying to load All of the listing elements into this one ruby object.
puts 'Parsing'
feed = Feed.parse(file)
# We are now iterating over each of the listing elements, but they have been "parsed" from the feed already.
puts 'Importing'
feed.listings.each do |listing|
Listing.import(listing.to_hash)
end
end
end
end
As you can see, I don't care about the <listings> element in the feed. I just want the attributes of each <listing> element.
The output looks like this:
Parsing
... wait forever
Importing (actually, I don't ever see this on the big file (1.6gb) because too much memory is used :(
Here's a Reader that will yield each listing's XML to a block, so you can process each Listing without loading the entire document into memory
reader = Nokogiri::XML::Reader(file)
while reader.read
if reader.node_type == Nokogiri::XML::Reader::TYPE_ELEMENT and reader.name == 'listing'
listing = FeedListing.parse(reader.outer_xml)
Listing.import(listing.to_hash)
end
end
If listing elements could be nested, and you wanted to parse the outermost listings as single documents, you could do this:
require 'rubygems'
require 'nokogiri'
# Monkey-patch Nokogiri to make this easier
class Nokogiri::XML::Reader
def element?
node_type == TYPE_ELEMENT
end
def end_element?
node_type == TYPE_END_ELEMENT
end
def opens?(name)
element? && self.name == name
end
def closes?(name)
(end_element? && self.name == name) ||
(self_closing? && opens?(name))
end
def skip_until_close
raise "node must be TYPE_ELEMENT" unless element?
name_to_close = self.name
if self_closing?
# DONE!
else
level = 1
while read
level += 1 if opens?(name_to_close)
level -= 1 if closes?(name_to_close)
return if level == 0
end
end
end
def each_outer_xml(name, &block)
while read
if opens?(name)
yield(outer_xml)
skip_until_close
end
end
end
end
once you have it monkey-patched, it's easy to deal with each listing individually:
open('~/feeds/large_feed.xml') do |file|
reader = Nokogiri::XML::Reader(file)
reader.each_outer_xml('listing') do |outer_xml|
listing = FeedListing.parse(outer_xml)
Listing.import(listing.to_hash)
end
end
Unfortunately there are now three different repos for sax-machine. And worse, the gemspec version was not bumped.
Despite the comment on Greg Weber's blog, I don't think this code was integrated into pauldix's or ezkl's forks. To use the lazy, fiber-based version of the code, I think you need to specifically reference gregweb's version in your gemfile like this:
gem 'sax-machine', :git => 'https://github.com/gregwebs/sax-machine'
I forked sax-machine so that it uses constant memory: https://github.com/gregwebs/sax-machine
Good news: there is a new maintainer that is planning on merging my changes.
Myself and the new maintainer have been using my fork without issue for a year now.
You are right, SAXMachine reads the whole document eagerly. Have a look at it's handler sources: https://github.com/pauldix/sax-machine/blob/master/lib/sax-machine/sax_handler.rb
To solve your Problem, I would use http://nokogiri.rubyforge.org/nokogiri/Nokogiri/XML/SAX/Parser.html directly and implement the handler yourself.
I am working on Exercise 49 of Learn Ruby the Hard Way
The exercise asks to write a unit test for each function provided. One of the items I am testing is if a proper exception is raised. It is suggested that we use assert_raise for this purpose.
Here is the code I am testing:
class ParserError < Exception
end
Pair = Struct.new(:token, :word)
def peek(word_list)
begin
word_list.first.token
rescue
nil
end
end
def match(word_list, expecting)
word = word_list.shift
if word.token == expecting
word
else
nil
end
end
def skip_word(word_list, token)
while peek(word_list) == token
match(word_list, token)
end
end
def parse_verb(word_list)
skip_word(word_list, :stop)
if peek(word_list) == :verb
return match(word_list, :verb)
else
raise ParserError.new("Expected a verb next.")
end
end
And here is the test, for the function parse_verb:
def test_parse_verb
list_one = [Pair.new(:verb, 'go'), Pair.new(:noun, 'king')]
assert_equal(parse_verb(list_one), Pair.new(:verb, 'go'))
list_two = [Pair.new(:noun, 'player') ,Pair.new(:verb, 'go'), Pair.new(:noun, 'king')]
assert_raise(ParserError.new("Expected a verb next.")) {parse_verb(list_two)}
end
When I run the test, it fails and here is the message I get:
Larson-2:test larson$ ruby test_sentence.rb
Loaded suite test_sentence
Started
.F..
Finished in 0.001204 seconds.
1) Failure:
test_parse_verb(SentenceTests) [test_sentence.rb:36]:
[#<ParserError: Expected a noun or direction next.>] exception expected, not
Class: <ParserError>
Message: <"Expected a verb next.">
---Backtrace---
/Users/larson/Ruby/projects/ex48/lib/sentence.rb:45:in `parse_verb'
test_sentence.rb:36:in `block in test_parse_verb'
---------------
4 tests, 7 assertions, 1 failures, 0 errors, 0 skips
Test run options: --seed 40627
Based on my understanding of the assert_raise function, this test should pass, is there something wrong with the way I am using it?
If anybody would like a full source code of all the files I am working with I it is available here
assert_raise expects one or more exception classes as its parameters, rather than an instance of the required exception.
It also returns the exception raised so if you want to assert the message (or any other properties) you can do that separately. So try replacing:
assert_raise(ParserError.new("Expected a verb next.")) {parse_verb(list_two)}
with:
exception = assert_raise(ParserError) {parse_verb(list_two)}
assert_equal("Expected a noun or direction next.", exception.message)
For some reason, the answer given above didn't work for me (i'm using Ruby 2.0.0).
I has to wrap the Error class name in a String for it to work:
assert_raise("RuntimeError") {
# some code to trigger the error
}
n the .net world, my specs would follow the Arrange, Act, Assert pattern. I'm having trouble replicating that in rspec, because there doesn't appear to be an ability to selectively verify your mocks after the SUT has taken it's action. That, coupled with the fact that EVERY expectation is evaluated at the end of each 'It' block, is causing me to repeat myself in a lot of my specs.
Here's an example of what I'm talking about:
describe 'AmazonImporter' do
before(:each) do
Kernel.**stubs**(:sleep).with(1)
end
# iterates through the amazon categories, and for each one, loads ideas with
# the right response group, upserting ideas as it goes
# then goes through, and fleshes out all of the ideas that only have asins.
describe "find_new_ideas" do
before(:all) do
#xml = File.open(File.expand_path('../amazon_ideas_in_category.xml', __FILE__), 'r') {|f| f.read }
end
before(:each) do
#category = AmazonCategory.new(:name => "name", :amazon_id => 1036682)
#response = Amazon::Ecs::Response.new(#xml)
#response_group = "MostGifted"
#asin = 'B002EL2WQI'
#request_hash = {:operation => "BrowseNodeLookup", :browse_node_id => #category.amazon_id,
:response_group => #response_group}
Amazon::Ecs.**expects**(:send_request).with(has_entries(#request_hash)).returns(#response)
GiftIdea.expects(:first).with(has_entries({:site_key => #asin})).returns(nil)
GiftIdea.any_instance.expects(:save)
end
it "sleeps for 1 second after each amazon request" do
Kernel.**expects**(:sleep).with(1)
AmazonImporter.new.find_new_ideas(#category, #response_group)
end
it "loads the ideas for the given response group from amazon" do
Amazon::Ecs.**expects**(:send_request).
with(has_entries(#request_hash)).
returns(#response)
**AmazonImporter.new.find_new_ideas(#category, #response_group)**
end
it "tries to load those ideas from repository" do
GiftIdea.expects(:first).with(has_entries({:site_key => #asin}))
**AmazonImporter.new.find_new_ideas(#category, #response_group)**
end
In this partial example, I'm testing the find_new_ideas method. But I have to call it for each spec (the full spec has 9 assertion blocks). I further have to duplicate the mock setup so that it's stubbed in the before block, but individually expected in the it/assertion block. I'm duplicating or nearly duplicating a ton of code here. I think it's even worse than the highlighting indicates, because a lot of those globals are only defined separately so that they can be consumed by an 'expects' test later on. Is there a better way I'm not seeing yet?
(SUT = System Under Test. Not sure if that's what everyone calls it, or just alt.net folks)
You can use shared example groups to reduce duplication:
shared_examples_for "any pizza" do
it "tastes really good" do
#pizza.should taste_really_good
end
it "is available by the slice" do
#pizza.should be_available_by_the_slice
end
end
describe "New York style thin crust pizza" do
before(:each) do
#pizza = Pizza.new(:region => 'New York' , :style => 'thin crust' )
end
it_behaves_like "any pizza"
it "has a really great sauce" do
#pizza.should have_a_really_great_sauce
end
end
Another technique is to use macros, which is handy if you need similar specs in different classes.
Note: the example above is borrowed from The RSpec Book, Chapter 12.
you can separate them using "context" if that helps...
https://github.com/dchelimsky/rspec/wiki/faq