UPDATE 2008/10/17: See Part 2 for transation support, including the ability to run multiple tests (which the code below fails to do because of missing transaction support).
I was bored this evening and wanted to do something at least semi interesting. I settled on integrating DataMapper with Rails. Seemed like a nice enough thing to do. Searching for datamapper on rails on Google gave me a link to DataMapper 0.9 avec Rails (Google translation). Nicolas’ article was very interesting, but it’s focus is just on the basics. I decided to tackle migrations and testing.
Unit testing DataMapper models from within Rails
Let’s start by generating a blank Rails application:
1 $ rails dm_on_rails
- GitHub commit: Initial version
Then, we’ll remove as much dependency on ActiveRecord as we can. From config/environment.rb, uncomment the config.frameworks line and edit it to the following:
config/environment.rb
1 # Skip frameworks you're not going to use. To use Rails without a database 2 # you must remove the Active Record framework. 3 config.frameworks -= [ :active_record ]
- GitHub commit: Don’t load ActiveRecord – we aren’t going to use it.
Rails 2.1 installs some new defaults in config/initializers/new_rails_defaults.rb. Remove the ones that reference ActiveRecord.
- GitHub commit: Remove new rails defaults that talk about ActiveRecord.
Then we need to load the DataMapper gem. Do it by editing config/environment.rb and loading gems:
config/environment.rb
1 # I use SQLite3 here, but if you need/want MySQL, replace sqlite3 with mysql 2 config.gem "do_sqlite3", :version => "0.9.6" 3 config.gem "dm-core", :version => "0.9.6"
- GitHub commit: Reference the DataMapper gems we need
Make sure your gems are up to date by running rake gems:install. I had problems with older versions of dm-core polluting my system. You might want to remove those if you get dependency issues.
Then, we need to configure config/database.yml. Completely replace the data in there with this:
config/database.yml
1 development: &defaults 2 :adapter: sqlite3 3 :database: db/development.sqlite3 4 5 test: 6 <<: *defaults 7 :database: db/test.sqlite3 8 9 production: 10 <<: *defaults 11 :database: db/production.sqlite3
Then, we need to load this YAML file to configure DataMapper. Create config/initializers/datamapper.rb:
config/initializers/datamapper.rb
1 require "dm-core" 2 hash = YAML.load(File.new(Rails.root + "/config/database.yml")) 3 DataMapper.setup(:default, hash[Rails.env])
- GitHub commit: Configured DataMapper to work with SQLite3
Next, let’s generate a model to play with.
1 $ script/generate model article title:string body:text 2 exists app/models/ 3 exists test/unit/ 4 exists test/fixtures/ 5 create app/models/article.rb 6 create test/unit/article_test.rb 7 create test/fixtures/articles.yml 8 uninitialized constant Rails::Generator::GeneratedAttribute::ActiveRecord
Seems we can’t use script/generate model. But the the important files have been created: the fixtures, test and model.
Running rake test:recent gives us another error:
1 $ rake test:recent 2 (in /Users/francois/Documents/work/dm_on_rails) 3 /System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/bin/ruby -Ilib:test "/Library/Ruby/Gems/1.8/gems/rake-0.8.2/lib/rake/rake_test_loader.rb" "test/unit/article_test.rb" 4 /Library/Ruby/Gems/1.8/gems/activesupport-2.1.0/lib/active_support/dependencies.rb:414:in `to_constant_name': Anonymous modules have no name to be referenced by (ArgumentError) 5 from /Library/Ruby/Gems/1.8/gems/activesupport-2.1.0/lib/active_support/dependencies.rb:226:in `qualified_name_for' 6 from /Library/Ruby/Gems/1.8/gems/activesupport-2.1.0/lib/active_support/dependencies.rb:491:in `const_missing' 7 from /Library/Ruby/Gems/1.8/gems/activerecord-2.1.0/lib/active_record/fixtures.rb:869:in `require_fixture_classes' 8 from /Library/Ruby/Gems/1.8/gems/activerecord-2.1.0/lib/active_record/fixtures.rb:867:in `each' 9 from /Library/Ruby/Gems/1.8/gems/activerecord-2.1.0/lib/active_record/fixtures.rb:867:in `require_fixture_classes' 10 from /Library/Ruby/Gems/1.8/gems/activerecord-2.1.0/lib/active_record/fixtures.rb:850:in `fixtures' 11 from ./test/test_helper.rb:35 12 from ./test/unit/article_test.rb:1:in `require' 13 from ./test/unit/article_test.rb:1 14 from /Library/Ruby/Gems/1.8/gems/rake-0.8.2/lib/rake/rake_test_loader.rb:5:in `load' 15 from /Library/Ruby/Gems/1.8/gems/rake-0.8.2/lib/rake/rake_test_loader.rb:5 16 from /Library/Ruby/Gems/1.8/gems/rake-0.8.2/lib/rake/rake_test_loader.rb:5:in `each' 17 from /Library/Ruby/Gems/1.8/gems/rake-0.8.2/lib/rake/rake_test_loader.rb:5 18 rake aborted! 19 Command failed with status (1): [/System/Library/Frameworks/Ruby.framework/...] 20 21 (See full trace by running task with --trace)
This is caused by the default fixtures :all line in test/test_helper.rb. In fact, all the fixtures code is highly dependent on ActiveRecord. So, we won’t use Rails fixtures with DataMapper. Comment out the fixtures :all line, as well as the lines that specify transactional fixtures options.
- GitHub commit: Copy the test_help.rb code in test/test_helper
Next up, the database isn’t created. Ooops! Let’s use a nice DataMapper feature, auto migrations. Append to the end of test/test_helper.rb:
test/test_helper.rb
1 Dir[File.join(Rails.root, "app", "models", "*")].each {|f| require f} 2 DataMapper.auto_migrate!
- GitHub commit: Running auto migrations on test start
We begin by loading all models, because at the end of test/test_helper.rb, no model files have been loaded yet. So we have to define the models in memory before DataMapper can auto migrate them.
Edit test/unit/article_test.rb and put the following:
test/unit/article_test.rb
1 class ArticleTest < ActiveSupport::TestCase 2 def test_create 3 article = Article.create("First Post", :body => "This is my first-ever post", :published_at => Time.now.utc) 4 assert !article.new_record? 5 end 6 end
And put real code in app/models/article.rb:
app/models/article.rb
1 class Article 2 include DataMapper::Resource 3 4 property :id, Integer, :serial => true 5 property :title, String 6 property :body, Text 7 property :published_at, DateTime 8 end
- GitHub commit: Added green test for creating an Article
And with that, we get a fully functional DataMapper integration!
1 $ rake test:units 2 (in /Users/francois/Documents/work/dm_on_rails) 3 /System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/bin/ruby -Ilib:test "/Library/Ruby/Gems/1.8/gems/rake-0.8.2/lib/rake/rake_test_loader.rb" "test/unit/article_test.rb" 4 Loaded suite /Library/Ruby/Gems/1.8/gems/rake-0.8.2/lib/rake/rake_test_loader 5 Started 6 . 7 Finished in 0.005368 seconds. 8 9 1 tests, 0 assertions, 0 failures, 0 errors
Running DataMapper migrations from Rails
What about migrations? Well, now we need to enter the world of dm-migrations. Back in config/environment.rb, depend on a new gem:
config/environment.rb
1 config.gem "dm-migrations", :version => "0.9.6"
- GitHub commit: Depend on dm-migrations
Then, create the migration file itself (remember! no script/generate migration for you):
db/migrate/articles.rb
1 migration 1, :create_articles do 2 up do 3 create_table :articles do 4 column :id, Integer, :serial => true, :nullable? => false 5 column :title, String 6 column :body, "TEXT" 7 end 8 end 9 10 down do 11 drop_table :articles 12 end 13 end
Finally, we want a Rake task which will run the migrations. Since the db:migrate namespace is already used, we’ll create a dm:migrate namespace instead. Create lib/tasks/migrations.rake:
lib/tasks/migrations.rake
1 namespace :dm do 2 task :migrate => :environment do 3 gem "dm-migrations", "=0.9.6" 4 require "migration_runner" 5 Dir[File.join(Rails.root, "db", "migrate", "*")].each {|f| require f} 6 migrate_up! 7 end 8 end
- GitHub commit: Added migrations support and an initial migration
With all that support in place, it’s time to run the migrations:
1 $ rake dm:migrate 2 (in /Users/francois/Documents/work/dm_on_rails) 3 == Performing Up Migration #1: create_articles 4 CREATE TABLE "articles" ("id" INTEGER PRIMARY KEY AUTOINCREMENT, "title" VARCHAR(50), "body" TEXT) 5 -> 0.0005s 6 -> 0.0010s
If you wish to see the code in action, you may checkout the GitHub repository dm_on_rails. Each commit corresponds to a step in this article.