Latest news about Bitcoin and all cryptocurrencies. Your daily crypto news habit.
Question: Wouldnât it be great if you could capture all app input and output as a string to run tests on that? And even better if you could mock input with a string, and run tests on the outcome of using it?
Answer: StringIOPost contents
- What is StringIO and why use it?
- Key methods when using the StringIOÂ class
- Designing an application to use an output object
- Using StringIO in your tests
- Working with logs
- Taking tests to the next level with StringIO mock input
- Putting it all together: a complete example
- Find out more
What is StringIO and why use it?
StringIO is a class in several programming languages, including Ruby, Python and Elixir, that allows you to print to and read from it in the same way you would to a file or output stream, such as standard output or standard error. This means you can:
- Test your output, logs and errors without having to test method calls on your application or logger.
- Check and modify the contents of the StringIO using String methods, such as include?.
This post will focus on examples using Ruby and its testing framework RSpec, but once you understand the principles used it should be adaptable to other languages (or you could even create your own custom StringIO object!).
Key methods when using the StringIOÂ classRuby docs:Â StringIO
There are several methods that are most useful to be familiar with when using StringIO instances. First of all is #new:
- new(string=ââ[, mode]) â Creates new StringIO instance from with string and mode. With no arguments it will create it with a blank string.
To see what has been written to this, you call the method #string
- string â Returns underlying String object, the subject of IO.
Calling Rubyâs input and output methods, such as puts and gets will print to or read from the StringIOÂ string.
This class has many methods that replicate reading and writing to a file, such as #open and #each_line.
A full list of the methods you can use with StringIO can be seen here- Ruby docs: StringIODesigning an application to use an output object
Testing output of a Ruby or Ruby on Rails application need not be difficult. Design an application, class, module or method to take output as an argument and you can replace this with an instance of the StringIO class in your tests.
This means you can test whatever has been sent to the output by checking the contents of this string.
Here is how you could design a class that initializes with a default output of $stdout that can be replaced with a StringIO instance.
class Printer attr_reader :output
def initialize(output = $stdout) @output = output end
def print_hi output.puts 'hi' endend
This class then calls puts âhiâ on $stdout, which sends the string to Standard Output.
printer = Printer.new($stdout)printer.print_hi=>'hi'
With the design of this class, you can substitute the $stdout object with a StringIO instance. This means that all the output is stored in the StringIO instance, which means you can check what has been sent to it.
output = StringIO.newprinter = Printer.new(output)printer.print_hiputs output.string=> 'hi'
The same principle applies if you only wanted to use a method.
def print_string(string, output = $stdout) output.puts stringend
print_string('hi')=> 'hi'
output = StringIO.newprint_string('hi', output)puts output.string=> 'hi'
Using StringIO in your tests
Now you can take control of testing output easily in your tests. The structure of an RSpec test involves setting up your StringIO instance, your logger (if you are testing logs), and your application/class/module/method.
require 'rspec'
RSpec.describe Printer do let(:output) { StringIO.new }
def setup_app described_class.new(output) endend
Call the #setup_app method before you test something to start a test with a blank StringIO.
describe '#say_hi' do subject { setup_app }
it 'prints hi' do subject.print_hiexpect(output.string).to eq 'hi'endend
As well as eq to test equality, another RSpec matcher to use with StringIO is include
expect("a string").to include("str")expect("a string").to include("str", "g")expect("a string").not_to include("foo")
Working with logs
There are various ways to test what is logged when an application is run. One way is to use the syntax:
expect(logger).to receive(:info).with('test string')
Using StringIO means that you do not have to worry about stubbing method calls and messages being sent to the logger. You can simply see what has been sent to output by the logger.
Ruby and Ruby on Rails Loggers can be initialized with a specified output. This means you can also pass it a StringIO instance instead.
Create a logger which logs messages to STDERR/STDOUT.
logger = Logger.new(STDERR)logger = Logger.new(STDOUT)
- Ruby Docs: Logger
As for a Ruby on Rails application, this uses a Rails.logger instance, which you can overwrite:
Rails.logger = Logger.new(STDOUT)
- Rails Guides: What is the logger?
Passing a StringIO instance to your logger initialization means you can capture what it outputs.
output = StringIO.newlogger = Logger.new(output)logger.info 'test string'puts output.string => I, [2018-03-04T19:25:32.687254 #23814] INFO -- : test string
For more on using Loggers and searching its output using bash commands- Ryan Davidson in HackernoonâââTo boldly log: debug Docker apps effectively using logs options, tail and grep
Now, to write a class that uses a logger⊠you generally want to be able to pass it as an argument upon initialization to follow the design principle of dependency injection.
For more on this see Christian Palingâs post: Introduction to dependency injection in Ruby
require 'logger'
class LogPrinter attr_reader :output, :logger
def initialize(logger = Logger.new($stdout), output = $stdout)@output = output@logger = logger || Logger.new(output) end
def choose_option(choice) logger.info "You chose #{choice}" endend
NOTE: this structure also defines defaults for the Logger and output so if you donât want to define them outside of your tests you wonât need to.
To test the Logger output using StringIO you follow the same structure as when testing a class, but pass the StringIO output object to the Logger instance, then the Logger instance to the class upon initialization.
require 'spec_helper'
RSpec.describe LogPrinter do let(:output) { StringIO.new } let(:logger) { Logger.new(output) }
def setup_app described_class.new(logger, output) end
describe '#choose_option' do subject { setup_app } it "logs 'Your choice is 2'" do subject.choose_option(2) expect(output.string).to include 'INFO -- : You chose 2' end endend
Run the tests⊠it passes!
Taking StringIO tests to the next level with mock input
There is a lot you more can do with StringIO. Another feature to demonstrate is if you need to also set up a test with input, such as from $stdin, and then test the output of your application/class/module/method etc.
For example, if you were wanting to test an application that uses the command line, you can send it a StringIO string object to mock input from $stdin. Again, you inject this upon initialization, just like you did with the output object.
class Chooser def initialize(input = $stdin, output = $stdout)@input = input @output = output end
def choose_option output.puts âPick an option from 1, 2, or 3â choice = @input.gets output.puts âYou chose #{choice}â endend
This means you add the input object to the #setup_app method in your tests.
let(:output) { StringIO.new }
def setup_app(input_string)input = StringIO.new(input_string) described_class.new(input, output)end
For each test you can now just specify the input you want to test, and examine the output string to see the outcome.
require ârspecâ
RSpec.describe Chooser do let(:output) { StringIO.new }
def setup_app(input_string) input = StringIO.new(input_string) described_class.new(input, output) end
describe â#choose_optionâ do context âinput is 2â dolet(:input_string) { â2\nâ } subject { setup_app(input_string) } it âoutputs âYour choice is 2ââ do subject.choose_option expect(output.string).to include âYour choice is 2â end end end
end
Putting it all together: a complete example
Here is a full example of using StringIO with input, output and Loggers. The class:
Source on Github: log_printer.rb
âŠand the test:
Source on Github: log_printer_spec.rbFind out more
Now testing the effects of input and output on your app and loggers is straightforward using the StringIOÂ class.
If you are working with another language, such as Python, why not have a go at converting these test structures for your own work?
Here are a selection of posts, testing book and web links to read up more on testing and StringIO.
Follow me on Twitter for more software development postsRead more from Ryan Davidson
- Testing private methods in Ruby using Rspec
- Target individual RSpec tests withâââexample andâââfail-fast flags
- Published in HackernoonâââTo boldly log: debug Docker apps effectively using logs options, tail and grep
- A roundup of my posts on: Testing Ruby applications with RSpec
Recommended book
- Effective Testing with RSpec 3 has been released! (in particular this bookâs section Faking I/O with StringIO)
Read more from the web
- Ruby Doc:Â StringIO
- Ruby Doc:Â Logger
- Rails Guides: What is the logger?
- 8th Light blogâââTesting Code Thatâs hard to test
- Python docs:Â StringIO
StringIO theory: test output and logs easily using the StringIO class was originally published in Hacker Noon on Medium, where people are continuing the conversation by highlighting and responding to this story.
Disclaimer
The views and opinions expressed in this article are solely those of the authors and do not reflect the views of Bitcoin Insider. Every investment and trading move involves risk - this is especially true for cryptocurrencies given their volatility. We strongly advise our readers to conduct their own research when making a decision.