activerecord-nulldb-adapter 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|