Just like other code, test suites tend to grow in both size and complexity throughout the lifecycle of a project. The following techniques help keep things tidy and well factored, allowing your tests to continue to serve as a road map to your project.
If you are working on a very small program or library, and you want to be able to run your tests while in development, but then require the code as part of another program later, there is a simple idiom that is useful for embedding your tests:
class Foo ... end if __FILE__ == $PROGRAM_NAME require "test/unit" class TestFoo < Test::Unit::TestCase #... end end
Simply wrapping your tests in this if
statement
will allow running ruby foo.rb
to
execute your tests, while require
"foo"
will still work as expected without running the tests.
This can be useful for sharing small programs with others, or for
writing some tests while developing a small prototype of a larger
application. However, once you start to produce more than a few test
cases, be sure to break things back out into their normal directory
structure. Giant files can be a bit unwieldy to deal with, and it is a
bit awkward (even though it is possible) to treat your
lib/ directory as if it were also your test
suite.
When you begin to chain together a large amount of test cases, you
might find that you are repeating some information across them. Some of
the most common things in this regard are require
statements and basic helper functions.
A good solution to keep things clean is to create a test/test_helpers.rb file and then do all of your global configuration there. In your individual tests, you can require this file by expanding the direct path to it, using the following idiom:
require File.dirname(__FILE__) + '/test_helpers'
This allows your test files to be run individually from any directory, not just the top-level directory. Here is a sample test_helpers.rb from the Prawn project to give you a sense of what kinds of things might go into the file:
require "rubygems" require "test/unit" $LOAD_PATH << File.join(File.dirname(__FILE__), '..', 'lib') require "prawn" gem 'pdf-reader', ">=0.7.3" require "pdf/reader" def create_pdf @pdf = Prawn::Document.new( left_margin: 0, right_margin: 0, top_margin: 0, bottom_margin: 0 ) end def observer(klass) @output = @pdf.render obs = klass.new PDF::Reader.string(@output,obs) obs end def parse_pdf_object(obj) PDF::Reader::Parser.new( PDF::Reader::Buffer.new(StringIO.new(obj)), nil).parse_token end puts "Prawn tests: Running on Ruby Version: #{RUBY_VERSION}"
Here you can see that load path adjustments, project-specific dependencies, and some basic helper functions are being loaded. The helper functions are obviously Prawn-specific, but as you can see, they provide wrappers around common operations that need to be done in a number of our tests, which result in something like this in practice:
class PolygonTest < Test::Unit::TestCase must "draw each line passed to polygon()" do @pdf = Prawn::Document.new @pdf.polygon([100,500],[100,400],[200,400]) line_drawing = observer(LineDrawingObserver) assert_equal [[100,500],[100,400],[200,400],[100,500]], line_drawing.points end end
It’s completely up to you how far you wish to take this sort of thing. As a rule of thumb, if you find yourself using a feature in more than a few places, consider adding it to test_helpers.rb. If you want a little more of a clean approach, you can wrap your helpers in a module, but depending on what you’re doing, just defining them at the top level might be fine as well.
Your helper file essentially allows you to centralize the support features for your test suite. When used effectively, this approach can greatly simplify your tests and reduce duplicated code that can lead to problems.
In addition to building helper functions to support your examples, you can actually build custom assertions to augment the vocabulary of your tests.
Porting an example from RSpec’s documentation, it is easy to see how simple it is to add a custom assertion to your tests. We want to transform a basic statement that looks like this:
assert bob.current_zone.eql?(Zone.new("4"))
into something a bit more friendly, such as:
assert_in_zone("4", bob)
To do this in Test::Unit
, we’ll
make use of the low-level function assert_block()
. Here’s how you would define
assert_in_zone
and its complement,
assert_not_in_zone
:
def assert_in_zone(expected, person) assert_block("Expected #{person.inspect} to be in Zone #{expected}") do person.current_zone.eql?(Zone.new(expected)) end end def assert_not_in_zone(expected_zone, person) assert_block("Expected #{person.inspect} not to be in Zone #{expected}") do !person.current_zone.eql?(Zone.new(expected)) end end
With these definitions in place, you can use the assertions as we
specified earlier. When the statement is true, the assertion will pass;
when it is false, the assertion will fail and display the custom
message. All of the assertions in Test::Unit
can be built upon assert_block
, which indicates how powerful it
can be for creating your own higher-level assertions.
We’re winding to a close with the discussion of testing practices, but here’s the recap of things you can do to keep your testing code neat and well formed:
If you’re working with a tiny program, don’t bother with the formal directory structure—just use the simple idiom that allows your script to be both loaded as a library and run as an executable.
If your application is bigger, eliminate duplication by centralizing your boilerplate and support code in a test/test_helpers.rb file that is required by all of your tests.
If your code seems to be doing a lot of complicated stuff and
Test::Unit
’s built-in assertions aren’t doing the trick, build your own via the simpleassert_block
function.
Get Ruby Best Practices now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.