Replace your test helpers with reusable API

drbrain | Tue, 10 Jan 2012 18:37:50 GMT

test/test_helper.rb is a great idea Rails brought to the Ruby world as a place for functionality that helps you write better tests. There's now a standard place for you to implement common setup/teardown, shortcuts and custom assertions. However, a test helper is not the best place to store this functionality for a Ruby library.

One of the benefits you get out of writing tests is knowing where your API is clumsy and inadequate. If you have a test helper file full of methods to make your library easy to use in a test why is that not part of your library's API? Wouldn't your users also want a bunch of methods that make your library easier to use in their applications?

For example, in RubyGems we have a test helper that does this: gem_file = Gem::Builder.new(spec).build which is a little silly. Every time you create a Gem::Builder you want to build a gem. You don't create a Gem::Builder object for fun! To help out RubyGems users I added a new method: gem_file = Gem::Builder.build spec which immediately creates and builds them gem which is much nicer for everyone (but really, you should use Gem::PackageTask when building gems).

Whether you're writing a library or a Rails app, this kind of functionality belongs in your library (or application) code, not in the test helper where only your tests can benefit from it.

Even after you improve your API by moving helpful functionality back into your library there's still going to be some things that only make sense for tests. For example, you probably don't want to type t = Some::Deeply::Namespaced::Thing.new 1, 2, 3 many, many times in your tests, so you write a short wrapper method you can call like this: t = thing 1, 2, 3. Your tests may need setup and teardown to maintain a clean environment between tests, custom assertions for readability or you may want to include a pre-built stub or mock.

While this having this functionality in a test helper is fine for a Rails app, it shouldn't go in a library's test helper. When you keep testing functionality hidden in the test directory a user who wants to write a third-party extension for your gem can't access them. Why force a happy user to re-implement (possibly poorly or incorrectly) the work you've done to have nice, clean tests that are easy to read and write?

Instead of having a private test helper I have a public test case like MyGem::TestCase that lives in lib/my_gem/test_case.rb. This gives anyone who wants to extend my libraries a documented, ready-to-go API for writing tests for their extension.

My gem-specific test case typically contains all the requires needed to load the library (ideally require 'my_gem'), proper setup and teardown to sandbox the tests, any utility methods that don't belong in the library itself and possibly some custom assertions. This makes a brand new test easy to start:

require 'my_gem/test_case'

class TestMyGemSomeClass < MyGem::TestCase
  def setup
    super

    # …
  end

  def test_something
    # …
  end
end

There is the minor downside that an extension writer must use minitest (my preferred testing library) to test their extension. Perhaps this inconvenience could be solved by a module providing setup, teardown and shortcuts that is included in the proper place for the extension writer's favorite testing library.

PS: Actually, Gem::Builder.build is Gem::Package.build since Gem::Format, Gem::Builder and Gem::Package are getting merged into one convenient class that deals with reading and writing gem files for RubyGems 2.0. This means there will only be one place to look for the API of messing with packages and it reduces the implementation of Gem::Installer a bit.

Posted in ,  | 1 comment | no trackbacks

How to Sleep in Tests

drbrain | Thu, 06 Jan 2011 21:52:00 GMT

And Use Other Kernel Methods

Ruby provides many methods for you in Kernel including such favorites as #sleep, #fork, #open, #system and #` that can be hard to test reliably and quickly.

When you write code that uses these methods you want your tests to be reliable and fast but not need to set up too much state, like having files to open, before running (most importantly because that's more typing, but also because it's more stuff that may break). You also don't want the tests to be slow so actually sleeping or running a command are undesirable.

Let's start with some sample code. Imagine you've got a severe case of NIH and you're implementing cron (poorly):

def run command, every
  loop do
    status = system command
    raise "#{command} failed" unless status
    sleep every
  end
end

And here's a test:

def test_run
  e = assert_raises RuntimeError do
    @cron.run 'false', 0
  end

  assert_equal 'false failed', e.message
end

How do we test the sleep behavior though? We'd need a command that failed on the second invocation. We'd also want to check the sleep duration somehow which would involve waiting.

There's another way though, since both #system and #sleep are in Kernel we can avoid calling the real implementations through the power of inheritance! If we have our tests inject an implementation of #sleep and #system into the implementation so the real methods never get called:

def test_run
  def @cron.sleep time
    @sleep = time
  end

  def @cron.system command
    @commands ||= []
    @commands << command
    @commands.length <= 1
  end

  e = assert_raises RuntimeError do
     @cron.run 'any old command', 2**30
  end

  assert_equal 'any old command failed', e.message
  assert_equal 2**30, @cron.instance_variable_get(:@sleep)
  assert_equal ['any old command', 'any old command'],
               @cron.instance_variable_get(:@commands)
end

This test never calls Kernel#sleep nor Kernel#system, problem solved! As an additional benefit, this test will work on windows where the first will not because false may not exist.

Of course, you can always use a mocking or stubbing framework, but ruby is powerful enough on its own that you don't really need one.

Posted in ,  | 2 comments

Testing Effectively with Rails

drbrain | Wed, 15 Dec 2010 23:23:42 GMT

I wrote a blog post for AT&T Interactive's Engineering Blog on how to test effectively with rails.

I know you all know how to write tests, so this blog post is not about that. I want to move your testing to the next level. Your current testing practices are OK, but some changes in how you approach testing will allow you to refactor and improve your code even faster and more easily than you currently do.

Check it out!

Posted in , ,  | no comments

How I Use Autotest

drbrain | Mon, 26 Nov 2007 22:33:23 GMT

During the Q&A portion of my talk, I was asked a very important question about autotest, something like “How often do you save when using autotest?” I save all the time when using autotest. There was a followup question, something like “Don’t you get syntax errors?” and I don’t.

Before I wrote autotest I was making fine-grained saves that were syntactically correct. I wrote autotest to automate the running of tests so I wouldn’t have to choose which tests to run. My changes were so small that I spent an annoying fraction of my time editing my command line.

When I’m using autotest, rather than writing all of a method’s tests, I start by performing the setup (which should cause an error), add a flunk, and save, which is usually four lines:

  def test_blah
    result = @thingy.blah
    flunk
  end

When the tests run they’ll see the method blah doesn’t exist and fail, so I define the method, (the def blah and end lines), and save. Then I write an assertion and save the test, which will fail, so I implement what I need to make the assertion pass, and save. Now I repeatedly perform the minimum changes to go from failure to flunking until my test is complete, remove the flunk, and start over with the next method.

Each save I do is a handful of lines, so it’s easy to keep them syntactically correct. I know I’m going from working state to working state as I work towards my end goal so I can easily roll back my changes with undo.

Posted in ,  | 1 comment

Firebrigade Home Page Fixed

drbrain | Tue, 18 Sep 2007 02:40:22 GMT

I denormalized a bit and beat the tests back into shape and the Firebrigade home page is back to life! Next I’m going to sand down a few rough edges on RubyGems to get a beta shipped suitable for feedback.

Firebrigade is now fully vladified using perforce.

Here’s the two setup steps you need for perforce:

  1. Require ‘vlad/perforce’ at the top of config/deploy.rb.
  2. Your .p4config goes in the scm/ directory of the server’s checkout.
  3. Run p4 client in the scm/ directory and set your View to //path/to/project/… //clientname/…

Hopefully we can fully automate this so the vlad:setup_app task can handle this automatically.

I also had to do some custom setup for firebrigade because it uses RubyInline and needs the INLINEDIR set. I just added extra stuff to the setup_app task:

namespace :vlad do remote_task :setup_app do cmds = [ "mkdir #{inline_dir}", "sudo chown www:www #{inline_dir}", ]

run cmds.join(’ && ’) end

end

Update: Automatic p4 setup is done!

Posted in ,  | 1 comment

Notes on Heckle

drbrain | Mon, 12 Feb 2007 22:32:17 GMT

When I started heckle I was hoping for somehing better than rcov that would tell you that your code is poorly tested… But code so poorly engineered that changing anything as bound to make everything fall apart looks like well tested (read: heckle-proof) code in a sense. We both think that running just the unit tests that directly test the MUT (method under test) would help, but really it wouldn’t help enough. […]

Suggestions? How would you differentiate between “well tested” and “tightly coupled crap”?

Notes on Heckle via Polishing Ruby

I’m puzzled by this too. How can we detect well-tested vs tightly-coupled (and poorly tested) code automatically?

Posted in  | no comments

tinderbox version 1.0.0 has been released!

drbrain | Wed, 31 Jan 2007 09:24:00 GMT

tinderbox version 1.0.0 has been released!

http://seattlerb.rubyforge.org/tinderbox

Description

Tinderbox tests projects and tries to make them break by running them on as
many different platforms as possible.

Features & Problems

Changes:

1.0.0 / 2007-01-30

  • Tests gems in a sandbox
  • Submits results to Firebrigade
  • Birthday!

http://seattlerb.rubyforge.org/tinderbox

Posted in , , ,  | 4 comments

firebrigade_api version 1.0.0 has been released!

drbrain | Wed, 31 Jan 2007 09:13:02 GMT

http://seattlerb.rubyforge.org/firebrigade_api


firebrigade_api is an API wrapper for http://firebrigade.seattlerb.org

Changes

Posted in , ,  | no comments

Test Profiling by Lines Logged

drbrain | Mon, 22 Jan 2007 23:41:00 GMT

At work I've been cleaning up the tests and trying to make them run faster. One way of doing this is profiling the tests and fixing the slow spots. That only works so well, especially if there's lots of duplication or extra work in the tests. With Rails you can go about this a different way, since you have a second source of information on your tests' operation, the log file.

I added the following code to test/test_helper.rb:

raise 'require\'d test/test_helper twice!, you broke it!' if
  ENV['RAILS_ENV'] == 'test'

At the very top, even above ENV["RALIS_ENV"] = "test" to make sure that the hack to Test::Unit::TestCase below happens only once, then the magic to figure out which test outputs which log lines:

class Test::Unit::TestCase
  alias unlogged_run run
  def run(result, &block)
    RAILS_DEFAULT_LOGGER.debug "RUNNING #{self.class} #{@method_name}"
    unlogged_run result, &block
  end
end if ENV['PROFILE_LOG']

Each test run will be prefixed with text like RUNNING SomeTest test_blah which I can then run this script on:

#!/usr/local/bin/ruby -w

test = nil
tests = Hash.new 0

File.open 'log/test.log' do |fp|
  fp.each_line do |line|
    if line.strip =~ /^RUNNING (.*)/ then
      test = $1
    else
      tests[test] += 1
    end
  end
end

tests.sort_by { |test, count| -count }.each do |test, count|
  puts "%5d %s" % [count, test]
end

To get the profile information, I run:

rake log:clear; PROFILE_LOG=y rake && script/count_logs

When run on Firebrigade, I get the following output (top 10 only):

  130 ProjectViewTest test_show
   74 DummyControllerTest test_error_500
   59 OwnerTest test_class_owner_count
   42 ProjectControllerTest test_index
   39 HomeControllerTest test_index_no_builds
   38 RestControllerTest test_add_build
   35 ProjectControllerTest test_show_no_versions
   35 ProjectControllerTest test_search_many_matches
   34 RestControllerTest test_add_project
   33 RestControllerTest test_add_version

Now I can quickly discover good candidates for refactoring. #test_error_500 above dumps an email into the logs, so it is a bogus result. That leaves ProjectViewTest#test_show as a candidate for simplification or refactoring.

Posted in , ,  | 2 comments

Ruby, Rails, Test::Rails Cheat Sheet

drbrain | Thu, 24 Aug 2006 18:07:39 GMT

A cheat sheet that shows the assertions in Test::Unit, the ones added by Rails, and the ones further added by Test::Rails (part of the ZenTest gem).

Ruby, Rails, Test::Rails Cheat Sheet

Ruby, Rails, Test::Rails Cheat Sheet via Nuby on Rails

Posted in , ,  | no comments