Civilization advances by extending the number of important operations which we can perform without thinking of them.
—Alfred North Whitehead
This excerpt is from Advanced Rails . This is the book for experienced Rails developers who want to go to the next level with this web development framework, with an in-depth look at techniques for dealing with databases, security, performance, web services and much more. Chapters in this book help you understand not only the tricks and techniques used within the Rails framework itself, but also how make use of ideas borrowed from other programming paradigms.
Ruby on Rails is very powerful, but it cannot do everything. There are many features that are too experimental, out of scope of the Rails core, or even blatantly contrary to the way Rails was designed (it is opinionated software, after all). The core team cannot and would not include everything that anybody wants in Rails.
Luckily, Rails comes with a very flexible extension system. Rails plugins allow developers to extend or override nearly any part of the Rails framework, and share these modifications with others in an encapsulated and reusable manner.
By default, plugins are loaded from directories under
vendor/plugins in the Rails application root.
Should you need to change or add to these paths, the plugin_paths configuration item contains the
plugin load paths:
config.plugin_paths += [File.join(RAILS_ROOT, 'vendor', 'other_plugins')]
By default, plugins are loaded in alphabetical order; attachment_fu is loaded before http_authentication. If the plugins have
dependencies on each other, a manual loading order can be specified
with the plugins configuration
element:
config.plugins = %w(prerequisite_plugin actual_plugin)
Any plugins not specified in config.plugins will not be loaded. However,
if the last plugin specified is the symbol :all, Rails will load all remaining plugins
at that point. Rails accepts either symbols or strings as plugin names
here.
config.plugins = [ :prerequisite_plugin, :actual_plugin, :all ]
The plugin locator searches for plugins under the configured paths, recursively. Because a recursive search is performed, you can organize plugins into directories; for example, vendor/plugins/active_record_acts and vendor/plugins/view_extensions.
The actual plugin locating and loading system is extensible, and
you can write your own strategies. The locator (which by default is
Rails::Plugin::FileSystemLocator)
searches for plugins; the loader (by default Rails::Plugin::Loader) determines whether a
directory contains a plugin and does the work of loading it.
To write your own locators and loaders, examine railties/lib/rails/plugin/locator.rb and railties/lib/rails/plugin/loader.rb. The locators (more than one locator can be used) and loader can be changed with configuration directives:
config.plugin_locators += [MyPluginLocator] config.plugin_loader = MyPluginLoader
Plugins are most often installed with the built-in Rails plugin
tool, script/plugin. This plugin
tool has several commands:
discover/source/unsource/sourcesThe plugin tool uses an ad-hoc method of finding plugins.
Rather than requiring you to specify the URL of a plugin
repository, script/plugin
tries to find it for you. One way it does this is by scraping
the "Plugins" page of the Rails wiki [24] for source URLs. This can be triggered with the
discover command.
The source and unsource commands add and remove
source URLs, respectively. The sources command lists all current
source URLs.
install/update/removeThese commands install, update, and uninstall plugins.
They can take an HTTP URL, a Subversion URL (svn:// or svn+ssh://), or a bare plugin name, in
which case the list of sources is scanned.
script/plugin install takes
an option, -x, that directs it to manage
plugins as Subversion externals. This has the advantage that the
directory is still linked to the external repository. However, it is a
bit inflexible—you cannot cherry-pick changes from the upstream
repository. We will examine some better options later.
RaPT (http://rapt.rubyforge.org/) is a
replacement for the standard Rails plugin installer, script/plugin. It can be installed with
gem install rapt.
The first advantage that RaPT has is that it can search for plugins from the command line. (The second advantage is that it is extremely fast, because it caches everything.)
The rapt searchcommand
looks for plugins matching a specified keyword. To search for
plugins that add calendar features to Rails, change to the root
directory of a Rails application and execute:
$ rapt search calendar Calendar Helper Info: http://agilewebdevelopment.com/plugins/show/98 Install: http://topfunky.net/svn/plugins/calendar_helper Calendariffic 0.1.0 Info: http://agilewebdevelopment.com/plugins/show/743 Install: http://opensvn.csie.org/calendariffic/calendariffic/ Google Calendar Generator Info: http://agilewebdevelopment.com/plugins/show/277 Install: svn://rubyforge.org//var/svn/googlecalendar/plugins/googlecalendar dhtml_calendar Info: http://agilewebdevelopment.com/plugins/show/333 Install: svn://rubyforge.org//var/svn/dhtmlcalendar/dhtml_calendar Bundled Resource Info: http://agilewebdevelopment.com/plugins/show/166 Install: svn://syncid.textdriven.com/svn/opensource/bundled_resource/trunk DatebocksEngine Info: http://agilewebdevelopment.com/plugins/show/356 Install: http://svn.toolbocks.com/plugins/datebocks_engine/ datepicker_engine Info: http://agilewebdevelopment.com/plugins/show/409 Install: http://svn.mamatux.dk/rails-engines/datepicker_engine
One of these could then be installed with, for example,
rapt install
datepicker_engine.
In Rails, plugins are perhaps the most common use of code supplied by an external vendor (other than the Rails framework itself). This requires some special care where version control is concerned. Managing Rails plugins as Subversion externals has several disadvantages:
The remote server must be contacted on each update to determine whether any-thing has changed. This can incur quite a performance penalty if the project has many externals. In addition, it adds an unneeded dependency; problems can ensue if the remote server is down.
The project is generally at the mercy of code changes that happen at the remote branch; there is no easy way to cherry-pick or block changes that happen remotely. The only flexibility Subversion affords is the ability to lock to a certain remote revision.
Similarly, there is no way to maintain local modifications to a remote branch. Any needed modifications can only be kept in the working copy, where they are unversioned.
No history is kept of how external versions relate to the local repository. If you want to update your working copy to last month's version, nobody knows what version the external code was at.
To solve these problems, François Beausoleil created Piston, [25] a program to manage vendor branches in Subversion. Piston imports the remote branch into the local repository, only synchronizing when requested. As a full copy of the code exists inside the project's repository, it can be modified as needed. Any changes made to the local copy will be merged when the project is updated from the remote server.
Piston is available as a gem; install it with sudo gem install --include-dependencies
piston.
To install a plugin using Piston, you need to manually find the Subversion URL of the repository. Then, simply import it with Piston, specifying the repository URL and the destination path in your working copy:
$ piston import http://svn.rubyonrails.org/rails/plugins/deadlock_retry \ vendor/plugins/deadlock_retry Exported r7144 from 'http://svn.rubyonrails.org/rails/plugins/deadlock_retry/' to 'vendor/plugins/deadlock_retry' $ svn ci
The svn ci is necessary
because Piston adds the code to your working copy. To Subversion, it
is as if you wrote the code yourself—it is versioned alongside the
rest of your application. This makes it very simple to patch the
vendor branch for local use; simply make modifications to the
working copy and check them in.
When the time comes to update the vendor branch, piston update vendor/plugins/
deadlock_retry will fetch all changes from the remote
repository and merge them in. Any local modifications will be
preserved in the merge. piston
update can be called without an argument; in that case, it
will recursively update any Piston-controlled directories under the
current one.
Piston-controlled directories can be locked to their current
version with piston lock and
unlocked with piston unlock. And
for current svn:externals users,
existing directories managed with svn:externals can be converted to Piston
all at once with piston
convert.
Piston is also good for managing edge Rails, along with any patches you may apply. To import Rails from the edge, with all of the features of Piston:
$ piston import http://svn.rubyonrails.org/rails/trunk vendor/rails
Piston effectively creates one layer between a remote repository and the working copy. Decentralized version control systems take this model to its logical conclusion: every working copy is a repository, equally able to share changes with other repositories. This can be a much more flexible model than normal centralized version control systems. We examine decentralized version control systems in more detail in Chapter 10, Large Projects.
Plugins and other vendor code can be managed very well with a decentralized version control system. These systems afford much more flexibility, especially in complicated situations with multiple developers and vendors.
A tool is available, hgsvn,[26] which will migrate changes from a SVN repository to a
Mercurial repository. This can be used to set up a system similar to
Piston, but with much more flexibility. One repository (the
"upstream" or "incoming") can mirror the remote repository, and
other projects can cherry-pick desired patches from the upstream and
ignore undesired ones. Local modifications suitable for the upstream
can be exported to patches and sent to the project
maintainer.
Once you know how to extend Rails by opening classes, it is easy to write a plugin. First, let's look at the directory structure of a typical plugin (see Figure 3.1, “Directory structure of a typical plugin”).
There are several files and directories involved in a Rails plugin:
This is the newest feature of Rails plugins—embedded metadata. Right now, this feature
works only with RaPT. The command rapt
about plugin_name will give a
summary of the plugin's information. In the future, more features
are expected; right now, it exists for informational purposes.
Metadata is stored in the about.yml file;
here is an example from acts_as_attachment:
author: technoweenie summary: File upload handling plugin. homepage: http://technoweenie.stikipad.com plugin: http://svn.techno-weenie.net/projects/plugins/acts_as_attachment license: MIT version: 0.3a rails_version: 1.1.2+
This is a Ruby file run upon initialization of the plugin.
Typically, it will require
files from the lib/ directory. As many
plugins patch core functionality,
init.rb may extend core classes with
extensions from the plugin:
require 'my_plugin' ActionController::Base.send :include, MyPlugin::ControllerExtensions
The send hack is needed
here because Module#include is
a private method and, at least for now, send bypasses access control on the
receiver. [27]
This hook is run when the plugin is installed with one of
the automated plugin installation tools such as script/plugin or RaPT. It is a good idea
not to do any-thing mission-critical in this file, as it will not
be run if the plugin is installed manually (by checking out the
source to a directory under
vendor/plugins).
A typical use for the install.rb hook is to display the contents of the plugin's README file:
puts IO.read(File.join(File.dirname(__FILE__), 'README'))
This is the directory in which all of the plugin code is
contained. Rails adds this directory to the Ruby load path as well
as the Rails Dependencies load
path.
For example, assume you have a class, MyPlugin, in
lib/my_plugin.rb. Since it is in the Ruby
load path, a simple require
'my_plugin' will find it. But since Dependencies auto loads missing
constants, you could also load the file simply by referring to
MyPlugin in your plugin.
All plugins, no matter how small, should include a license. Failure to include a license can prevent people from using your software—no matter how insignificant the plugin may be, it is against the law for someone else to distribute your code without your permission.
For most projects, the MIT license (under which Rails itself is released) is sufficient. Under that license, anyone can redistribute your software freely, provided that they include a copy of the license (preserving your copyright notice). Including the MIT-LICENSE file in the plugin is important in this case, as it makes compliance automatic.
This is the core Rake task definition file for the plugin. Usually it is used to launch tests for the plugin itself or package the plugin for distribution.
It is helpful to provide a short explanation here of the plugin's purpose, usage, and any special instructions. A hook can be included in install.rb (described earlier) to print this file upon plugin installation.
This folder contains the plugin's tests. These tests are run using Rake, without loading Rails. Any tests written in this folder must stand alone—they must either mock out any required Rails functionality or actually load the Rails framework. We will explore both of these options later.
This is the uninstall hook, run when a plugin is removed by
tools such as script/plugin or
RaPT. Unless you have a very pressing need, the use of this file
is discouraged. Like install.rb, uninstall.rb
is not always used—many people simply delete the plugin directory
without thought.
Of course, you should feel free to add any folders required by
your plugin. Use File.dirname(__FILE__) in
init.rb to refer to your plugin's directory as
installed. None of these files are specifically required; for example, a
simple plugin may do all of its work in
init.rb.
You can generate a plugin skeleton with a built-in Rails generator:
$ script/generate plugin my_plugin
This generates a skeleton in vendor/plugins/my_plugin with sample files, a fill-in-the-blanks MIT license, and instructions.
To illustrate the flexibility and design of a typical Rails plugin, we will examine some of the plugins available from the rubyonrails.org Subversion repository. Most of these plugins are used fairly commonly; many of them are used in 37signals applications. Consider them the "standard library" of Rails. They are all available from http://svn.rubyonrails.org/rails/plugins
Plugins can be very simple in structure. For example, consider
David Heinemeier Hansson's account_location plugin. This plugin
provides controller and helper methods to support using part of the
domain name as an account name (for example, to support customers with
domain names of customer1.example.com and customer2.example.com, using customer1 and customer2 as keys to look up the account
information). To use the plugin, include AccountLocation in one or more of your
controllers, which adds the appropriate instance methods:
class ApplicationController < ActionController::Base include AccountLocation end puts ApplicationController.instance_methods.grep /^account/ => ["account_domain", "account_subdomain", "account_host", "account_url"]
Including the AccountLocation
module in the controller allows you to access various URL options from
the controller and the view. For example, to set the @account variable from the subdomain on each
request:
class ApplicationController < ActionController::Base include AccountLocation before_filter :find_account protected def find_account @account = Account.find_by_username(account_subdomain) end end
The account_location plugin
has no init.rb; nothing needs to be set up on
load, as all functionality is encapsulated in the AccountLocation module. Here is the
implementation, in lib/account_location.rb (minus
some license text):
module AccountLocation
def self.included(controller)
controller.helper_method(:account_domain, :account_subdomain,
:account_host, :account_url)
end
protected
def default_account_subdomain
@account.username if @account && @account.respond_to?(:username)
end
def account_url(account_subdomain = default_account_subdomain,
use_ssl = request.ssl?)
(use_ssl ? "https://" : "http://") + account_host(account_subdomain)
end
def account_host(account_subdomain = default_account_subdomain)
account_host = ""
account_host << account_subdomain + "."
account_host << account_domain
end
def account_domain
account_domain = ""
account_domain << request.subdomains[1..-1].join(".") +
"." if request.subdomains.size > 1
account_domain << request.domain + request.port_string
end
def account_subdomain
request.subdomains.first
end
endThe self.included method is a
standard idiom for plugins; it is triggered after the module is
included in a class. In this case, that method marks the included
instance methods as Rails helper methods, so they can be used from a
view.
Finally, remember that Dependencies.load_paths contains the
lib directories of all loaded plugins, so the act of mentioning AccountLocation searches for
account_ location.rb among those lib directories. Because of this, you do not
need to require anything in order
to use the plugin—just drop the code into
vendor/plugins.
The ssl_requirement plugin
allows you to specify certain actions that must be protected by SSL.
This plugin conceptually divides actions into three
categories:
All requests to this action must be protected by SSL. If
this action is requested without SSL, it will be redirected to
use SSL. Actions of this type are specified with the ssl_required class method.
SSL is allowed on this action but not required. Actions of
this type are specified by the ssl_allowed class method.
SSL is not allowed for this action. If an action is not
marked with ssl_required or
ssl_allowed, SSL requests to
that action will be redirected to the non-SSL URL.
In typical Rails fashion, the methods that specify SSL requirements are a declarative language. They specify what the requirements are, not how to enforce them. This means that the code reads very cleanly:
class OrderController < ApplicationController ssl_required :checkout, :payment ssl_allowed :cart end
Like the account_location
plugin, the ssl_requirement plugin
is enabled by including a module. The SslRequirement module contains the entire
SSL requirement logic:
module SslRequirement def self.included(controller) controller.extend(ClassMethods) controller.before_filter(:ensure_proper_protocol) end module ClassMethods def ssl_required(*actions) write_inheritable_array(:ssl_required_actions, actions) end def ssl_allowed(*actions) write_inheritable_array(:ssl_allowed_actions, actions) end end protected def ssl_required? (self.class.read_inheritable_attribute(:ssl_required_actions) || []). include?(action_name.to_sym) end def ssl_allowed? (self.class.read_inheritable_attribute(:ssl_allowed_actions) || []). include?(action_name.to_sym) end private def ensure_proper_protocol return true if ssl_allowed? if ssl_required? && !request.ssl? redirect_to "https://" + request.host + request.request_uri return false elsif request.ssl? && !ssl_required? redirect_to "http://" + request.host + request.request_uri return false end end end
Again, the SslRequirement.included method is triggered
when SslRequirement is included in
a controller class. The included
method does two things here. First, it extends the controller with the
SslRequirement::ClassMethods
module, to include the ssl_required
and ssl_allowed class methods. This
is a common Ruby idiom for adding class methods, and it is required
because module methods of an included module do not become class methods of the
including class. (In other words, ssl_required and ssl_allowed could not be module methods of
SslRequirement, because they would
not be added as class methods of the controller class.)
The second thing that SslRequirement.included does is to set up a
before_filter on the controller to
enforce the SSL requirement. This filter redirects to the proper
http:// or https:// URL, depending
on the logic declared by the class methods.
The final plugin we will examine is the http_authentication plugin, which allows you
to protect certain actions in an application by HTTP Basic
authentication (currently, Digest
authentication is stubbed out but not implemented).
The HTTP Authentication plugin is very straightforward; the
most common interface is the ActionController class method authenticate_or_request_with_http_basic,
typically used in a before_filter
on protected actions. That method takes as parameters an
authentication realm and a login procedure block that verifies the
given credentials. If the login procedure returns true, the action is allowed to continue. If
the login procedure returns false,
the action is blocked and an HTTP
401 Unauthorized status code is sent, with instructions on
how to authenticate (a WWW-Authenticate header). In that case, the
browser will typically present the user with a login and password and
allow three tries before displaying an "Unauthorized" page.
The following is a typical use of the HTTP Authentication plugin:
class PrivateController < ApplicationController before_filter :authenticate def secret render :text => "Password correct!" end protected def authenticate authenticate_or_request_with_http_basic do |username, password| username == "bob" && password == "secret" end end end
Notice that, unlike the two plugins described earlier, here we
did not have to include anything in the PrivateController—the authenticate_or_request_with_http_basic
method was already provided for us. This is because the plugin added
some methods to ActionController::Base (of which ApplicationController is a subclass).
One way to include methods like this is direct monkeypatching.
The plugin could have directly written the methods into ActionController::Base:
class ActionController::Base def authenticate_or_request_with_http_basic(realm = "Application", &login_procedure) authenticate_with_http_basic(&login_procedure) || request_http_basic_authentication(realm) end def authenticate_with_http_basic(&login_procedure) HttpAuthentication::Basic.authenticate(self, &login_procedure) end def request_http_basic_authentication(realm = "Application") HttpAuthentication::Basic.authentication_request(self, realm) end end
This works for small plugins, but it can get clunky. The better solution, chosen by this plugin, is to first create a module named for the plugin (sometimes including the developer's name or company to reduce the chance of namespace collisions). Here is the abridged code for the HTTP Authentication plugin's class methods:
module HttpAuthentication module Basic extend self module ControllerMethods def authenticate_or_request_with_http_basic(realm = "Application", &login_procedure) authenticate_with_http_basic(&login_procedure) || request_http_basic_authentication(realm) end def authenticate_with_http_basic(&login_procedure) HttpAuthentication::Basic.authenticate(self, &login_procedure) end def request_http_basic_authentication(realm = "Application") HttpAuthentication::Basic.authentication_request(self, realm) end end end end
Now, the methods are self-contained within HttpAuthentication::Basic::
ControllerMethods. A simple statement in the plugin's
init.rb file adds the methods to ActionController::Base:
ActionController::Base.send :include, HttpAuthentication::Basic::ControllerMethods
Like the rest of Rails, plugins have very mature testing facilities. However, plugin tests usually require a bit more work than standard Rails tests, as the tests are designed to be run on their own, outside of the Rails framework. Some things to keep in mind when writing tests for plugins:
Unlike in the Rails plugin initializer, when running tests,
load paths are not set up automatically, and Dependencies does not load missing
constants for you. You need to manually set up the load paths and
require any parts of the plugin that you will be testing, as in this example from the HTTP
Authentication plugin:
$LOAD_PATH << File.dirname(__FILE__) + '/../lib/' require 'http_authentication'
Similarly, the plugin's init.rb file is
not loaded, so you must set up anything your tests need, such as
including your plugin's modules in the TestCase class:
class HttpBasicAuthenticationTest < Test::Unit::TestCase include HttpAuthentication::Basic # … end
You must usually recreate (mock or stub) any Rails
functionality involved in your test. In the case of the HTTP
Authentication plugin, it would be too much overhead to load the
entire ActionController framework
for the tests. The functionality being tested is very simple, and
requires very little of ActionController:
def test_authentication_request authentication_request(@controller, "Megaglobalapp") assert_equal 'Basic realm="Megaglobalapp"', @controller.headers["WWW-Authenticate"] assert_equal :unauthorized, @controller.renders.first[:status] end
To support this limited subset of ActionController's features, the test's
setup method creates a stub
controller:
def setup
@controller = Class.new do
attr_accessor :headers, :renders
def initialize
@headers, @renders = {}, []
end
def request
Class.new do
def env
{'HTTP_AUTHORIZATION' =>
HttpAuthentication::Basic.encode_credentials("dhh", "secret") }
end
end.new
end
def render(options)
self.renders << options
end
end.new
endThe Class.newdo…end.new
syntax creates an instance of an anonymous class with the provided
class definition. A more verbose, named, equivalent would be:
class MyTestController # class definition… end @controller = MyTestController.new
Sometimes, dependencies are complicated enough so as to
require actually loading a full framework. This is the case with the
SSL Requirement plugin, which actually loads ActionController and sets up a controller
for testing purposes. First, the code loads ActionController (this either requires
RUBYOPT="rubygems" and a suitable
gem version of ActionController,
or setting the ACTIONCONTROLLER_PATH environment variable
to a copy of the ActionController
source):
begin require 'action_controller' rescue LoadError if ENV['ACTIONCONTROLLER_PATH'].nil? abort <<MSG Please set the ACTIONCONTROLLER_PATH environment variable to the directory containing the action_controller.rb file. MSG else $LOAD_PATH.unshift << ENV['ACTIONCONTROLLER_PATH'] begin require 'action_controller' rescue LoadError abort "ActionController could not be found." end end end
Then, the test code loads ActionController's test_process, which
affords access to ActionController::TestRequest and ActionController::TestResponse. After
that, logging is silenced and routes are reloaded:
require 'action_controller/test_process'
require 'test/unit'
require "#{File.dirname(__FILE__)}/../lib/ssl_requirement"
ActionController::Base.logger = nil
ActionController::Routing::Routes.reload rescue nilFinally come the test controller and test case—these follow much the same format as Rails functional tests, as we have done all of the setup manually.
class SslRequirementController < ActionController::Base include SslRequirement ssl_required :a, :b ssl_allowed :c # action definitions… end class SslRequirementTest < Test::Unit::TestCase def setup @controller = SslRequirementController.new @request = ActionController::TestRequest.new @response = ActionController::TestResponse.new end # tests… end
The Deadlock Retry plugin, another standard Rails plugin designed to retry deadlocked database transactions, provides a good example of how to stub out an ActiveRecord model class: [28]
class MockModel def self.transaction(*objects, &block) block.call end def self.logger @logger ||= Logger.new(nil) end include DeadlockRetry end
This allows simple features to be tested without introducing a database dependency:
def test_error_if_limit_exceeded
assert_raise(ActiveRecord::StatementInvalid) do
MockModel.transaction { raise ActiveRecord::StatementInvalid,
DEADLOCK_ERROR }
end
endThe semantics of some plugins makes them difficult to test without relying on a data-base. But while you would like your tests to run everywhere, you cannot depend on a particular DBMS being installed. Additionally, you want to avoid requiring your users to create a test database in order to test a plugin.
Scott Barron has come up with a clever solution, which he uses in his acts_as_state_ machine plugin [29] (a plugin to assign states to ActiveRecord model objects, such as pending, shipped, and refunded orders). The solution is to allow the user to test with any DBMS, and fall back to SQLite (which is widely installed) if none is chosen.
To make this work, a set of test model objects and corresponding fixtures are included in the plugin's test/fixtures directory. The plugin also includes a database schema backing the models (schema.rb) and some statements in test_helper.rb that load the fixtures into the database. The full test directory structure is shown in Figure 3.2, “Plugin testing directory structure”.
The first piece of the puzzle is the database.yml file, which includes not only configuration blocks for standard DBMSs, but also for SQLite and SQLite3, which save their database in a local file:
sqlite: :adapter: sqlite :dbfile: state_machine.sqlite.db sqlite3: :adapter: sqlite3 :dbfile: state_machine.sqlite3.db # (postgresql and mysql elided)
The schema files, fixtures, and models are self-explanatory; they are a Ruby schema file, YAML fixtures, and ActiveRecord model classes, respectively. The real magic happens in test_helper.rb, which ties everything together.
The test helper first sets up Rails load paths and loads ActiveRecord. Then it loads database.yml and instructs ActiveRecord to connect to the database (defaulting to SQLite):
config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml')) ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/debug.log") ActiveRecord::Base.establish_connection(config[ENV['DB'] || 'sqlite'])
Next, the schema file is loaded into the database:
load(File.dirname(__FILE__) + "/schema.rb") if File.exist?(File.dirname(__FILE__) + "/schema.rb")
Finally, the plugin's fixture path is set as TestCase's fixture path and added to the
load path so that models in that directory will be recognized:
Test::Unit::TestCase.fixture_path = File.dirname(__FILE__) + "/fixtures/" $LOAD_PATH.unshift(Test::Unit::TestCase.fixture_path)
Now, the test (acts_as_state_machine_test.rb) can reference ActiveRecord classes and their fixture data just as in a standard Rails unit test.
Geoffrey Grosenbach has a two-part article on Rails plugins, including some information on writing plugins. The two parts are available from the following:
| http://nubyonrails.com/articles/the-complete-guide-to-rails-plugins-part-i |
| http://nubyonrails.com/articles/the-complete-guide-to-rails-plugins-part-ii |
[27] In Ruby 1.9, Object#send will not automatically
ignore access control on the receiving object, although the
new method Object#send!
will.
[28] I would call this a stub, not a mock object, though some do not make the distinction. A stub tends to be "dumb" and has no test-related logic—it only serves to reduce external dependencies. A mock is much smarter and has knowledge of the test environment. It may keep track of its own state or know whether it is "valid" with respect to the test cases that interact with it.
If you enjoyed this excerpt, buy a copy of Advanced Rails .
Copyright © 2009 O'Reilly Media, Inc.