activerecord-nulldb-adapter 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +21 -0
- data/README +143 -0
- data/Rakefile +53 -0
- data/VERSION +1 -0
- data/activerecord-nulldb-adapter.gemspec +51 -0
- data/init.rb +1 -0
- data/lib/active_record/connection_adapters/nulldb_adapter.rb +231 -0
- data/lib/nulldb_rspec.rb +93 -0
- data/spec/nulldb_spec.rb +189 -0
- data/tasks/database.rake +33 -0
- metadata +65 -0
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2008 Avdi Grimm
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README
ADDED
@@ -0,0 +1,143 @@
|
|
1
|
+
= The NullDB Connection Adapter Plugin
|
2
|
+
|
3
|
+
== What
|
4
|
+
|
5
|
+
NullDB is the Null Object pattern as applied to ActiveRecord database
|
6
|
+
adapters. It is a database backend that translates database
|
7
|
+
interactions into no-ops. Using NullDB enables you to test your model
|
8
|
+
business logic - including +after_save+ hooks - without ever touching
|
9
|
+
a real database.
|
10
|
+
|
11
|
+
== How
|
12
|
+
|
13
|
+
Once installed, NullDB can be used much like any other ActiveRecord
|
14
|
+
database adapter:
|
15
|
+
|
16
|
+
ActiveRecord::Base.establish_connection :adapter => :nulldb
|
17
|
+
|
18
|
+
NullDB needs to know where you keep your schema file in order to
|
19
|
+
reflect table metadata. By default it looks in
|
20
|
+
RAILS_ROOT/db/schema.rb. You can override that by setting the
|
21
|
+
+schema+ option:
|
22
|
+
|
23
|
+
ActiveRecord::Base.establish_connection :adapter => :nulldb,
|
24
|
+
:schema => foo/myschema.rb
|
25
|
+
|
26
|
+
NullDB comes with RSpec integration. To replace the database with
|
27
|
+
NullDB in all of your specs, put the following in your
|
28
|
+
spec/spec_helper:
|
29
|
+
|
30
|
+
require 'nulldb_rspec'
|
31
|
+
include NullDB::RSpec::NullifiedDatabase
|
32
|
+
|
33
|
+
Or if you just want to use NullDB in a specific spec context, you can
|
34
|
+
include the same module inside a context:
|
35
|
+
|
36
|
+
require 'nulldb_rspec'
|
37
|
+
|
38
|
+
describe Employee, "with access to the database" do
|
39
|
+
fixtures :employees
|
40
|
+
# ...
|
41
|
+
end
|
42
|
+
|
43
|
+
describe Employee, "with NullDB" do
|
44
|
+
include NullDB::RSpec::NullifiedDatabase
|
45
|
+
# ...
|
46
|
+
end
|
47
|
+
|
48
|
+
NullDB::Rspec provides some custom matcher support for verifying
|
49
|
+
expectations about interactions with the database:
|
50
|
+
|
51
|
+
describe Employee do
|
52
|
+
include NullDB::RSpec::NullifiedDatabase
|
53
|
+
|
54
|
+
it "should cause an insert statement to be executed" do
|
55
|
+
Employee.create!
|
56
|
+
Employee.connection.should have_executed(:insert)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
UnitRecord-style verification that no database calls have been made at
|
61
|
+
all can be achieved by using the special +:anything+ symbol:
|
62
|
+
|
63
|
+
describe "stuff that shouldn't touch the database" do
|
64
|
+
after :each do
|
65
|
+
Employee.connection.should_not have_executed(:anything)
|
66
|
+
end
|
67
|
+
# ...
|
68
|
+
end
|
69
|
+
|
70
|
+
You can also experiment with putting NullDB in your database.yml:
|
71
|
+
|
72
|
+
unit_test:
|
73
|
+
adapter: nulldb
|
74
|
+
|
75
|
+
However, due to the way Rails hard-codes specific database adapters
|
76
|
+
into its standard Rake tasks, you may find that this generates
|
77
|
+
unexpected and difficult-to-debug behavior. Workarounds for this are
|
78
|
+
under development.
|
79
|
+
|
80
|
+
== Why
|
81
|
+
|
82
|
+
There are a number of advantages to writing unit tests that never
|
83
|
+
touch the database. The biggest is probably speed of execution - unit
|
84
|
+
tests must be fast for test-driven development to be practical.
|
85
|
+
Another is separation of concerns: unit tests should be exercising
|
86
|
+
only the business logic contained in your models, not ActiveRecord.
|
87
|
+
For more on why testing-sans-database is a god idea, see:
|
88
|
+
http://www.dcmanges.com/blog/rails-unit-record-test-without-the-database.
|
89
|
+
|
90
|
+
NullDB is one way to separate your unit tests from the database. It
|
91
|
+
was inspired by the ARBS[http://arbs.rubyforge.org/] and
|
92
|
+
UnitRecord[http://unit-test-ar.rubyforge.org/] libraries. It differs
|
93
|
+
from them in a couple of ways:
|
94
|
+
|
95
|
+
1. It works. At the time of writing both ARBS and UnitRecord were
|
96
|
+
not working for me out of the box with Rails 2.0.
|
97
|
+
|
98
|
+
2. It avoids monkey-patching as much as possible. Rather than
|
99
|
+
re-wiring the secret inner workings of ActiveRecord (and thus being
|
100
|
+
tightly coupled to those inner workings), NullDB implements the
|
101
|
+
same [semi-]well-documented public interface that the other standard
|
102
|
+
database adapters, like MySQL and SQLServer, implement.
|
103
|
+
|
104
|
+
3. UnitRecord takes the approach of eliminating database interaction
|
105
|
+
in tests by turning almost every database interaction into an
|
106
|
+
exception. NullDB recognizes that ActiveRecord objects typically
|
107
|
+
can't take two steps without consulting the database, so instead it
|
108
|
+
turns database interactions into no-ops.
|
109
|
+
|
110
|
+
One concrete advantage of this null-object pattern design is that it
|
111
|
+
is possible with NullDB to test +after_save+ hooks. With NullDB, you
|
112
|
+
can call +#save+ and all of the usual callbacks will be called - but
|
113
|
+
nothing will be saved.
|
114
|
+
|
115
|
+
== Limitations
|
116
|
+
|
117
|
+
* It is *not* an in-memory database. Finds will not work. Neither
|
118
|
+
will +reload+, currently. Test fixtures won't work either, for
|
119
|
+
obvious reasons.
|
120
|
+
* It has only the most rudimentery schema/migration support. Complex
|
121
|
+
migrations will probably break it.
|
122
|
+
* Lots of other things probably don't work. Patches welcome!
|
123
|
+
|
124
|
+
== Who
|
125
|
+
|
126
|
+
NullDB was written by Avdi Grimm <mailto:avdi@avdi.org>
|
127
|
+
|
128
|
+
== Where
|
129
|
+
|
130
|
+
* Homepage: http://nulldb.rubyforge.org
|
131
|
+
* Project Info: http://rubyforge.org/projects/nulldb/
|
132
|
+
* SCM: http://rubyforge.org/scm/?group_id=7512
|
133
|
+
|
134
|
+
== Changes
|
135
|
+
|
136
|
+
* Version 0.0.1 (2007-02-18)
|
137
|
+
- Initial Release
|
138
|
+
* Version 0.0.2 (2007-05-31)
|
139
|
+
- Moved to Rubyforge
|
140
|
+
|
141
|
+
== License
|
142
|
+
|
143
|
+
See the LICENSE file for licensing information.
|
data/Rakefile
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/rdoctask'
|
3
|
+
require 'spec/rake/spectask'
|
4
|
+
|
5
|
+
# active_record connection_adapters abstract connection_specification
|
6
|
+
GEM_NAME = 'activerecord-nulldb-adapter'
|
7
|
+
|
8
|
+
desc "Run all examples"
|
9
|
+
Spec::Rake::SpecTask.new('spec') do |t|
|
10
|
+
t.spec_files = FileList['spec/**/*_spec.rb']
|
11
|
+
end
|
12
|
+
|
13
|
+
Rake::RDocTask.new do |rd|
|
14
|
+
rd.main = "README"
|
15
|
+
rd.rdoc_files.include("README", "LICENSE", "lib/**/*.rb")
|
16
|
+
end
|
17
|
+
|
18
|
+
desc "Publish project home page"
|
19
|
+
task :publish => ["rdoc"] do
|
20
|
+
sh "scp -r html/* avdi@rubyforge.org:/var/www/gforge-projects/nulldb"
|
21
|
+
end
|
22
|
+
|
23
|
+
desc "Tag release"
|
24
|
+
task :tag do
|
25
|
+
warn "This needs to be updated for git"
|
26
|
+
exit 1
|
27
|
+
repos = "http://svn.avdi.org/nulldb"
|
28
|
+
version = ENV["VERSION"]
|
29
|
+
raise "No version specified" unless version
|
30
|
+
sh "svn cp #{repos}/trunk #{repos}/tags/nulldb-#{version}"
|
31
|
+
end
|
32
|
+
|
33
|
+
desc "Build gem"
|
34
|
+
task :gem do
|
35
|
+
system 'rake gemspec'
|
36
|
+
system "gem build #{GEM_NAME}.gemspec"
|
37
|
+
end
|
38
|
+
|
39
|
+
begin
|
40
|
+
require 'jeweler'
|
41
|
+
Jeweler::Tasks.new do |gem|
|
42
|
+
gem.name = GEM_NAME
|
43
|
+
gem.summary = %Q{NullDB lets you to test your models without ever touching a real database.}
|
44
|
+
gem.email = "avdi@avdi.org"
|
45
|
+
gem.homepage = 'http://nulldb.rubyforge.org'
|
46
|
+
gem.description = "An ActiveRecord null database adapter for greater speed and isolation in unit tests"
|
47
|
+
gem.rubyforge_project = 'nulldb'
|
48
|
+
gem.authors = ['Avdi Grimm']
|
49
|
+
end
|
50
|
+
|
51
|
+
rescue LoadError
|
52
|
+
puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
|
53
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.0.3
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{activerecord-nulldb-adapter}
|
8
|
+
s.version = "0.0.3"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Avdi Grimm"]
|
12
|
+
s.date = %q{2009-12-30}
|
13
|
+
s.description = %q{An ActiveRecord null database adapter for greater speed and isolation in unit tests}
|
14
|
+
s.email = %q{avdi@avdi.org}
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"LICENSE",
|
17
|
+
"README"
|
18
|
+
]
|
19
|
+
s.files = [
|
20
|
+
"LICENSE",
|
21
|
+
"README",
|
22
|
+
"Rakefile",
|
23
|
+
"VERSION",
|
24
|
+
"activerecord-nulldb-adapter.gemspec",
|
25
|
+
"init.rb",
|
26
|
+
"lib/active_record/connection_adapters/nulldb_adapter.rb",
|
27
|
+
"lib/nulldb_rspec.rb",
|
28
|
+
"spec/nulldb_spec.rb",
|
29
|
+
"tasks/database.rake"
|
30
|
+
]
|
31
|
+
s.homepage = %q{http://nulldb.rubyforge.org}
|
32
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
33
|
+
s.require_paths = ["lib"]
|
34
|
+
s.rubyforge_project = %q{nulldb}
|
35
|
+
s.rubygems_version = %q{1.3.5}
|
36
|
+
s.summary = %q{NullDB lets you to test your models without ever touching a real database.}
|
37
|
+
s.test_files = [
|
38
|
+
"spec/nulldb_spec.rb"
|
39
|
+
]
|
40
|
+
|
41
|
+
if s.respond_to? :specification_version then
|
42
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
43
|
+
s.specification_version = 3
|
44
|
+
|
45
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
46
|
+
else
|
47
|
+
end
|
48
|
+
else
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
data/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
#require 'active_record/connection_adapters/nulldb_adapter'
|
@@ -0,0 +1,231 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require 'stringio'
|
3
|
+
require 'singleton'
|
4
|
+
require 'active_record/connection_adapters/abstract_adapter'
|
5
|
+
|
6
|
+
class ActiveRecord::Base
|
7
|
+
# Instantiate a new NullDB connection. Used by ActiveRecord internally.
|
8
|
+
def self.nulldb_connection(config)
|
9
|
+
ActiveRecord::ConnectionAdapters::NullDBAdapter.new(config)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
module ActiveRecord
|
14
|
+
# Just make sure you have the latest version of your schema
|
15
|
+
class Schema < Migration
|
16
|
+
def self.define(info={}, &block)
|
17
|
+
instance_eval(&block)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class ActiveRecord::ConnectionAdapters::NullDBAdapter <
|
23
|
+
ActiveRecord::ConnectionAdapters::AbstractAdapter
|
24
|
+
|
25
|
+
class Statement
|
26
|
+
attr_reader :entry_point, :content
|
27
|
+
|
28
|
+
def initialize(entry_point, content = "")
|
29
|
+
@entry_point, @content = entry_point, content
|
30
|
+
end
|
31
|
+
|
32
|
+
def ==(other)
|
33
|
+
self.entry_point == other.entry_point
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
class Checkpoint < Statement
|
38
|
+
def initialize
|
39
|
+
super(:checkpoint, "")
|
40
|
+
end
|
41
|
+
|
42
|
+
def ==(other)
|
43
|
+
self.class == other.class
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
TableDefinition = ActiveRecord::ConnectionAdapters::TableDefinition
|
48
|
+
|
49
|
+
class NullObject
|
50
|
+
def method_missing(*args, &block)
|
51
|
+
nil
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# A convenience method for integratinginto RSpec. See README for example of
|
56
|
+
# use.
|
57
|
+
def self.insinuate_into_spec(config)
|
58
|
+
config.before :all do
|
59
|
+
ActiveRecord::Base.establish_connection(:adapter => :nulldb)
|
60
|
+
end
|
61
|
+
|
62
|
+
config.after :all do
|
63
|
+
ActiveRecord::Base.establish_connection(:test)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Recognized options:
|
68
|
+
#
|
69
|
+
# [+:schema+] path to the schema file, relative to RAILS_ROOT
|
70
|
+
def initialize(config={})
|
71
|
+
@log = StringIO.new
|
72
|
+
@logger = Logger.new(@log)
|
73
|
+
@last_unique_id = 0
|
74
|
+
@tables = {'schema_info' => TableDefinition.new(nil)}
|
75
|
+
@schema_path = config.fetch(:schema){ "db/schema.rb" }
|
76
|
+
super(nil, @logger)
|
77
|
+
end
|
78
|
+
|
79
|
+
# A log of every statement that has been "executed" by this connection adapter
|
80
|
+
# instance.
|
81
|
+
def execution_log
|
82
|
+
(@execution_log ||= [])
|
83
|
+
end
|
84
|
+
|
85
|
+
# A log of every statement that has been "executed" since the last time
|
86
|
+
# #checkpoint! was called, or since the connection was created.
|
87
|
+
def execution_log_since_checkpoint
|
88
|
+
checkpoint_index = @execution_log.rindex(Checkpoint.new)
|
89
|
+
checkpoint_index = checkpoint_index ? checkpoint_index + 1 : 0
|
90
|
+
@execution_log[(checkpoint_index..-1)]
|
91
|
+
end
|
92
|
+
|
93
|
+
# Inserts a checkpoint in the log. See also #execution_log_since_checkpoint.
|
94
|
+
def checkpoint!
|
95
|
+
self.execution_log << Checkpoint.new
|
96
|
+
end
|
97
|
+
|
98
|
+
def adapter_name
|
99
|
+
"NullDB"
|
100
|
+
end
|
101
|
+
|
102
|
+
def supports_migrations?
|
103
|
+
true
|
104
|
+
end
|
105
|
+
|
106
|
+
def create_table(table_name, options = {})
|
107
|
+
table_definition = ActiveRecord::ConnectionAdapters::TableDefinition.new(self)
|
108
|
+
unless options[:id] == false
|
109
|
+
table_definition.primary_key(options[:primary_key] || "id")
|
110
|
+
end
|
111
|
+
|
112
|
+
yield table_definition
|
113
|
+
|
114
|
+
@tables[table_name] = table_definition
|
115
|
+
end
|
116
|
+
|
117
|
+
def add_fk_constraint(*args)
|
118
|
+
# NOOP
|
119
|
+
end
|
120
|
+
|
121
|
+
def add_pk_constraint(*args)
|
122
|
+
# NOOP
|
123
|
+
end
|
124
|
+
|
125
|
+
# Retrieve the table names defined by the schema
|
126
|
+
def tables
|
127
|
+
@tables.keys.map(&:to_s)
|
128
|
+
end
|
129
|
+
|
130
|
+
# Retrieve table columns as defined by the schema
|
131
|
+
def columns(table_name, name = nil)
|
132
|
+
if @tables.size <= 1
|
133
|
+
ActiveRecord::Migration.verbose = false
|
134
|
+
Kernel.load(File.join(RAILS_ROOT, @schema_path))
|
135
|
+
end
|
136
|
+
table = @tables[table_name]
|
137
|
+
table.columns.map do |col_def|
|
138
|
+
ActiveRecord::ConnectionAdapters::Column.new(col_def.name.to_s,
|
139
|
+
col_def.default,
|
140
|
+
col_def.type,
|
141
|
+
col_def.null)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def execute(statement, name = nil)
|
146
|
+
self.execution_log << Statement.new(entry_point, statement)
|
147
|
+
NullObject.new
|
148
|
+
end
|
149
|
+
|
150
|
+
def select_rows(statement, name = nil)
|
151
|
+
returning([]) do
|
152
|
+
self.execution_log << Statement.new(entry_point, statement)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def insert(statement, name, primary_key, object_id, sequence_name)
|
157
|
+
returning(object_id || next_unique_id) do
|
158
|
+
with_entry_point(:insert) do
|
159
|
+
super(statement, name, primary_key, object_id, sequence_name)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
def update(statement, name=nil)
|
165
|
+
with_entry_point(:update) do
|
166
|
+
super(statement, name)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
def delete(statement, name=nil)
|
171
|
+
with_entry_point(:delete) do
|
172
|
+
super(statement, name)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
def select_all(statement, name=nil)
|
177
|
+
with_entry_point(:select_all) do
|
178
|
+
super(statement, name)
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
def select_one(statement, name=nil)
|
183
|
+
with_entry_point(:select_one) do
|
184
|
+
super(statement, name)
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
def select_value(statement, name=nil)
|
189
|
+
with_entry_point(:select_value) do
|
190
|
+
super(statement, name)
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
protected
|
195
|
+
|
196
|
+
def select(statement, name)
|
197
|
+
returning([]) do
|
198
|
+
self.execution_log << Statement.new(entry_point, statement)
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
private
|
203
|
+
|
204
|
+
def next_unique_id
|
205
|
+
@last_unique_id += 1
|
206
|
+
end
|
207
|
+
|
208
|
+
def with_entry_point(method)
|
209
|
+
if entry_point.nil?
|
210
|
+
with_thread_local_variable(:entry_point, method) do
|
211
|
+
yield
|
212
|
+
end
|
213
|
+
else
|
214
|
+
yield
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
def entry_point
|
219
|
+
Thread.current[:entry_point]
|
220
|
+
end
|
221
|
+
|
222
|
+
def with_thread_local_variable(name, value)
|
223
|
+
old_value = Thread.current[name]
|
224
|
+
Thread.current[name] = value
|
225
|
+
begin
|
226
|
+
yield
|
227
|
+
ensure
|
228
|
+
Thread.current[name] = old_value
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
data/lib/nulldb_rspec.rb
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
require 'active_record/connection_adapters/nulldb_adapter'
|
2
|
+
|
3
|
+
module NullDB
|
4
|
+
module RSpec
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
module NullDB::RSpec::NullifiedDatabase
|
9
|
+
NullDBAdapter = ActiveRecord::ConnectionAdapters::NullDBAdapter
|
10
|
+
|
11
|
+
class HaveExecuted
|
12
|
+
|
13
|
+
def initialize(entry_point)
|
14
|
+
@entry_point = entry_point
|
15
|
+
end
|
16
|
+
|
17
|
+
def matches?(connection)
|
18
|
+
log = connection.execution_log_since_checkpoint
|
19
|
+
if entry_point == :anything
|
20
|
+
not log.empty?
|
21
|
+
else
|
22
|
+
log.include?(NullDBAdapter::Statement.new(@entry_point))
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def description
|
27
|
+
"connection should execute #{@entry_point} statement"
|
28
|
+
end
|
29
|
+
|
30
|
+
def failure_message
|
31
|
+
" did not execute #{@entry_point} statement when it should have"
|
32
|
+
end
|
33
|
+
|
34
|
+
def negative_failure_message
|
35
|
+
" executed #{@entry_point} statement when it should not have"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.globally_nullify_database
|
40
|
+
Spec::Runner.configure do |config|
|
41
|
+
nullify_database(config)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.contextually_nullify_database(context)
|
46
|
+
nullify_database(context)
|
47
|
+
end
|
48
|
+
|
49
|
+
# A matcher for asserting that database statements have (or have not) been
|
50
|
+
# executed. Usage:
|
51
|
+
#
|
52
|
+
# ActiveRecord::Base.connection.should have_executed(:insert)
|
53
|
+
#
|
54
|
+
# The types of statement that can be matched mostly mirror the public
|
55
|
+
# operations available in
|
56
|
+
# ActiveRecord::ConnectionAdapters::DatabaseStatements:
|
57
|
+
# - :select_one
|
58
|
+
# - :select_all
|
59
|
+
# - :select_value
|
60
|
+
# - :insert
|
61
|
+
# - :update
|
62
|
+
# - :delete
|
63
|
+
# - :execute
|
64
|
+
#
|
65
|
+
# There is also a special :anything symbol that will match any operation.
|
66
|
+
def have_executed(entry_point)
|
67
|
+
HaveExecuted.new(entry_point)
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def self.included(other)
|
73
|
+
if other.ancestors.include?(ActiveSupport::TestCase)
|
74
|
+
contextually_nullify_database(other)
|
75
|
+
else
|
76
|
+
globally_nullify_database
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.nullify_database(receiver)
|
81
|
+
receiver.before :all do
|
82
|
+
ActiveRecord::Base.establish_connection(:adapter => :nulldb)
|
83
|
+
end
|
84
|
+
|
85
|
+
receiver.before :each do
|
86
|
+
ActiveRecord::Base.connection.checkpoint!
|
87
|
+
end
|
88
|
+
|
89
|
+
receiver.after :all do
|
90
|
+
ActiveRecord::Base.establish_connection(:test)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
data/spec/nulldb_spec.rb
ADDED
@@ -0,0 +1,189 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'spec'
|
3
|
+
require 'active_record'
|
4
|
+
$: << File.join(File.dirname(__FILE__), "..", "lib")
|
5
|
+
|
6
|
+
class Employee < ActiveRecord::Base
|
7
|
+
after_save :on_save_finished
|
8
|
+
|
9
|
+
def on_save_finished
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
RAILS_ROOT = "RAILS_ROOT"
|
14
|
+
|
15
|
+
describe "NullDB with no schema pre-loaded" do
|
16
|
+
before :each do
|
17
|
+
Kernel.stub!(:load)
|
18
|
+
ActiveRecord::Migration.stub!(:verbose=)
|
19
|
+
end
|
20
|
+
|
21
|
+
it "should load RAILS_ROOT/db/schema.rb if no alternate is specified" do
|
22
|
+
ActiveRecord::Base.establish_connection :adapter => :nulldb
|
23
|
+
Kernel.should_receive(:load).with("RAILS_ROOT/db/schema.rb")
|
24
|
+
ActiveRecord::Base.connection.columns('schema_info')
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should load the specified schema relative to RAILS_ROOT" do
|
28
|
+
Kernel.should_receive(:load).with("RAILS_ROOT/foo/myschema.rb")
|
29
|
+
ActiveRecord::Base.establish_connection :adapter => :nulldb,
|
30
|
+
:schema => "foo/myschema.rb"
|
31
|
+
ActiveRecord::Base.connection.columns('schema_info')
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should suppress migration output" do
|
35
|
+
ActiveRecord::Migration.should_receive(:verbose=).with(false)
|
36
|
+
ActiveRecord::Base.establish_connection :adapter => :nulldb,
|
37
|
+
:schema => "foo/myschema.rb"
|
38
|
+
ActiveRecord::Base.connection.columns('schema_info')
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe "NullDB" do
|
43
|
+
before :all do
|
44
|
+
ActiveRecord::Base.establish_connection :adapter => :nulldb
|
45
|
+
ActiveRecord::Migration.verbose = false
|
46
|
+
ActiveRecord::Schema.define do
|
47
|
+
create_table(:employees) do |t|
|
48
|
+
t.string :name
|
49
|
+
t.date :hire_date
|
50
|
+
t.integer :employee_number
|
51
|
+
t.decimal :salary
|
52
|
+
end
|
53
|
+
|
54
|
+
add_fk_constraint "foo", "bar", "baz", "buz", "bungle"
|
55
|
+
add_pk_constraint "foo", "bar", {}, "baz", "buz"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
before :each do
|
60
|
+
@employee = Employee.new(:name => "John Smith",
|
61
|
+
:hire_date => Date.civil(2000, 1, 1),
|
62
|
+
:employee_number => 42,
|
63
|
+
:salary => 56000.00)
|
64
|
+
end
|
65
|
+
|
66
|
+
it "should enable instantiation of AR objects without a database" do
|
67
|
+
@employee.should_not be_nil
|
68
|
+
@employee.should be_a_kind_of(ActiveRecord::Base)
|
69
|
+
end
|
70
|
+
|
71
|
+
it "should remember columns defined in migrations" do
|
72
|
+
should_have_column(Employee, :name, :string)
|
73
|
+
should_have_column(Employee, :hire_date, :date)
|
74
|
+
should_have_column(Employee, :employee_number, :integer)
|
75
|
+
should_have_column(Employee, :salary, :decimal)
|
76
|
+
end
|
77
|
+
|
78
|
+
it "should enable simulated saving of AR objects" do
|
79
|
+
lambda { @employee.save! }.should_not raise_error
|
80
|
+
end
|
81
|
+
|
82
|
+
it "should enable AR callbacks during simulated save" do
|
83
|
+
@employee.should_receive(:on_save_finished)
|
84
|
+
@employee.save
|
85
|
+
end
|
86
|
+
|
87
|
+
it "should enable simulated deletes of AR objects" do
|
88
|
+
lambda { @employee.destroy }.should_not raise_error
|
89
|
+
end
|
90
|
+
|
91
|
+
it "should enable simulated creates of AR objects" do
|
92
|
+
emp = Employee.create(:name => "Bob Jones")
|
93
|
+
emp.name.should == "Bob Jones"
|
94
|
+
end
|
95
|
+
|
96
|
+
it "should generate new IDs when inserting unsaved objects" do
|
97
|
+
cxn = Employee.connection
|
98
|
+
id1 = cxn.insert("some sql", "SomeClass Create", "id", nil, nil)
|
99
|
+
id2 = cxn.insert("some sql", "SomeClass Create", "id", nil, nil)
|
100
|
+
id2.should == (id1 + 1)
|
101
|
+
end
|
102
|
+
|
103
|
+
it "should re-use object ID when inserting saved objects" do
|
104
|
+
cxn = Employee.connection
|
105
|
+
id1 = cxn.insert("some sql", "SomeClass Create", "id", 23, nil)
|
106
|
+
id1.should == 23
|
107
|
+
end
|
108
|
+
|
109
|
+
it "should log executed SQL statements" do
|
110
|
+
cxn = @employee.connection
|
111
|
+
exec_count = cxn.execution_log.size
|
112
|
+
@employee.save!
|
113
|
+
cxn.execution_log.size.should == (exec_count + 1)
|
114
|
+
end
|
115
|
+
|
116
|
+
it "should have the adapter name 'NullDB'" do
|
117
|
+
@employee.connection.adapter_name.should == "NullDB"
|
118
|
+
end
|
119
|
+
|
120
|
+
it "should support migrations" do
|
121
|
+
@employee.connection.supports_migrations?.should be_true
|
122
|
+
end
|
123
|
+
|
124
|
+
it "should always have a schema_info table definition" do
|
125
|
+
@employee.connection.tables.should include("schema_info")
|
126
|
+
end
|
127
|
+
|
128
|
+
it "should return an empty array from #select" do
|
129
|
+
@employee.connection.select_all("who cares", "blah").should == []
|
130
|
+
end
|
131
|
+
|
132
|
+
it "should provide a way to set log checkpoints" do
|
133
|
+
cxn = @employee.connection
|
134
|
+
@employee.save!
|
135
|
+
cxn.execution_log_since_checkpoint.size.should > 0
|
136
|
+
cxn.checkpoint!
|
137
|
+
cxn.execution_log_since_checkpoint.size.should == 0
|
138
|
+
@employee.save!
|
139
|
+
cxn.execution_log_since_checkpoint.size.should == 1
|
140
|
+
end
|
141
|
+
|
142
|
+
def should_contain_statement(cxn, entry_point)
|
143
|
+
cxn.execution_log_since_checkpoint.should \
|
144
|
+
include(ActiveRecord::ConnectionAdapters::NullDBAdapter::Statement.new(entry_point))
|
145
|
+
end
|
146
|
+
|
147
|
+
def should_not_contain_statement(cxn, entry_point)
|
148
|
+
cxn.execution_log_since_checkpoint.should_not \
|
149
|
+
include(ActiveRecord::ConnectionAdapters::NullDBAdapter::Statement.new(entry_point))
|
150
|
+
end
|
151
|
+
|
152
|
+
it "should tag logged statements with their entry point" do
|
153
|
+
cxn = @employee.connection
|
154
|
+
|
155
|
+
should_not_contain_statement(cxn, :insert)
|
156
|
+
@employee.save
|
157
|
+
should_contain_statement(cxn, :insert)
|
158
|
+
|
159
|
+
cxn.checkpoint!
|
160
|
+
should_not_contain_statement(cxn, :update)
|
161
|
+
@employee.save
|
162
|
+
should_contain_statement(cxn, :update)
|
163
|
+
|
164
|
+
cxn.checkpoint!
|
165
|
+
should_not_contain_statement(cxn, :delete)
|
166
|
+
@employee.destroy
|
167
|
+
should_contain_statement(cxn, :delete)
|
168
|
+
|
169
|
+
cxn.checkpoint!
|
170
|
+
should_not_contain_statement(cxn, :select_all)
|
171
|
+
Employee.find(:all)
|
172
|
+
should_contain_statement(cxn, :select_all)
|
173
|
+
|
174
|
+
cxn.checkpoint!
|
175
|
+
should_not_contain_statement(cxn, :select_value)
|
176
|
+
Employee.count_by_sql("frobozz")
|
177
|
+
should_contain_statement(cxn, :select_value)
|
178
|
+
end
|
179
|
+
|
180
|
+
it "should allow #finish to be called on the result of #execute" do
|
181
|
+
@employee.connection.execute("blah").finish
|
182
|
+
end
|
183
|
+
|
184
|
+
def should_have_column(klass, col_name, col_type)
|
185
|
+
col = klass.columns_hash[col_name.to_s]
|
186
|
+
col.should_not be_nil
|
187
|
+
col.type.should == col_type
|
188
|
+
end
|
189
|
+
end
|
data/tasks/database.rake
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# Sadly, we have to monkeypatch Rake because all of the Rails database tasks are
|
2
|
+
# hardcoded for specific adapters, with no extension points (!)
|
3
|
+
Rake::TaskManager.class_eval do
|
4
|
+
def remove_task(task_name)
|
5
|
+
@tasks.delete(task_name.to_s)
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
def remove_task(task_name)
|
10
|
+
Rake.application.remove_task(task_name)
|
11
|
+
end
|
12
|
+
|
13
|
+
def wrap_task(task_name, &wrapper)
|
14
|
+
wrapped_task = Rake::Task[task_name]
|
15
|
+
remove_task(Rake::Task.scope_name(Rake.application.current_scope,
|
16
|
+
task_name))
|
17
|
+
task(task_name) do
|
18
|
+
wrapper.call(wrapped_task)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# For later exploration...
|
23
|
+
# namespace :db do
|
24
|
+
# namespace :test do
|
25
|
+
# wrap_task :purge do |wrapped_task|
|
26
|
+
# if ActiveRecord::Base.configurations["test"]["adapter"] == "nulldb"
|
27
|
+
# # NO-OP
|
28
|
+
# else
|
29
|
+
# wrapped_task.invoke
|
30
|
+
# end
|
31
|
+
# end
|
32
|
+
# end
|
33
|
+
# end
|
metadata
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: activerecord-nulldb-adapter
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.3
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Avdi Grimm
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-12-30 00:00:00 -08:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: An ActiveRecord null database adapter for greater speed and isolation in unit tests
|
17
|
+
email: avdi@avdi.org
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files:
|
23
|
+
- LICENSE
|
24
|
+
- README
|
25
|
+
files:
|
26
|
+
- LICENSE
|
27
|
+
- README
|
28
|
+
- Rakefile
|
29
|
+
- VERSION
|
30
|
+
- activerecord-nulldb-adapter.gemspec
|
31
|
+
- init.rb
|
32
|
+
- lib/active_record/connection_adapters/nulldb_adapter.rb
|
33
|
+
- lib/nulldb_rspec.rb
|
34
|
+
- spec/nulldb_spec.rb
|
35
|
+
- tasks/database.rake
|
36
|
+
has_rdoc: true
|
37
|
+
homepage: http://nulldb.rubyforge.org
|
38
|
+
licenses: []
|
39
|
+
|
40
|
+
post_install_message:
|
41
|
+
rdoc_options:
|
42
|
+
- --charset=UTF-8
|
43
|
+
require_paths:
|
44
|
+
- lib
|
45
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - ">="
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: "0"
|
50
|
+
version:
|
51
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: "0"
|
56
|
+
version:
|
57
|
+
requirements: []
|
58
|
+
|
59
|
+
rubyforge_project: nulldb
|
60
|
+
rubygems_version: 1.3.5
|
61
|
+
signing_key:
|
62
|
+
specification_version: 3
|
63
|
+
summary: NullDB lets you to test your models without ever touching a real database.
|
64
|
+
test_files:
|
65
|
+
- spec/nulldb_spec.rb
|