Yesterday, I read Brian Cardarella‘s post entitled Brian Cardarella’s post entitled A case against Mocking and Stubbing. In the article, Brian says:
SQLite3 can be an in-memory database. Problem solved, right? Not quite. SQLite3 is pretty limited. Most people are probably using MySQL and rely upon many of the SQL functions that are included.
What would be nice (and well beyond my ability) is to have a Gem that simulated the database you use, only it is in-memory. Optimized for small data sets. No need to go through a heavy hashing algorithm. Keep it light. Keep it fast.
Brian Cardarella in A case against Mocking and Stubbing
MySQL already has the MEMORY storage engine, and I wanted to see if that would help for testing purposes. Since we’re staying in MySQL-land, this should have been a simple matter.
First, the good news. I had to change only a couple of lines:
1 diff -git a/vendor/rails/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/vendor/rails/activerecor
2 index 1e452ae..c207080 100644
3 -- a/vendor/rails/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
4 + b/vendor/rails/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
5 @ -442,7 +442,13 @ module ActiveRecord
6 end
7
8 def create_table(table_name, options = {}) #:nodoc:
9 – super(table_name, options.reverse_merge(:options => "ENGINE=InnoDB"))
10 + engine = case Rails.env
11 + when "test"
12 + "MEMORY"
13 + else
14 + "InnoDB"
15 + end
16 + super(table_name, options.reverse_merge(:options => "ENGINE=#{engine}"))
17 end
18
19 def rename_table(table_name, new_name)
20 diff -git a/vendor/rails/railties/lib/tasks/databases.rake b/vendor/rails/railties/lib/tasks/databases.rake
21 index 5cb27f1..c520d4a 100644
22 -- a/vendor/rails/railties/lib/tasks/databases.rake
23 + b/vendor/rails/railties/lib/tasks/databases.rake
24 @ -368,9 +368,9 @ namespace :db do
25
26 desc ‘Check for pending migrations and load the test schema’
27 task :prepare => ‘db:abort_if_pending_migrations’ do
28 – if defined?(ActiveRecord) && !ActiveRecord::Base.configurations.blank?
29 – Rake::Task[{ :sql => "db:test:clone_structure", :ruby => "db:test:load" }[ActiveRecord::Base.schema_format]].i
30 – end
31 + # if defined?(ActiveRecord) && !ActiveRecord::Base.configurations.blank?
32 + # Rake::Task[{ :sql => "db:test:clone_structure", :ruby => "db:test:load" }[ActiveRecord::Base.schema_format]]
33 + # end
34 end
35 end
36
Great, but there’s little benefit. First, a regular run (InnoDB):
1 $ time rake
2 (in /Users/francois/Documents/work/fasttest)
3 /System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/bin/ruby -Ilib:test "/Library/Ruby/Gems/1.8/gems/rake-0.8.3/lib/rake/rake_test_loader.rb" "test/unit/password_reset_mailer_test.rb" "test/unit/person_test.rb"
4 Loaded suite /Library/Ruby/Gems/1.8/gems/rake-0.8.3/lib/rake/rake_test_loader
5 Started
6 ……………………….
7 Finished in 0.520087 seconds.
8
9 28 tests, 33 assertions, 0 failures, 0 errors
10 /System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/bin/ruby -Ilib:test "/Library/Ruby/Gems/1.8/gems/rake-0.8.3/lib/rake/rake_test_loader.rb" "test/functional/accounts_controller_test.rb" "test/functional/password_resets_controller_test.rb" "test/functional/people_controller_test.rb" "test/functional/sessions_controller_test.rb"
11 [DEPRECATION] should_be_restful is deprecated. Please see http://thoughtbot.lighthouseapp.com/projects/5807/tickets/78 for more information.
12 [DEPRECATION] should_be_restful is deprecated. Please see http://thoughtbot.lighthouseapp.com/projects/5807/tickets/78 for more information.
13 Loaded suite /Library/Ruby/Gems/1.8/gems/rake-0.8.3/lib/rake/rake_test_loader
14 Started
15 ……………………………………………..
16 Finished in 1.828175 seconds.
17
18 53 tests, 72 assertions, 0 failures, 0 errors
19 /System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/bin/ruby -Ilib:test "/Library/Ruby/Gems/1.8/gems/rake-0.8.3/lib/rake/rake_test_loader.rb"
20
21 real 0m10.365s
22 user 0m6.582s
23 sys 0m1.925s
Next, a run with the MEMORY engine:
1 $ time rake
2 (in /Users/francois/Documents/work/fasttest)
3 /System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/bin/ruby -Ilib:test "/Library/Ruby/Gems/1.8/gems/rake-0.8.3/lib/rake/rake_test_loader.rb" "test/unit/password_reset_mailer_test.rb" "test/unit/person_test.rb"
4 Loaded suite /Library/Ruby/Gems/1.8/gems/rake-0.8.3/lib/rake/rake_test_loader
5 Started
6 ……………………….
7 Finished in 0.602607 seconds.
8
9 28 tests, 33 assertions, 0 failures, 0 errors
10 /System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/bin/ruby -Ilib:test "/Library/Ruby/Gems/1.8/gems/rake-0.8.3/lib/rake/rake_test_loader.rb" "test/functional/accounts_controller_test.rb" "test/functional/password_resets_controller_test.rb" "test/functional/people_controller_test.rb" "test/functional/sessions_controller_test.rb"
11 [DEPRECATION] should_be_restful is deprecated. Please see http://thoughtbot.lighthouseapp.com/projects/5807/tickets/78 for more information.
12 [DEPRECATION] should_be_restful is deprecated. Please see http://thoughtbot.lighthouseapp.com/projects/5807/tickets/78 for more information.
13 Loaded suite /Library/Ruby/Gems/1.8/gems/rake-0.8.3/lib/rake/rake_test_loader
14 Started
15 ……………………………………………..
16 Finished in 1.132142 seconds.
17
18 53 tests, 72 assertions, 0 failures, 0 errors
19 /System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/bin/ruby -Ilib:test "/Library/Ruby/Gems/1.8/gems/rake-0.8.3/lib/rake/rake_test_loader.rb"
20
21 real 0m9.111s
22 user 0m6.862s
23 sys 0m1.870s
Note that in test/test_helper.rb, I had to disable transactional fixtures. This would account for a lot the lost time difference.
If you want to play with this further, the sample application’s code is available at http://github.com/francois/fasttest
I would be interested in seeing other people’s runs, to know if it’s just my machine that runs at essentially the same speed.