In Part I, I showed how to configure Rails to use DataMapper instead of ActiveRecord. Well, after some thought, I realized we couldn’t really test anything. Here’s the proof:


1 class ArticleTest < ActiveSupport::TestCase
2 def test_database_is_empty
3 assert Article.all.empty?
4 end
5 end

Running the tests results in a failure:


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 .F
7 Finished in 0.049976 seconds.
8
9 1) Failure:
10 test_database_is_empty(ArticleTest)
11 [./test/unit/article_test.rb:10:in `test_db_empty_on_start’
12 /Library/Ruby/Gems/1.8/gems/activesupport-2.1.0/lib/active_support/testing/setup_and_teardown.rb:67:in `__send__’
13 /Library/Ruby/Gems/1.8/gems/activesupport-2.1.0/lib/active_support/testing/setup_and_teardown.rb:67:in `run’]:
14 <false> is not true.
15
16 2 tests, 2 assertions, 1 failures, 0 errors
17 rake aborted!
18 Command failed with status (1): [/System/Library/Frameworks/Ruby.framework/…]
19
20 (See full trace by running task with —trace)

Time to open the wonderful world of transactions! At the time of this writing, DataMapper is at 0.9.6. There is a comment in lib/dm-core/transaction.rb saying TODO: move to dm-more/dm-transactions. If the code below doesn’t work, add a new dependency on dm-transaction.

So, let’s begin by adding new setup / teardown hooks to Test::Unit::TestCase:

test/test_helper.rb

1 class Test::Unit::TestCase
2 setup do
3 __transaction</span> = <span class="co">DataMapper</span>::<span class="co">Transaction</span>.new(<span class="co">DataMapper</span>.repository(<span class="sy">:default</span>)) <span class="no"> 4</span> <span class="iv">transaction.begin
5
6 # FIXME: Should I really be calling #push_transaction like that, or is there a better way?
7 DataMapper.repository(:default).adapter.push_transaction(@
transaction)
8 end
9
10 teardown do
11 if __transaction</span> <span class="no">12</span> <span class="co">DataMapper</span>.repository(<span class="sy">:default</span>).adapter.pop_transaction <span class="no">13</span> <span class="iv">transaction.rollback
14 @
transaction = nil
15 end
16 end
17 end

With that code in place, we can run the tests again and see the results of our hard work:


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.009485 seconds.
8
9 2 tests, 2 assertions, 0 failures, 0 errors

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

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.

Search

Your Host

A picture of me

I am François Beausoleil, a Ruby on Rails and Scala developer. During the day, I work on Seevibes, a platform to measure social interactions related to TV shows. At night, I am interested many things. Read my biography.

Top Tags

Books I read and recommend

Links

Projects I work on

Projects I worked on