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.