Skip to content

Commit

Permalink
* Actually works in a real world project.
Browse files Browse the repository at this point in the history
 * Updated documentation.
  • Loading branch information
Avdi Grimm committed Feb 18, 2008
1 parent a19a383 commit 2e46b54
Show file tree
Hide file tree
Showing 6 changed files with 151 additions and 27 deletions.
55 changes: 37 additions & 18 deletions README
Expand Up @@ -3,21 +3,40 @@
== What

NullDB is a Rails database connection adapter that interprets common
database operations as no-ops.
database operations as no-ops. It is the Null Object pattern as
applied to database adapters.

== How

In your database.yml:
Once installed, NullDB can be used much like any other ActiveRecord
database adapter:

ActiveRecord::Base.establish_connection :adapter => nulldb

NullDB needs to know where you keep your schema file in order to
reflect table metadata. By default it looks in
RAILS_ROOT/db/schema.rb. You can override that by setting the
+schema+ option:

ActiveRecord::Base.establish_connection :adapter => nulldb,
:schema => foo/myschema.rb

There is a helper method included for configuring RSpec sessions to
use NullDB. Just put the following in your spec/spec_helper.rb:

Spec::Runner.configure do |config|
::NullDB.insinuate_into_spec(config)
end

You can also experiment with putting NullDB in your database.yml:

unit_test:
adapter: nulldb

That's all there is to it. Now any code run in the +unit_test+
environment, *including* schema definitions, will execute using the
NullDB database connector. Most simple database operations will be
safe no-ops. If a schema definition is executed - which is normally
done automatically before unit tests and specs in Rails - NullDB will
use the schema information to reflect table and column metadata.
However, due to the way Rails hard-codes specific database adapters
into its standard Rake tasks, you may find that this generates
unexpected and difficult-to-debug behavior. Workarounds for this are
under development.

== Why

Expand All @@ -31,19 +50,19 @@ UnitRecord[http://unit-test-ar.rubyforge.org/] libraries. It differs
from them in a couple of ways:

1. It works. At the time of writing both ARBS and UnitRecord were
broken out of the box when used with Rails 2.0.
not working for me out of the box with Rails 2.0.

2. It avoids monkey-patching. Rather than re-wiring the secret inner
workings of ActiveRecord (and thus being tightly coupled to said
inner workings), NullDB implements the same well-documented public
interface that the other standard database adapters, like MySQL
and SQLServer, implement.
2. It avoids monkey-patching as much as possible. Rather than
re-wiring the secret inner workings of ActiveRecord (and thus being
tightly coupled to those inner workings), NullDB implements the
same [semi-]well-documented public interface that the other standard
database adapters, like MySQL and SQLServer, implement.

3. UnitRecord takes the approach of eliminating database interaction
in tests by turning almost every database interaction into an
exception. NullDB recognizes that today's ActiveRecord objects
can't take two steps without consulting the database, and instead
turns database interactions into safe no-ops.
exception. NullDB recognizes that ActiveRecord objects typically
can't take two steps without consulting the database, so instead it
turns database interactions into no-ops.

One concrete advantage of this null-object pattern design is that it
is possible with NullDB to test +after_save+ hooks. With NullDB, you
Expand All @@ -60,7 +79,7 @@ nothing will be saved.

== Who

NullDB was written by Avdi Grimm <avdi@avdi.org>
NullDB was written by Avdi Grimm <mailto:avdi@avdi.org>

== License

Expand Down
2 changes: 1 addition & 1 deletion Rakefile
Expand Up @@ -9,5 +9,5 @@ end

Rake::RDocTask.new do |rd|
rd.main = "README"
rd.rdoc_files.include("README", "lib/**/*.rb")
rd.rdoc_files.include("README", "LICENSE", "lib/**/*.rb")
end
4 changes: 3 additions & 1 deletion init.rb
@@ -1 +1,3 @@
# Nothing do to here...
require 'active_record/connection_adapters/nulldb_adapter'

::NullDB = ActiveRecord::ConnectionAdapters::NullDB
47 changes: 40 additions & 7 deletions lib/active_record/connection_adapters/nulldb_adapter.rb
@@ -1,25 +1,45 @@
require 'logger'
require 'stringio'
require 'singleton'
require 'active_record/connection_adapters/abstract_adapter'

class ActiveRecord::Base
# Instantiate a new NullDB connection. Used by ActiveRecord internally.
def self.nulldb_connection(config)
ActiveRecord::ConnectionAdapters::NullDB.new
ActiveRecord::ConnectionAdapters::NullDB.new(config)
end
end

class ActiveRecord::ConnectionAdapters::NullDB <
ActiveRecord::ConnectionAdapters::AbstractAdapter

TableDefinition = ActiveRecord::ConnectionAdapters::TableDefinition

# A convenience method for integratinginto RSpec. See README for example of
# use.
def self.insinuate_into_spec(config)
config.before :all do
ActiveRecord::Base.establish_connection(:adapter => :nulldb)
end

config.after :all do
ActiveRecord::Base.establish_connection(:test)
end
end

def self.execution_log
(@@execution_log ||= [])
end

def initialize
# Recognized options:
#
# [+:schema+] path to the schema file, relative to RAILS_ROOT
def initialize(config={})
@log = StringIO.new
@logger = Logger.new(@log)
@tables = {}
@last_unique_id = 0
@tables = {'schema_info' => TableDefinition.new(nil)}
@schema_path = config.fetch(:schema){ "db/schema.rb" }
super(nil, @logger)
end

Expand All @@ -42,12 +62,21 @@ def create_table(table_name, options = {})
@tables[table_name] = table_definition
end

def tables
@tables.keys.map(&:to_s)
end

def columns(table_name, name = nil)
@tables[table_name].columns.map do |col_def|
if @tables.size <= 1
ActiveRecord::Migration.verbose = false
Kernel.load(File.join(RAILS_ROOT, @schema_path))
end
table = @tables[table_name]
table.columns.map do |col_def|
ActiveRecord::ConnectionAdapters::Column.new(col_def.name.to_s,
col_def.default,
col_def.type,
col_def.null)
col_def.default,
col_def.type,
col_def.null)
end
end

Expand All @@ -60,6 +89,10 @@ def insert(statement, name, primary_key, object_id, *args)
object_id || next_unique_id
end

def select(statement, name)
[]
end

private

def next_unique_id
Expand Down
37 changes: 37 additions & 0 deletions spec/nulldb_spec.rb
Expand Up @@ -9,6 +9,35 @@ def on_save_finished
end
end

RAILS_ROOT = "RAILS_ROOT"

describe "NullDB with no schema pre-loaded" do
before :each do
Kernel.stub!(:load)
ActiveRecord::Migration.stub!(:verbose=)
end

it "should load RAILS_ROOT/db/schema.rb if no alternate is specified" do
ActiveRecord::Base.establish_connection :adapter => :nulldb
Kernel.should_receive(:load).with("RAILS_ROOT/db/schema.rb")
ActiveRecord::Base.connection.columns('schema_info')
end

it "should load the specified schema relative to RAILS_ROOT" do
Kernel.should_receive(:load).with("RAILS_ROOT/foo/myschema.rb")
ActiveRecord::Base.establish_connection :adapter => :nulldb,
:schema => "foo/myschema.rb"
ActiveRecord::Base.connection.columns('schema_info')
end

it "should suppress migration output" do
ActiveRecord::Migration.should_receive(:verbose=).with(false)
ActiveRecord::Base.establish_connection :adapter => :nulldb,
:schema => "foo/myschema.rb"
ActiveRecord::Base.connection.columns('schema_info')
end
end

describe "NullDB" do
before :all do
ActiveRecord::Base.establish_connection :adapter => :nulldb
Expand Down Expand Up @@ -88,6 +117,14 @@ def on_save_finished
@employee.connection.supports_migrations?.should be_true
end

it "should always have a schema_info table definition" do
@employee.connection.tables.should include("schema_info")
end

it "should return an empty array from #select" do
@employee.connection.select("who cares", "blah").should == []
end

def should_have_column(klass, col_name, col_type)
col = klass.columns_hash[col_name.to_s]
col.should_not be_nil
Expand Down
33 changes: 33 additions & 0 deletions tasks/database.rake
@@ -0,0 +1,33 @@
# Sadly, we have to monkeypatch Rake because all of the Rails database tasks are
# hardcoded for specific adapters, with no extension points (!)
Rake::TaskManager.class_eval do
def remove_task(task_name)
@tasks.delete(task_name.to_s)
end
end

def remove_task(task_name)
Rake.application.remove_task(task_name)
end

def wrap_task(task_name, &wrapper)
wrapped_task = Rake::Task[task_name]
remove_task(Rake::Task.scope_name(Rake.application.current_scope,
task_name))
task(task_name) do
wrapper.call(wrapped_task)
end
end

# For later exploration...
# namespace :db do
# namespace :test do
# wrap_task :purge do |wrapped_task|
# if ActiveRecord::Base.configurations["test"]["adapter"] == "nulldb"
# # NO-OP
# else
# wrapped_task.invoke
# end
# end
# end
# end

0 comments on commit 2e46b54

Please sign in to comment.