blake.app

Preventing console output in tests

I’m working on creating some rake tasks to help delete unwanted categories in a Discourse site. Deleting categories through the UI is actually a very tedious process because it won’t allow you to delete a category if it has subcategories nor will it allow you to delete a category if it contains topics. So if there was a category you wanted to delete you would manually have to delete all the subcategories along with all the topics one by one (well using the bulk selector, but that has limitations too). So this is why we have a rake task for this because it will allow you to easily delete a category along with all subcategories and a topics with one simple command.

I think it is important to test your code, so when creating these destroy rake tasks I made sure to leave out most of logic in the actual rake task and create a DestroyTask class instead that I could easily test.

In order to keep puts statements out of our rspec tests I originally created a log array that I would just append output to, and have any methods just return that log array for the rake task to then output. This left my tests nice and tidy, but it does have some downsides like not being able to immediately see output in the console and if it errored out for any reason I might lose all of my output.

I’m sure there are many ways to do this, but this article about IO in Ruby proved to be the most helpful out of anything else I found online.

Here is a rake task that doesn’t have much logic in it other than initializing a new object and calling a method on it in a loop. Oh and it allows for specifying an arbitrary amount of arguments.

desc "Destroy a comma separated list of category ids."
task "destroy:categories" => :environment do |t, args|
  destroy_task = DestroyTask.new
  categories = args.extras
  puts "Going to delete these categories: #{categories}"
  categories.each do |id|
    destroy_task.destroy_category(id, true)
  end
end

So in order to allow puts output in my method calls, but keeping it from occurring in my test runs I first had to initialize a new @io instance variable on my DestroyTask like so:

class DestroyTask

  def initialize(io=$stdout)
    @io = io
  end
  ...
end

Then inside of any of my methods instead of using puts directly I then would use @io.puts <message>:

@io.puts "There are #{topics.count} topics to delete in #{c.slug} category"

And then in my specs I can initialize my DestroyTask class by passing in a new StringIO object:

DestroyTask.new(StringIO.new)

Here is an example spec so that you can see it in action:

it 'destroys specified category' do
  destroy_task = DestroyTask.new(StringIO.new)

  expect { destroy_task.destroy_category(c.id) }
    .to change { Category.where(id: c.id).count }.by (-1)
  end
end

I’m still not totally sure why the puts method on StringIO returns nil, but you can test it out using the irb console to see how it compares to just puts:

irb(main):008:0> a = StringIO.new
=> #<StringIO:0x000055a3d4debb60>
irb(main):010:0> a.puts "asdf"
=> nil
irb(main):011:0> puts "asdf"
asdf
=> nil
irb(main):012:0> 

As you can see from the above output puts will display output, but the StringIO puts won’t have any output. This means that when the tests encounter an @io.puts statement there won’t by any output to the console, but when these commands are executed via a rake task it will output to the console. Pretty cool!