For example, it generated a log(my_work.log) content as:
I, [2015-05-14T00:00:00.000000 #5590] INFO -- : Work started.
I want to test if my_work.log has content Work started., how to do?
I don't want to match all line include datetime, because that contains #5590, I can't stub it.
You can pass in an instance of StringIO when initializing Logger to capture the output and then match on the expected content:
require 'logger'
describe "log" do
let(:log) { StringIO.new }
let(:logger) { Logger.new(log) }
let(:expected) { "Work started" }
it "should add the expected content to the log" do
logger.info(expected)
log.rewind
expect(log.read).to match(/.+#{expected}$/)
end
end
Rails.logger uses some methods to log things, for example:
debug
info
fatal
warn
So in your case you use info, to log something, instead loking for a match, you can detect if method info was called:
it 'logs exception' do
# some code
expect(Rails.logger).to receive(:info)
# execute the line that logs something
end
Even you can add parameters to receive method with reserved word with:
expect(Rails.logger).to receive(:info).with('Work started.')
This cause you need to specify something
Check rspec and rails logger
Also check this stackoverflow post
With RSpec's output matcher (introduced in 3.0) you can do the following:
expect { my_method }.to output("my message").to_stdout
expect { my_method }.to output("my error").to_stderr
In case of libraries such as Logger or Logging you may have to use output.to_<stdout/stderr>_from_any_process.
It's simple, clean and will test whether your messages actually reach the output.
Related
I have this code in one of my tests:
it 'returns ids when successful' do
allow_any_instance_of(Importer).to receive(:import).and_return('12589', '12590', '12591', '12592', '12593', '12594')
expect(#dispatcher.run).to eq(['12589', '12590', '12591', '12592', '12593', '12594'])
end
The test fails because it only return the first value:
expected: ["12589", "12590", "12591", "12592", "12593", "12594"]
got: ["12589", "12589", "12589", "12589", "12589", "12589"]
I just saw that #and_return's capability of returning multiple values only works when used with #allow.
What can I do for #allow_any_instance_of to get this behaviour?
EDIT:
The class I am testing is called Dispatcher. It takes an xml file, and splits it into parts concerning exactly one object. Each of those splitted parts is taken by the Importer which returns exactly one ID. The Dispatcher then creates an Array from those Ids. So, no, I am not expecting an array to be returned by the Importer.
The class I am testing, Dispatcher, calls Importer for every file it finds in an input directory.
Here's what should work (intercept importer creation)
class Dispatcher
def run
files.each do |file|
create_importer(file).import
end
end
def create_importer(file)
::Importer.new(file)
end
end
# spec
let(:fake_importer) { ::Importer.new }
before do
allow(#dispatcher).to receive(:create_importer).and_return(fake_importer)
allow(fake_importer).to receive(:import).and_return(your, multiple, values, here)
end
I was looking into logging actions for example when you create a new user it sends it to the logger etc.. so every action is logged. I can see how the logger.info sends information into the development.log file.
I was wondering how I Would set-up a different file e.g. users.log and then when I log a line or variable, it saves inside that log file instead of the development.log?
Ruby has a Logger class in its standard lib: http://ruby-doc.org/stdlib-2.1.0/libdoc/logger/rdoc/Logger.html
You would instantiate that and pass it the file path of your new log file, like this:
user_log = File.open('logs/users.log', File::WRONLY | File::APPEND)
You can place that in a controller method that your controllers can use. The first string argument is the path to the log file, and the following are opening the file for writing and appending only (so that each log line is added to the log rather than overwriting it each time).
You can customize the format of each log line by setting a formatter:
user_log.formatter = proc { |severity, datetime, progname, msg|
"#{severity}, #{datetime}, #{progname}, #{msg.dump}"
}
You can specify the file path used in the config file, which can vary according to the environment, as so:
config.paths.log = "/some/path/#{Rails.env}.log"
If you want to create different log files for each model, you can simply create a logger object when needed, as explained in this answer.
However if you just want to somehow mark different logs according to where they were generated, it may be easier to use tagged logging:
logger = ActiveSupport::TaggedLogging.new(Logger.new(STDOUT))
logger.tagged("BCX") { logger.info "Stuff" } # Logs "[BCX] Stuff"
logger.tagged("BCX", "Jason") { logger.info "Stuff" } # Logs "[BCX] [Jason] Stuff"
logger.tagged("BCX") { logger.tagged("Jason") { logger.info "Stuff" } } # Logs "[BCX] [Jason] Stuff"
I've got a question about how to share rspec-mocks' double between examples. I'm writing a new rails app with rspec-mocks 3.1.3. I'm used to using the old (< 2.14 and and trying to update my knowledge if current rspec usage.
I have a model method:
def self.from_strava(activity_id, race_id, user)
#client ||= Strava::Api::V3::Client.new(access_token: 'abc123')
activity = #client.retrieve_an_activity(activity_id)
result_details = {race_id: race_id, user: user}
result_details[:duration] = activity['moving_time']
result_details[:date] = Date.parse(activity['start_date'])
result_details[:comment] = activity['description']
result_details[:strava_url] = "http://www.strava.com/activities/#{activity_id}"
Result.create!(result_details)
end
And here is the spec:
describe ".from_strava" do
let(:user) { FactoryGirl.build(:user) }
let(:client) { double(:client) }
let(:json_response) { JSON.parse(File.read('spec/support/strava_response.json')) }
before(:each) do
allow(Strava::Api::V3::Client).to receive(:new) { client }
allow(client).to receive(:retrieve_an_activity) { json_response }
allow(Result).to receive(:create!)
end
it "sets the duration" do
expect(Result).to receive(:create!).with(hash_including(duration: 3635))
Result.from_strava('123', 456, user)
end
it "sets the date" do
expect(Result).to receive(:create!).with(hash_including(date: Date.parse("2014-11-14")))
Result.from_strava('123', 456, user)
end
end
When I run a single test on it's own it's fine, but when I run the whole describe ".from_strava" block it fails with the message
Double :client was originally created in one example but has leaked into another example and can no longer be used. rspec-mocks' doubles are designed to only last for one example, and you need to create a new one in each example you wish to use it for.
I understand what it's saying, but surely this is an appropriate use of a double being used in 2 examples. After all, the client double isn't important to the example, it's just a way for me to load the canned response. I guess I could use WebMock but that seems very low-level and doesn't translate well to the actual code written. We should only be asserting one thing per example after all.
I had thought about replacing the client double with a call to
allow(Strava::Api::V3::Client).to receive_message_chain(:new, :retrieve_an_activity) { json_response }
but that doesn't seem to be the right approach either, given that the documentation states that receive_message_chain should be a code smell.
So if I shouldn't use receive_message_chain, shared client double and also follow the standard DRY principle then how should I fix this?
I would love some feedback on this.
Thanks,
Dave
Caching clients for external components can often be really desired (keeping alive connections/any SSL setup that you might need, etc.) and removing that for the sake of fixing an issue with tests is not a desirable solution.
In order to fix your test (without refactoring your code), you can do the following to clear the instance variable after each of your tests:
after { Result.instance_variable_set("#client", nil) }
While admittedly, this is not the cleanest solution, it seems to be the simplest and achieves both, lets you have a clear setup with no state shared in between tests, and keep your client cached in "normal" operation mode.
surely this is an appropriate use of a double being used in 2 examples.
No, it's not. :) You're trying to use a class variable; do not do that because the variable doesn't span examples. The solution is to set the client each time i.e. in each example.
Bad:
#client ||= Strava::Api::V3::Client.new(access_token: 'abc123')
Good:
#client = Strava::Api::V3::Client.new(access_token: 'abc123')
I had the same use case in an app of mine, and we solved it by extracting the cacheing into a private method and then stubbing that method to return the double (instead of stubbing the new method directly).
For example, in the class under test:
def self.from_strava(activity_id, race_id, user)
activity = strava_client.retrieve_an_activity(activity_id)
...
end
private
def self.strava_client
#client ||= Strava::Api::V3::Client.new(access_token: 'abc123')
end
And in the spec:
let(:client) { double(:client) }
before { allow(described_class).to receive(:strava_client).and_return(client) }
...
TLDR: Add after { order.vendor_service = nil } to balance the before block. Or read on...
I ran into this, and it was not obvious where it was coming from. In order_spec.rb model tests, I had this:
describe 'order history' do
before do
service = double('VendorAPI')
allow(service).to receive(:order_count).and_return(5)
order.vendor_service = service
end
# tests here ..
end
And in my Order model:
def too_many_orders?
##vendor_service ||= VendorAPI.new(key: 'abc', account: '123')
return ##vendor_service.order_count > 10
end
This worked fine when I only ran rspec on order_spec.rb
I was mocking something completely different in order_controller_spec.rb a little differently, using allow_any_instance_of() instead of double and allow:
allow_any_instance_of(Order).to receive(:too_many_orders?).and_return(true)
This, too, tested out fine.
The confounding trouble is that when I ran the full suite of tests, I got the OP's error on the controller mock -- the one using allow_any_instance. This was very hard to track down, as the problem (or at least my solution) lay in the model tests where I use double/allow.
To fix this, I added an after block clearing the class variable ##vendor_service, balancing the before block's action:
describe 'order history' do
before do
service = double('VendorAPI')
allow(service).to receive(:order_count).and_return(5)
order.vendor_service = service
end
after do
order.vendor_service = nil
end
# tests here ..
end
This forced the ||= VendorAPI.new() to use the real new function in later unrelated tests, not the mock object.
Say I have the following test
describe "bob" do
subject {
response = get "/expensive_lookup"
JSON.parse(response.body)
}
its(["transaction_id"]) { should == 1 }
its(["order_id"]) { should == 33 }
end
Then for each its() {} the subject will be reevaluated, which in my case it is a very slow lookup.
I could bundle all my tests together in one like
describe "bob" do
subject(res) {
response = get "/expensive_lookup"
JSON.parse(response.body)
}
it "returns the right stuff" do
res["transaction_id"]).should == 1
res["order_id"].should == 33
end
end
But this makes it less obvious which line of the test has failed if there is a failure.
Is there a way I can stop the subject from being reevaluated for each it block?
You can put that in to a before(:all) block. I don't know if that syntax has changed in a new rspec version, but regardless, your test would become this:
before(:all) do
response = get "/expensive_lookup"
#res = JSON.parse(response.body)
end
it "returns the right transaction ID" do
#res["transaction_id"].should == 1
end
# etc
The pro is that the code in the before-all block gets run just once for your spec. The con is that, as you can see, you can't take advantage of the subject; you need to write each more explicitly. Another gotcha is that any data saved to the test database is not part of the transaction and will not be rolled back.
There are two possible source of issues
Network request is slow/prone to fail
You should really mock all you network requests, slow or not.
The gem VCR is excellent. It makes it trivial to run your request once and persist the result for subsequent testing.
Building the immutable subject is slow
If you have multiple it blocks, the subject will be rebuild every time. Assuming you don't modify the subject, you can build it once.
You can use before(:all):
before(:all) { #cache = very_long_computation.freeze }
subject { #cache }
Note: that I call freeze to avoid modifying it by mistake, but of course that's not a deep freeze so you still need to mind what you are doing. If you are mutating your subject, your tests are no longer independent and shouldn't share the subject.
I use mini_test for testing. I have a code part like below.
raise Type_Error, 'First Name must not be empty' if #person.first_name == nil
How can I write this code's test?
Thanks...
I think you want assert_raises, which asserts that the block/expression passed to it will raise an exception when run.
For example, one of my projects has the following minitest:
test 'attempting to create a CreditCard that fails to save to the payment processorshould prevent the record from being saved' do
assert_equal false, #cc.errors.any?
failure = "simulated payment processor failure"
failure.stubs(:success?).returns(false)
failure.stubs(:credit_card).returns(nil)
PaymentProcessor::CreditCard.stubs(:create).returns(failure)
assert_raises(ActiveRecord::RecordNotSaved) { create(:credit_card) }
end
What this does, in my case, is:
Create a simulated failure message from the payment processor API
Stub the creation of a credit card on the payment processor to return the simulated failure
Try to create a credit card, simulating that the payment processor has returned a failed status, and assert that my internal save/create method throws an error under these conditions
I should say that this test code includes things in addition to minitest, such as FactoryGirl, and (I think) shoulda and mocha matchers. In other words, what is shown above isn't strictly minitest code.
raise Type_Error, 'First Name must not be empty' if #person.first_name == nil
For testing above line, I wrote a test like below. I used minitest::spec for this.
def test_first_name_wont_be_nil
person.name = nil
exception = proc{ Context.new(person).call }.must_raise(TypeError)
exception.message.must_equal 'First Name must not be empty'
end
Context is place where make some process.