Testing files and IO

Dir.mktmpdir

A file test needs a real file; a real file needs a folder nobody else uses. Dir.mktmpdir makes one in the system temporary area, and the block removes it even when the test raises:

require "minitest/autorun" require "tmpdir" require "fileutils" class ReportTest < Minitest::Test def setup @dir = Dir.mktmpdir end def teardown FileUtils.remove_entry(@dir) end end

The setup/teardown pair is the same guarantee as the block, written where a test class needs the folder in several tests. The folder name is random, so two runs at once do not collide.

The class

The class under test writes a CSV-shaped report into a folder it is given. Taking the folder as an argument is what makes it testable:

class Report def initialize(dir) = @dir = dir def write(rows) path = File.join(@dir, "report.txt") File.write(path, rows.map { |row| row.join(",") }.join("\n") + "\n") end end

The test

The assertion reads the file back and compares the whole content. A test that checks the file exists passes for the wrong reason; the bytes are the behaviour:

def test_writes_rows Report.new(@dir).write([["a", 1], ["b", 2]]) assert_equal "a,1\nb,2\n", File.read(File.join(@dir, "report.txt")) end

The expected string contains the exact newlines. A test written with readlines and a chomp would pass while the file gained a blank last line, which is the kind of difference a downstream parser notices.

Capturing output

A method that prints is tested by capturing what it printed. capture_io replaces $stdout and $stderr for the block and returns both:

def test_captures_stdout out, _err = capture_io { puts "hello" } assert_equal "hello\n", out end

ruby test/report_test.rb

.. Finished in 0.001841s, 1086.4281 runs/s, 1086.4281 assertions/s. 2 runs, 2 assertions, 0 failures, 0 errors, 0 skips

A test that prints during a run and does not capture is noise in the report. capture_io keeps the assertion on the text and the run quiet.

What to watch for

A test that writes into the project folder leaves a file behind and fails on the second run when the content is already there. Every write goes into the temporary folder, and the teardown removes it.

A path with __dir__ in a test points at the test file, not at the project root. A fixture under test/fixtures/ is reached from the test file with File.join(__dir__, "fixtures", name), which works from any working directory.

Next: Metaprogramming

Article author: Arthur Isaev