Testing is important, no matter what framework or methodology you use. Once that is internalized, you need to make some practical decisions, such as “What testing framework do I learn?” I learned how to test my Rails apps using Minitest because it’s the Rails default. I continue using it because Minitest syntax is just Ruby instead of a complicated DSL, but that’s another story. If you’ve ever used Rails’ implementation of Minitest, you’ll be familiar with tests looking like this:
test "the truth" do
assert true
end
Since I’ve started writing scripts with Ruby, I’ve used Minitest to provide test coverage. Since Minitest isn’t baked into Ruby like it is in Rails, you have to add install the gem and require
it in your test files. Imagine my surprise when, looking at the documentation for Minitest, I saw syntax that looked like this:
def test_the_truth
assert true
end
While it isn’t incredibly different, it’s different enough that it raised some questions. Why was Rails’ Minitest syntax different from standard Minitest syntax?
Until yesterday, that question remained unanswered. Even the Rails guide didn’t seem to mention anything. Puzzling over this difference, I found a link to the Rails edge guide for testing, which must have recently been updated. Section 2.3 opened my eyes, which reads:
Any method defined within a class inherited from
Minitest::Test
(which is the superclass ofActiveSupport::TestCase
) that begins with test_ (case sensitive) is simply called a test. So, methods defined as test_password and test_valid_password are legal test names and are run automatically when the test case is run.
Rails also adds a test method that takes a test name and a block. It generates a normal
Minitest::Unit
test with method names prefixed with test_.
So, ActiveSupport::Testcase
adds functionality to Minitest
, which includes this new syntax. Determined to figure out what was going on under the hood, I went spelunking in Rails’ source code. Looking through the ActiveSupport
directory, I found the following file:
module ActiveSupport
module Testing
module Declarative
unless defined?(Spec)
# Helper to define a test method using a String. Under the hood, it replaces
# spaces with underscores and defines the test method.
#
# test "verify something" do
# ...
# end
def test(name, &block)
test_name = "test_#{name.gsub(/\s+/,'_')}".to_sym
defined = method_defined? test_name
raise "#{test_name} is already defined in #{self}" if defined
if block_given?
define_method(test_name, &block)
else
define_method(test_name) do
flunk "No implementation provided for #{name}"
end
end
end
end
end
end
end
This one little method in this one little module is responsible for the slight syntax change. It uses Ruby’s awesome metaprogramming abilities to transform the Rails Minitest syntax into the standard Minitest syntax. If you read that method and are scratching your head, let me go through it line by line using the following test as an example:
test "the truth" do
assert true
end
- This test actually uses the
test
method defined inActiveSupport::Testing::Declarative
. Thename
argument is"the truth"
and the block (ie. the code betweendo
andend
) isassert true
. test_name = test_#{name.gsub(/\s+/,'_')}".to_sym
transforms the name argument into the appropriate Minitest syntax. It prepends the name you provide withtest_
, then usesgsub
to replace the spaces with underscores. Finally.to_sym
returns the symbol corresponding to the object (which is important later), resulting in:test_the_truth
.defined = method_defined? test_name
checks to see whether the name of the test you’ve written has already been used elsewhere. If so, an exception is raised.if block_given?
evaluates if you’ve provided a block. In this case, we have. Here’s where the metaprogramming magic really happens –define_method
is called with the test name (as a symbol) and the block.define_method
takes a symbol for the name of the method (hence, why the name you provide is transformed into a symbol) and a block, which then becomes the body of the method. Usingdefine_method
, the above sample method is transformed into the standard Minitest syntax:
def test_the_truth
assert true
end
A couple of lessons learned:
- Metaprogramming is really cool. Ruby made it really easy for the Rails team to slightly simplify the Minitest syntax all through using the standard library. I need to learn more about Metaprogramming.
- When I don’t know how something works, it’s not nearly as hard as I imagined to figure it out. Rails is incredibly well organized, and it only took me a couple of minutes of looking around to find the
Declarative
module. Actually understanding it took a little more research, but it was also very doable. Don’t be scared to dive into the source of something you don’t quite understand!