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:
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 ]
Rails 2.1 installs some new defaults in config/initializers/new_rails_defaults.rb. Remove the ones that reference 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"
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])
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.
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!
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
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"
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
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, "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.