Datum: A flexible data-driven test solution for Rails

Synopsis

Datum includes an easy-to-use mechanism for generating test cases based on data attributes. For more granular control when seeding the test database, Datum introduces Scenarios. By combining these features, Datum can be extremely useful and fun.

Feature List

1. Tests coupled with datasets: Datum adds a data-driven extension to the default Rails testing infrastructure. When a data_test is defined, it is coupled with a dataset you define. For each dataset, a test case is generated greatly simplifying adding and removing additional cases.

2. On-demand test database seeding: Datum Scenarios are a per-test or per-test-class database seeding mechanism. Each Scenario can use any set of Models in a single file and can be self-contained or referenced via other Scenarios.

Installing

Update your Gemfile with:

gem 'datum'

Next run the bundle command:

bundle install

To create the default datum directories:

rake datum:install

Simple Examples

data_test: Data Driven Tests for Rails

To get started, we'll look at a simple Person Model app/models/person.rb:

class Person < ActiveRecord::Base

  validates_presence_of :first_name, :last_name

  # "John Doe" from first_name: "John", last_name: "Doe"
  def name
    "#{self.first_name} #{self.last_name}"
  end

  # "John D." from first_name: "John" last_name: "Doe"
  def short_name
    "#{self.first_name} #{self.last_name.capitalize[0]}."
  end
end

Now let's create a test test/models/person_test.rb:

require 'test_helper'
class PersonTest < ActiveSupport::TestCase
  test 'should confirm short_name' do
    person = Person.create first_name: "Marge", last_name: "Simpson"
    assert_equal "Marge S.", person.short_name
  end
end

Executing the test:

# Running:

.

1 runs, 1 assertions, 0 failures, 0 errors, 0 skips

To convert our test to be data-driven - test/models/person_test.rb:

require 'test_helper'
class PersonTest < ActiveSupport::TestCase
  #test 'should confirm short_name' do
  #  person = Person.create first_name: "Marge", last_name: "Simpson"
  #  assert_equal "Marge S.", person.short_name
  #end

  data_test 'should_confirm_shortname' do
    person = Person.create first_name: @datum.first_name, last_name: @datum.last_name
    assert_equal @datum.short_name, person.short_name
  end
end

In the data_test, the fixed values of the original test have been replaced with the usage of the @datum.[attribute] variables. For each dataset defined, the data_test will be called and @datum will provide access.

Next, we'll define our Datum and data in test/datum/data/should_confirm_shortname.rb:

  # Sub-class the Datum struct with attributes we need for our test
  SimpleShortName = Datum.new(:first_name, :last_name, :short_name)

  # Define instances for our test cases
  SimpleShortName.new "Marge", "Simpson", "Marge S."
  SimpleShortName.new "Homer", "Simpson", "Homer S."

Executing:

# Running:

..

2 runs, 2 assertions, 0 failures, 0 errors, 0 skips

Adding more datasets and thus more generated test cases test/datum/data/should_confirm_shortname.rb:

  SimpleShortName = Datum.new(:first_name, :last_name, :short_name)

  # Define instances for our test cases
  SimpleShortName.new "Marge",  "Simpson", "Marge S."
  SimpleShortName.new "Homer",  "Simpson", "Homer S."
  SimpleShortName.new "Lisa",   "Simpson", "Lisa S."
  SimpleShortName.new "Bart",   "Simpson", "Bart S."
  SimpleShortName.new "Maggie", "Simpson", "Maggie S."
# Running:

.....

5 runs, 5 assertions, 0 failures, 0 errors, 0 skips

Scenarios: On-demand Test Database Seeding

To get started, we'll look at a simple Person Model app/models/person.rb:

class Person < ActiveRecord::Base

  validates_presence_of :first_name, :last_name

  # "John Doe" from first_name: "John", last_name: "Doe"
  def name
    "#{self.first_name} #{self.last_name}"
  end

  # "John D." from first_name: "John" last_name: "Doe"
  def short_name
    "#{self.first_name} #{self.last_name.capitalize[0]}."
  end
end

Now let's create a test test/models/person_test.rb:

require 'test_helper'
class PersonTest < ActiveSupport::TestCase
  test 'should confirm short_name' do
    person = Person.create first_name: "Marge", last_name: "Simpson"
    assert_equal "Marge S.", person.short_name
  end
end

Instead of using a Fixture or defining our variables in-line, we'll modify our test to use a Scenario. First, let's define the Scenario test/datum/scenarios/simpsons_scenario.rb:

 @marge = Person.create(first_name: "Marge", last_name: "Simpson")
 @homer = Person.create(__clone(@marge, {first_name: "Homer"}))

Now, let's modify our test, test/models/person_test.rb:

require 'test_helper'
class PersonTest < ActiveSupport::TestCase
  test 'should confirm short_name' do
    process_scenario :simpsons_scenario
    assert_equal "Marge S.", @marge.short_name
    assert_equal "Homer S.", @homer.short_name
  end
end

Data-Driven Tests Combined with On-Demand Test DB Seeding

To get started, we'll look at a simple Person Model app/models/person.rb:

class Person < ActiveRecord::Base

  validates_presence_of :first_name, :last_name

  # "John Doe" from first_name: "John", last_name: "Doe"
  def name
    "#{self.first_name} #{self.last_name}"
  end

  # "John D." from first_name: "John" last_name: "Doe"
  def short_name
    "#{self.first_name} #{self.last_name.capitalize[0]}."
  end
end

Now let's define a data_test test/models/person_test.rb:

require 'test_helper'
class PersonTest < ActiveSupport::TestCase
  data_test 'should_confirm_shortname' do
    person = Person.create first_name: @datum.first_name, last_name: @datum.last_name
    assert_equal @datum.short_name, person.short_name
  end
end

Let's add some initial data test/datum/data/should_confirm_shortname.rb:

  SimpleShortName = Datum.new(:first_name, :last_name, :short_name)

  # Define instances for our test cases
  m = SimpleShortName.new "Marge",  "Simpson", "Marge S."
  SimpleShortName.new "Homer",  m.last_name, "Homer S."
  SimpleShortName.new "Lisa",   m.last_name, "Lisa S."
  SimpleShortName.new "Bart",   m.last_name, "Bart S."
  SimpleShortName.new "Maggie", m.last_name, "Maggie S."

Now let's define a Scenario test/datum/scenarios/simpsons_scenario.rb:

 @marge   = Person.create(first_name: "Marge", last_name: "Simpson")
 @homer   = Person.create(__clone(@marge, {first_name: "Homer"}))
 @lisa    = Person.create(__clone(@marge, {first_name: "Lisa"}))
 @bart    = Person.create(__clone(@marge, {first_name: "Bart"}))
 @maggie  = Person.create(__clone(@marge, {first_name: "Maggie"}))

Now let's update our data_test to make use of our Scenario test/models/person_test.rb:

require 'test_helper'
class PersonTest < ActiveSupport::TestCase
  data_test 'should_confirm_shortname' do
    process_scenario :simpsons_scenario
    person = self.instance_variable_get("@#{@datum.first_name.downcase}")
    assert_equal @datum.first_name, person.first_name
    assert_equal @datum.last_name, person.last_name
    assert_equal @datum.short_name, person.short_name
  end
end

Executing the test:

# Running:

.....

5 runs, 15 assertions, 0 failures, 0 errors, 0 skips

Real-World Examples (Not Finished)

A Model Test to verify addresses from different countries:

require 'test_helper'
class AddressLabelTest < ActiveSupport::TestCase

  # makes use of 'AddressLabel' Model
  data_test 'should_verify_address_labels' do
    # label = AddressLabel.create

    #assert_equal @datum.
    #assert_equal @datum.
    #assert_equal @datum.
  end
end

A Functional Test to verify CRUD with different permissions:

require 'test_helper'
class AddressLabelTest < ActiveSupport::TestCase

  # makes use of 'AddressLabel' Model
  data_test 'should_verify_address_labels' do
    # label = AddressLabel.create

    #assert_equal @datum.
    #assert_equal @datum.
    #assert_equal @datum.
  end
end

License

MIT License. Copyright 2012-2015 Tyemill. http://tyemill.com

You are not granted rights or licenses to the trademarks of Tyemill, including without limitation the Datum name or logo.

Datum struct uses ImprovedStruct and ImmutableStruct which are derivitive works based on ImmutableStruct by Theo Hultberg. This great class can be found here: Immutable Struct