How can I test logger-messages with MiniTest? - ruby

I have an application and I want to test if I get correct
messages from my logger.
A short example (you may switch between log4r and logger):
gem 'minitest'
require 'minitest/autorun'
require 'log4r'
#~ require 'logger'
class Testlog < MiniTest::Test
def setup
if defined? Log4r
#log = Log4r::Logger.new('log')
#log.outputters << Log4r::StdoutOutputter.new('stdout', :level => Log4r::INFO)
else
#log = Logger.new(STDOUT)
#log.level = Logger::INFO
end
end
def test_silent
assert_silent{ #log.debug("hello world") }
assert_output(nil,nil){ #log.debug("Hello World") }
end
def test_output
#~ refute_silent{ #log.INFO("Hello") }#-> NoMethodError: undefined method `refute_silent'
assert_output("INFO log: Hello World\n",''){ #log.info("Hello World") }
end
end
But I get:
1) Failure:
Testlog#test_output [minitest_log4r.rb:27]:
In stdout.
Expected: "INFO log: Hello World\n"
Actual: ""
On my output screen I see the message.
I have similar results with Log4r::StderrOutputter and Log4r::Outputter.stdout.
So it seems it is send to the output screen, but it is not catched by minitest in STDOUT or STDERR.
Before I start to write a minitest-log4r-Gem:
Is there a possibility to test logger-output in minitest?
If not:
Any recommendations how to implement a minitest-log4r-Gem?
Examples what I could imagine:
define new outputter for minitest (Log4r::MinitestOutputter)
Mock the logger.
new assertions (add the new outputter as parameter?):
assert_message('INFO log: Hello World'){ #log.info("Hello World") }
assert_messages(:info => 1, :debug => 2){ #log.info("Hello World") } to count messages.
assert_info_messages('Hello World'){ #log.info("Hello World") }
assert_debug_messages('Hello World'){ #log.info("Hello World") }

You can set up a pipe, pass the writer from the pipe to the logger, and then use the reader from the pipe to test your assertions.
http://ruby-doc.org/core-2.1.0/IO.html#method-c-pipe
Something like:
require 'logger'
r, w = IO.pipe
log = Logger.new(w)
log.info "testing info log message"
output = r.gets
puts "Test passed: #{!!(/testing/ =~ output)}"

In meantime I created a minitest-logger-Gem
A code example how to use it:
require 'log4r'
require 'minitest-logger'
class Test_log4r < MiniTest::Test
def setup
#log = Log4r::Logger.new('log')
#log.level = Log4r::INFO
end
def test_output_1
assert_log(" INFO log: Hello World\n"){ #log.info("Hello World") }
end
def test_output_regex
assert_log(/Hello World/){ #log.info("Hello World") }
end
def test_silent
assert_silent_log(){
#log.debug("Hello World")
#~ #log.info("Hello World") #uncomment this to see a failure
}
refute_silent_log(){
#log.warn("Hello World") #comment this to see a failure
}
end
end
During the test a temporary outputter is added to the logger #log. After the test the outputter is removed again.
The gem supports log4r and logger.

#Puhlze answer is good. Just for non-blocking, check before hand if there is input available:
if IO.select([r], [], [], 0.01).nil?

Suppose we have this code here on a file called logger.rb:
require 'logger'
class Framework
attr_reader :logger
def initialize
#logger = Logger.new("/tmp/minitest.log")
end
end
class Custom
def initialize(framework)
#framework = framework
end
def error!
raise StandardError, 'Forced error'
rescue StandardError => e
#framework.logger.error "Error: #{e}"
end
end
And we need to test the logger error messages. We can use a stub method and a StringIO object:
require 'minitest'
require 'minitest/autorun'
require_relative 'logger.rb'
class LoggerTest < MiniTest::Test
def test_logger
framework = Framework.new
custom = Custom.new(framework)
io = StringIO.new
framework.stub :logger, Logger.new(io) do
custom.error!
assert_match(/Forced error/, io.string)
end
end
end
This way we don't need to override the framework logger, just stub it.

Related

Uninitialized constant NameError in Rspec

When I run rails c, I can call the following class and the method works:
test = SlackService::BoardGameNotifier
test.create_alert("test")
>>method works
I'm trying to set this up in rspec like this:
require 'spec_helper'
require 'slack-notifier'
RSpec.describe SlackService::BoardGameNotifier do
describe '#notify' do
#notifier = SlackService::BoardGameNotifier
it 'pings Slack' do
error = nil
message = "test"
expect(notifier).to receive(:ping).with(message)
notifier.send_message()
end
end
end
But I keep getting the error:
NameError:
uninitialized constant SlackService
Does this have to do with how I set up the module?
My current setup:
slack_service/board_game_notifier.rb
module SlackService
class BoardGameNotifier < BaseNotifier
WEBHOOK_URL = Rails.configuration.x.slack.url
DEFAULT_OPTIONS = {
channel: "board-games-channel",
text: "board games alert",
username: "bot",
}
def create_alert(message)
message #testing
end
end
end
slack_service/base_notifier.rb
module SlackService
class BaseNotifier
include Singleton
def initialize
webhook_url = self.class::WEBHOOK_URL
options = self.class::DEFAULT_OPTIONS
#notifier = Slack::Notifier.new(webhook_url, options)
end
def self.send_message
message = instance.create_alert("test")
instance.notify(message)
end
def notify(message)
#notifier.post blocks: message
end
end
end
Add this to your spec_helper.rb
# spec_helper.rb
ENV["RAILS_ENV"] ||= "test"
require File.expand_path("../config/environment", __dir__)
When running RSpec, Rails doesn't automatically boot up, and therefore doesn't automatically load all the libraries.
Also, I'd suggest creating a .rspec in your app's root folder with the following lines so that spec_helper is automatically loaded for all your RSpec tests:
# .rspec
--format documentation
--color
--require spec_helper
I would use the described_class from Rspec
require 'spec_helper'
require 'slack-notifier'
RSpec.describe ::SlackService::BoardGameNotifier do
describe '#notify' do
it 'pings Slack' do
error = nil
message = "test"
expect(described_class).to receive(:ping).with(message)
notifier.send_message()
end
end
end

Ruby: const_set outside block?

I want to mock a class with Ruby.
How do I write a method that will take care of the boilerplate code?
The following code:
module Mailgun
end
module Acani
def self.mock_mailgun(mock)
temp = Mailgun
const_set(:Mailgun, mock)
p Mailgun
yield
ensure
const_set(:Mailgun, temp)
end
end
Acani.mock_mailgun('mock') { p Mailgun }
prints:
"mock"
Mailgun
What's going on here? Why is Mailgun its original value inside the block? Does this have to do with Ruby bindings?
Ruby version: 2.1.1p76
Try putting Object. before each const_set.
The code in the question is simplified. Here is the pertinent code:
test/test_helper.rb
require 'minitest/autorun'
module Acani
def self.const_mock(const, mock)
temp = const_get(const)
const_set_silent(const, mock)
yield
ensure
const_set_silent(const, temp)
end
private
def self.const_set_silent(const, value)
temp = $VERBOSE
$VERBOSE = nil
Object.const_set(const, value)
ensure
$VERBOSE = temp
end
end
test/web_test.rb
require 'test_helper'
require 'rack/test'
require_relative '../web'
class AppTest < MiniTest::Test
include Rack::Test::Methods
def app
Sinatra::Application
end
def test_password_reset
post '/users', {email: 'user1#gmail.com', password: 'password1'}
mailgun_mock = MiniTest::Mock.new
mailgun_mock.expect(:send, 200, [Hash])
Acani.const_mock(:Mailgun, mailgun_mock) do
post '/password_resets', {email: 'user1#gmail.com'}
end
mailgun_mock.verify
assert_equal 201, last_response.status
end
end

How do I log correctly inside my Ruby gem?

Currently I'm using puts, but I'm sure that's not the correct answer. How do I correctly setup a logger, inside my gem, to output my internal logging instead of puts?
The most flexible approach for users of your gem is to let them provide a logger rather than setting it up inside the gem. At its simplest this could be
class MyGem
class << self
attr_accessor :logger
end
end
You then use MyGem.logger.info "hello" to log messages from your gem (you might want to wrap it in a utility method that tests whether a logger is set at all)
Users of your gem can then control where messages get logged to (a file, syslog, stdout, etc...)
You can keep the logger in your top-level module. Allow user's to set their own logger but provide a reasonable default for those who don't care to deal with logging. For e.g.
module MyGem
class << self
attr_writer :logger
def logger
#logger ||= Logger.new($stdout).tap do |log|
log.progname = self.name
end
end
end
end
Then, anywhere within your gem code you can access the logger. For e.g.
class MyGem::SomeClass
def some_method
# ...
MyGem.logger.info 'some info'
end
end
References:
Using the Ruby Logger
Logger
A little example:
gem 'log4r'
require 'log4r'
class MyClass
def initialize(name)
#log = Log4r::Logger.new(name)
#Add outputter
#~ log.outputters << Log4r::FileOutputter.new('log_file', :filename => 'mini_example.log', :level => Log4r::ALL )
log.outputters << Log4r::StdoutOutputter.new('log_stdout') #, :level => Log4r::WARN )
#~ log.outputters << Log4r::StderrOutputter.new('log_stderr', :level => Log4r::ERROR)
#log.level = Log4r::INFO
#log.info("Creation")
#~ #log.level = Log4r::OFF
end
attr_reader :log
def myfunction(*par)
#log.debug("myfunction is called")
#log.warn("myfunction: No parameter") if par.empty?
end
end
x = MyClass.new('x')
x.myfunction
y = MyClass.new('y')
y.myfunction
y.myfunction(:a)
y.log.level = Log4r::DEBUG
y.myfunction(:a)
During initialization you create a Logger (#log). In your methods you call the logger.
With #log.level= (or MyClass#log.level=) you can influence, which messages are used.
You can use different outputters (in my example I log to STDOUT). You can also mix outputters (e.g. STDOUT with warnings, each data (including DEBUG) to a file...)
I think the easiest approach is to use it this way
Rails.logger.info "hello"

Gem not outputting the expected text

I've completed the First Gem section of this tutorial. However, in the Requiring More Gems section when I tried modifying my hola.rb as such:
class Hola
def self.hi(language = :english)
translator = Translator.new(language)
translator.hi
end
end
require 'hola/translator'
and added this translator.rb file:
class Hola::Translator
def initialize(language)
#language = language
end
def hi
case #language
when :spanish
"hola mundo"
else
"hello world"
end
end
end
If I typed in these commands in IRB, I should get identical outputs:
% irb -Ilib -rhola
irb(main):001:0> Hola.hi(:english)
=> "hello world"
irb(main):002:0> Hola.hi(:spanish)
=> "hola mundo"
However, I'm not getting proper results. I tried modifying my gemspec and reinstalling the gem, but no success. What do you think I did wrong in this case?
stanley#ubuntu:~/Github/webdev_class/ruby/hola_stanley/lib$ irb -Ilib -rhola
irb(main):001:0> Hola.hi(:english)
=> "hello world"
irb(main):002:0> Hola.hi(:spanish)
=> "hello world"
irb(main):003:0> quit
I don't see, what's wrong, but are you sure there was no type and you requested the file you expected?
I would modify the code a bit:
class Hola::Translator
def initialize(language, exceptions = false)
#language = language
#exceptions = exceptions
end
def hi
case #language
when :spanish
"hola mundo"
when :english
"hello world"
else
raise ArgumentError, "Unsupported language #{#language.inspect}" if #exceptions
"hello world"
end
end
end
You support two languages: Spanish and English. Other languages are detected and you can select, if you get an error or an alternative (english) text.
You can start it with:
class Hola
def self.hi(language = :english)
translator = Translator.new(language, true) ## <-- Modified
translator.hi
end
end
require 'hola/translator'
One advantage of the expection: You can see, which file really calls the exception.
Some other remarks:
I would recommend to define Hola as a module, not a class.
Instead the raise you could define a logger and report an error.
Example:
require 'log4r'
module Hola
LOG = Log4r::Logger.new('Hola')
class Translator
def initialize(language)
#language = language
end
def hi
case #language
when :spanish
"hola mundo"
when :english
"hello world"
else
LOG.error("Unsupported language #{#language.inspect}")
"hello world"
end
end
end
def self.hi(language = :english)
translator = Translator.new(language)
translator.hi
end
end #module Hola
p Hola.hi
p Hola.hi(:english)
p Hola.hi(:spanish)
p Hola.hi(:german)
puts "Activate Hola-warnings"
Hola::LOG.outputters << Log4r::StdoutOutputter.new('stdout')
p Hola.hi
p Hola.hi(:english)
p Hola.hi(:spanish)
p Hola.hi(:german)
After activating the logger, you are informed, if you have a type in :spanish (that's the error I would expect).

stream multiple body using async sinatra

I would like start a long poll request from javascript which is fine and i expect my ruby prog to stream multiple body sections to the javascript. Why doesn the following (pseudo)code work?
require 'rubygems'
require 'sinatra/async'
require 'eventmachine'
require 'thin'
require 'json'
class Test < Sinatra:Base
register Sinatra::Async
aget '/process' do
for c in 1..10
body {
{ :data => [ "this is part #{c}" ] }.to_json
end
end
end
run!
end
Maybe i misunderstood what long polling and async is supposed to do, but my expectation is that i get multiple bodies sent back to the client ? Do i need to use eventmachine or something?
thanks
require 'rubygems'
require 'sinatra/async'
require 'thin'
require 'json'
class Test < Sinatra::Base
register Sinatra::Async
class JSONStream
include EventMachine::Deferrable
def stream(object)
#block.call object.to_json + "\n"
end
def each(&block)
#block = block
end
end
aget '/process' do
puts 'ok'
out = JSONStream.new
body out
EM.next_tick do
c = 0
timer = EM.add_periodic_timer(0.3) do
c += 1
out.stream :data => ["this is part #{c}"]
if c == 100
timer.cancel
out.succeed
end
end
end
end
run!
end
See also: http://confreaks.net/videos/564-scotlandruby2011-real-time-rack
It appears in the example below that you need an EventMachine event to trigger the sending of the multiple bodies. Also see this previous answer as well.
require 'sinatra/async'
class AsyncTest < Sinatra::Base
register Sinatra::Async
aget '/' do
body "hello async"
end
aget '/delay/:n' do |n|
EM.add_timer(n.to_i) { body { "delayed for #{n} seconds" } }
end
end

Resources