active_data_migrations 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,10 @@
1
+ *.gem
2
+ .bundle
3
+ pkg/*
4
+ active_data_migrations.iml
5
+ active_data_migrations.ipr
6
+ active_data_migrations.iws
7
+ .svn
8
+ .svn/**
9
+ reinstall.sh
10
+ .rvmrc
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in active_data_migrations.gemspec
4
+ gemspec
@@ -0,0 +1,98 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ active_data_migrations (1.0.0.beta)
5
+
6
+ GEM
7
+ remote: http://rubygems.org/
8
+ specs:
9
+ actionmailer (3.1.6)
10
+ actionpack (= 3.1.6)
11
+ mail (~> 2.3.3)
12
+ actionpack (3.1.6)
13
+ activemodel (= 3.1.6)
14
+ activesupport (= 3.1.6)
15
+ builder (~> 3.0.0)
16
+ erubis (~> 2.7.0)
17
+ i18n (~> 0.6)
18
+ rack (~> 1.3.6)
19
+ rack-cache (~> 1.2)
20
+ rack-mount (~> 0.8.2)
21
+ rack-test (~> 0.6.1)
22
+ sprockets (~> 2.0.4)
23
+ activemodel (3.1.6)
24
+ activesupport (= 3.1.6)
25
+ builder (~> 3.0.0)
26
+ i18n (~> 0.6)
27
+ activerecord (3.1.6)
28
+ activemodel (= 3.1.6)
29
+ activesupport (= 3.1.6)
30
+ arel (~> 2.2.3)
31
+ tzinfo (~> 0.3.29)
32
+ activeresource (3.1.6)
33
+ activemodel (= 3.1.6)
34
+ activesupport (= 3.1.6)
35
+ activesupport (3.1.6)
36
+ multi_json (>= 1.0, < 1.3)
37
+ arel (2.2.3)
38
+ builder (3.0.0)
39
+ erubis (2.7.0)
40
+ hike (1.2.1)
41
+ i18n (0.6.0)
42
+ json (1.7.3)
43
+ mail (2.3.3)
44
+ i18n (>= 0.4.0)
45
+ mime-types (~> 1.16)
46
+ treetop (~> 1.4.8)
47
+ mime-types (1.19)
48
+ multi_json (1.2.0)
49
+ polyglot (0.3.3)
50
+ rack (1.3.6)
51
+ rack-cache (1.2)
52
+ rack (>= 0.4)
53
+ rack-mount (0.8.3)
54
+ rack (>= 1.0.0)
55
+ rack-ssl (1.3.2)
56
+ rack
57
+ rack-test (0.6.1)
58
+ rack (>= 1.0)
59
+ rails (3.1.6)
60
+ actionmailer (= 3.1.6)
61
+ actionpack (= 3.1.6)
62
+ activerecord (= 3.1.6)
63
+ activeresource (= 3.1.6)
64
+ activesupport (= 3.1.6)
65
+ bundler (~> 1.0)
66
+ railties (= 3.1.6)
67
+ railties (3.1.6)
68
+ actionpack (= 3.1.6)
69
+ activesupport (= 3.1.6)
70
+ rack-ssl (~> 1.3.2)
71
+ rake (>= 0.8.7)
72
+ rdoc (~> 3.4)
73
+ thor (~> 0.14.6)
74
+ rake (0.9.2.2)
75
+ rdoc (3.12)
76
+ json (~> 1.4)
77
+ sprockets (2.0.4)
78
+ hike (~> 1.2)
79
+ rack (~> 1.0)
80
+ tilt (~> 1.1, != 1.3.0)
81
+ sqlite3 (1.3.6)
82
+ sqlite3-ruby (1.3.3)
83
+ sqlite3 (>= 1.3.3)
84
+ thor (0.14.6)
85
+ tilt (1.3.3)
86
+ treetop (1.4.10)
87
+ polyglot
88
+ polyglot (>= 0.3.1)
89
+ tzinfo (0.3.33)
90
+
91
+ PLATFORMS
92
+ ruby
93
+
94
+ DEPENDENCIES
95
+ active_data_migrations!
96
+ activerecord
97
+ rails
98
+ sqlite3-ruby
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2012 Jason McDonald (http://www.McDonaldLand.info)
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
File without changes
@@ -0,0 +1,8 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rake/testtask'
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << 'test'
6
+ t.test_files = FileList['test/**/*_test.rb']
7
+ t.verbose = true
8
+ end
@@ -0,0 +1,28 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "active_data_migrations/version"
4
+
5
+ Gem::Specification.new do |s|
6
+
7
+ s.name = "active_data_migrations"
8
+ s.version = ActiveDataMigrations::VERSION
9
+
10
+ s.author = "Jason McDonald"
11
+ s.email = "jason@mcdonaldland.info"
12
+ s.homepage = "http://www.McDonaldLand.info"
13
+
14
+ s.summary = "Utilizing ActiveRecord migrations to enable data migrations independently from schema migrations."
15
+ s.description = "Utilizing ActiveRecord migrations to enable data migrations independently from schema migrations."
16
+
17
+ s.rubyforge_project = "active_data_migrations"
18
+
19
+ s.files = `git ls-files`.split("\n")
20
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
21
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
22
+ s.require_paths = ["lib"]
23
+
24
+ s.add_development_dependency "activerecord"
25
+ s.add_development_dependency "rails"
26
+ s.add_development_dependency "activerecord"
27
+ s.add_development_dependency "sqlite3-ruby"
28
+ end
@@ -0,0 +1,6 @@
1
+ module ActiveDataMigrations
2
+ require "active_data_migrations/version"
3
+ require 'active_data_migrations/railtie' if defined?(Rails)
4
+ end
5
+
6
+
@@ -0,0 +1,44 @@
1
+ class Migration
2
+
3
+ class << self
4
+
5
+ # Runs a migration, typically an alternative one to the defalt db/migrate schema migration. The default
6
+ # for this task is to look in the /db/data directory. This can be changed by specifying an alternative
7
+ # path by using the MIGRATE_PATH variable.
8
+ def migrate(migration_path)
9
+
10
+ puts "\n"
11
+
12
+ curr_migration_path = ActiveRecord::Migrator.migrations_path
13
+ puts "Rails migration path currently set to '#{curr_migration_path}'"
14
+
15
+ # Configure the database connection.
16
+ ActiveRecord::Base.configurations = Rails.application.config.database_configuration if defined?(Rails)
17
+
18
+ # Set up the path to the migration if it is nil.
19
+ migration_path ||= "/db/data"
20
+ puts "Operating with migration path '#{migration_path}'"
21
+
22
+ # Fully qualify the migration path.
23
+ if defined?(Rails)
24
+ root = Rails.root if defined?(Rails)
25
+ else
26
+ root = File.expand_path(File.join(File.dirname(__FILE__), "..", ".."))
27
+ end
28
+
29
+ migration_path = File.join(root, migration_path)
30
+
31
+ # Set the migration path to ActiveRecord.
32
+ ActiveRecord::Migrator.migrations_path = migration_path
33
+ puts "Updated Rails migration path to '#{ActiveRecord::Migrator.migrations_path}'"
34
+
35
+ # Execute the ActiveRecord migration.
36
+ ActiveRecord::Migration.verbose = ENV["VERBOSE"] ? ENV["VERBOSE"] == "true" : true
37
+ ActiveRecord::Migrator.migrate(ActiveRecord::Migrator.migrations_paths, ENV["VERSION"] ? ENV["VERSION"].to_i : nil) do |migration|
38
+ ENV["SCOPE"].blank? || (ENV["SCOPE"] == migration.scope)
39
+ end
40
+ ensure
41
+ ActiveRecord::Migrator.migrations_path = curr_migration_path
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,15 @@
1
+ require 'active_data_migrations'
2
+ require 'rails'
3
+
4
+ module ActiveDataMigrations
5
+
6
+ class Railtie < Rails::Railtie
7
+
8
+ railtie_name :active_data_migrations
9
+
10
+ rake_tasks do
11
+ load "active_data_migrations/tasks/data_migrations.rake"
12
+ end
13
+ end
14
+
15
+ end
@@ -0,0 +1,15 @@
1
+ require 'active_record'
2
+ require 'active_data_migrations/migration'
3
+
4
+ namespace :db do
5
+
6
+ namespace :data do
7
+
8
+ desc "Runs a migration, typically an alternative one to the defalt db/migrate schema migration. The default " +
9
+ "for this task is to look in the /db/data directory. This can be changed by specifying an alternative " +
10
+ "path by using the MIGRATE_PATH variable."
11
+ task :migrate => [ :environment, :rails_env ] do
12
+ Migration.migrate(ENV["MIGRATE_PATH"])
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,3 @@
1
+ module ActiveDataMigrations
2
+ VERSION = "1.0.0"
3
+ end
@@ -0,0 +1,109 @@
1
+ # test/test_helper.rb
2
+ $LOAD_PATH << File.join(File.dirname(__FILE__), '..', 'lib')
3
+
4
+ require 'rubygems'
5
+ require 'active_record'
6
+ require 'test/unit'
7
+
8
+ require 'config'
9
+ require 'active_data_migrations'
10
+ require 'active_data_migrations/migration'
11
+
12
+ require "#{MODEL_ROOT}/person"
13
+ require "#{MODEL_ROOT}/place"
14
+ require "#{MODEL_ROOT}/thing"
15
+
16
+ ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => ":memory:")
17
+
18
+ # Test to ensure that the schema is still being migrated as expected.
19
+ class ActiveDataMigrationTest < Test::Unit::TestCase
20
+
21
+ # TODO: TEST LOCALLY WITH MLT
22
+ # TODO: CREATE README
23
+ # TODO: UPDATE .gitignore
24
+ # TODO: PUBLISH TO RubyGems.org
25
+ # TODO: PUBLISH TO github
26
+
27
+
28
+ def setup
29
+ Migration.migrate("/test/migrations/schema_migrations")
30
+ assert_schema_structure_valid
31
+ end
32
+
33
+ def teardown
34
+ ActiveRecord::Base.connection.tables.each {|table|
35
+ puts "Teardown: Dropping table #{table}"
36
+ ActiveRecord::Base.connection.execute("drop table #{table}")
37
+ }
38
+ assert 0, ActiveRecord::Base.connection.tables.size
39
+ end
40
+
41
+ def test_data_migrations_do_not_overwrite_existing_migration_identifier
42
+
43
+ # Ensure we are in a known state to start with.
44
+ person_cnt = Person.count
45
+ place_cnt = Place.count
46
+ thing_cnt = Thing.count
47
+
48
+ assert_equal 1, person_cnt
49
+ assert_equal 1, place_cnt
50
+ assert_equal 1, thing_cnt
51
+
52
+ # Migrate the data
53
+ Migration.migrate("/test/migrations/skip_data_migrations")
54
+
55
+ # Make sure we have the expected number of results
56
+ # after all the data migrations have run.
57
+ assert_equal person_cnt, Person.count
58
+ assert_equal place_cnt, Place.count
59
+ assert_equal thing_cnt, Thing.count
60
+ end
61
+
62
+ def test_data_migrations
63
+
64
+ # Ensure we are in a known state to start with.
65
+ person_cnt = Person.count
66
+ place_cnt = Place.count
67
+ thing_cnt = Thing.count
68
+
69
+ assert_equal 1, person_cnt
70
+ assert_equal 1, place_cnt
71
+ assert_equal 1, thing_cnt
72
+
73
+ # Migrate the data
74
+ Migration.migrate("/test/migrations/data_migrations")
75
+
76
+ # Make sure we have the expected number of results
77
+ # after all the data migrations have run.
78
+ assert_equal person_cnt + 10, Person.count
79
+ assert_equal place_cnt + 10, Place.count
80
+ assert_equal thing_cnt + 10, Thing.count
81
+ end
82
+
83
+ private
84
+
85
+
86
+ def assert_schema_structure_valid
87
+ person_cnt = Person.count
88
+ place_cnt = Place.count
89
+ thing_cnt = Thing.count
90
+
91
+ person = Person.new
92
+ person.name = "TestPerson"
93
+ person.save
94
+
95
+ place = Place.new
96
+ place.name = "TestPlace"
97
+ place.person = person
98
+ place.save
99
+
100
+ thing = Thing.new
101
+ thing.name = "TestThing"
102
+ thing.person = person
103
+ thing.save
104
+
105
+ assert_equal person_cnt + 1, Person.count
106
+ assert_equal place_cnt + 1, Place.count
107
+ assert_equal thing_cnt + 1, Thing.count
108
+ end
109
+ end
@@ -0,0 +1,4 @@
1
+ TEST_ROOT = File.expand_path(File.dirname(__FILE__))
2
+ DATA_MIGRATIONS_ROOT = TEST_ROOT + "/data_migrations"
3
+ SCHEMA_MIGRATIONS_ROOT = TEST_ROOT + "/schema_migrations"
4
+ MODEL_ROOT = TEST_ROOT + "/models"
@@ -0,0 +1,37 @@
1
+ class CreateInitialData < ActiveRecord::Migration
2
+
3
+ def self.up
4
+
5
+ puts "== Creating 10 person records, 20 place records, and 20 thing records =============================================="
6
+
7
+ (1..10).each {|i|
8
+
9
+ person = Person.new
10
+ person.name = "TestPerson#{i}"
11
+ person.save
12
+
13
+ place = Place.new
14
+ place.name = "TestPlace#{i}"
15
+ place.person = person
16
+ place.save
17
+
18
+ place = Place.new
19
+ place.name = "TestPlace#{i}.2"
20
+ place.person = person
21
+ place.save
22
+
23
+ thing = Thing.new
24
+ thing.name = "TestThing#{i}"
25
+ thing.person = person
26
+ thing.save
27
+
28
+ thing = Thing.new
29
+ thing.name = "TestThing#{i}.2"
30
+ thing.person = person
31
+ thing.save
32
+ }
33
+ end
34
+
35
+ def self.down
36
+ end
37
+ end
@@ -0,0 +1,12 @@
1
+ class UpdateInitialData < ActiveRecord::Migration
2
+
3
+ def self.up
4
+ puts "== Destroying 10 place records and 10 thing records =============================================="
5
+
6
+ Place.destroy_all("name like '%.2'")
7
+ Thing.destroy_all("name like '%.2'")
8
+ end
9
+
10
+ def self.down
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ class CreatePeopleTable < ActiveRecord::Migration
2
+
3
+ def self.up
4
+ create_table("people") do |t|
5
+ t.string :name
6
+ end
7
+ end
8
+
9
+ def self.down
10
+ drop_table "people"
11
+ end
12
+ end
@@ -0,0 +1,13 @@
1
+ class CreatePlacesTable < ActiveRecord::Migration
2
+
3
+ def self.up
4
+ create_table :places do |t|
5
+ t.references :people
6
+ t.string :name
7
+ end
8
+ end
9
+
10
+ def self.down
11
+ drop_table :places
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ class CreateThingsTable < ActiveRecord::Migration
2
+
3
+ def self.up
4
+ create_table :things do |t|
5
+ t.references :people
6
+ t.string :name
7
+ end
8
+ end
9
+
10
+ def self.down
11
+ drop_table :things
12
+ end
13
+ end
@@ -0,0 +1,37 @@
1
+ class CreateInitialData < ActiveRecord::Migration
2
+
3
+ def self.up
4
+
5
+ puts "== Creating 10 person records, 20 place records, and 20 thing records =============================================="
6
+
7
+ (1..10).each {|i|
8
+
9
+ person = Person.new
10
+ person.name = "TestPerson#{i}"
11
+ person.save
12
+
13
+ place = Place.new
14
+ place.name = "TestPlace#{i}"
15
+ place.person = person
16
+ place.save
17
+
18
+ place = Place.new
19
+ place.name = "TestPlace#{i}.2"
20
+ place.person = person
21
+ place.save
22
+
23
+ thing = Thing.new
24
+ thing.name = "TestThing#{i}"
25
+ thing.person = person
26
+ thing.save
27
+
28
+ thing = Thing.new
29
+ thing.name = "TestThing#{i}.2"
30
+ thing.person = person
31
+ thing.save
32
+ }
33
+ end
34
+
35
+ def self.down
36
+ end
37
+ end
@@ -0,0 +1,12 @@
1
+ class UpdateInitialData < ActiveRecord::Migration
2
+
3
+ def self.up
4
+ puts "== Destroying 10 place records and 10 thing records =============================================="
5
+
6
+ Place.destroy_all("name like '%.2'")
7
+ Thing.destroy_all("name like '%.2'")
8
+ end
9
+
10
+ def self.down
11
+ end
12
+ end
@@ -0,0 +1,4 @@
1
+ class Person < ActiveRecord::Base
2
+ has_many :places
3
+ has_many :things
4
+ end
@@ -0,0 +1,3 @@
1
+ class Place < ActiveRecord::Base
2
+ belongs_to :person
3
+ end
@@ -0,0 +1,3 @@
1
+ class Thing < ActiveRecord::Base
2
+ belongs_to :person
3
+ end
metadata ADDED
@@ -0,0 +1,154 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: active_data_migrations
3
+ version: !ruby/object:Gem::Version
4
+ hash: 23
5
+ prerelease:
6
+ segments:
7
+ - 1
8
+ - 0
9
+ - 0
10
+ version: 1.0.0
11
+ platform: ruby
12
+ authors:
13
+ - Jason McDonald
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2012-07-17 00:00:00 Z
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: activerecord
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ hash: 3
29
+ segments:
30
+ - 0
31
+ version: "0"
32
+ type: :development
33
+ version_requirements: *id001
34
+ - !ruby/object:Gem::Dependency
35
+ name: rails
36
+ prerelease: false
37
+ requirement: &id002 !ruby/object:Gem::Requirement
38
+ none: false
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ hash: 3
43
+ segments:
44
+ - 0
45
+ version: "0"
46
+ type: :development
47
+ version_requirements: *id002
48
+ - !ruby/object:Gem::Dependency
49
+ name: activerecord
50
+ prerelease: false
51
+ requirement: &id003 !ruby/object:Gem::Requirement
52
+ none: false
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ hash: 3
57
+ segments:
58
+ - 0
59
+ version: "0"
60
+ type: :development
61
+ version_requirements: *id003
62
+ - !ruby/object:Gem::Dependency
63
+ name: sqlite3-ruby
64
+ prerelease: false
65
+ requirement: &id004 !ruby/object:Gem::Requirement
66
+ none: false
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ hash: 3
71
+ segments:
72
+ - 0
73
+ version: "0"
74
+ type: :development
75
+ version_requirements: *id004
76
+ description: Utilizing ActiveRecord migrations to enable data migrations independently from schema migrations.
77
+ email: jason@mcdonaldland.info
78
+ executables: []
79
+
80
+ extensions: []
81
+
82
+ extra_rdoc_files: []
83
+
84
+ files:
85
+ - .gitignore
86
+ - Gemfile
87
+ - Gemfile.lock
88
+ - MIT-LICENSE
89
+ - README
90
+ - Rakefile
91
+ - active_data_migrations.gemspec
92
+ - lib/active_data_migrations.rb
93
+ - lib/active_data_migrations/migration.rb
94
+ - lib/active_data_migrations/railtie.rb
95
+ - lib/active_data_migrations/tasks/data_migrations.rake
96
+ - lib/active_data_migrations/version.rb
97
+ - test/cases/active_data_migration_test.rb
98
+ - test/config.rb
99
+ - test/migrations/data_migrations/004_create_initial_data.rb
100
+ - test/migrations/data_migrations/005_update_initial_data.rb
101
+ - test/migrations/schema_migrations/001_create_people_table.rb
102
+ - test/migrations/schema_migrations/002_create_places_table.rb
103
+ - test/migrations/schema_migrations/003_create_things_table.rb
104
+ - test/migrations/skip_data_migrations/001_create_initial_data.rb
105
+ - test/migrations/skip_data_migrations/002_update_initial_data.rb
106
+ - test/models/person.rb
107
+ - test/models/place.rb
108
+ - test/models/thing.rb
109
+ homepage: http://www.McDonaldLand.info
110
+ licenses: []
111
+
112
+ post_install_message:
113
+ rdoc_options: []
114
+
115
+ require_paths:
116
+ - lib
117
+ required_ruby_version: !ruby/object:Gem::Requirement
118
+ none: false
119
+ requirements:
120
+ - - ">="
121
+ - !ruby/object:Gem::Version
122
+ hash: 3
123
+ segments:
124
+ - 0
125
+ version: "0"
126
+ required_rubygems_version: !ruby/object:Gem::Requirement
127
+ none: false
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ hash: 3
132
+ segments:
133
+ - 0
134
+ version: "0"
135
+ requirements: []
136
+
137
+ rubyforge_project: active_data_migrations
138
+ rubygems_version: 1.8.10
139
+ signing_key:
140
+ specification_version: 3
141
+ summary: Utilizing ActiveRecord migrations to enable data migrations independently from schema migrations.
142
+ test_files:
143
+ - test/cases/active_data_migration_test.rb
144
+ - test/config.rb
145
+ - test/migrations/data_migrations/004_create_initial_data.rb
146
+ - test/migrations/data_migrations/005_update_initial_data.rb
147
+ - test/migrations/schema_migrations/001_create_people_table.rb
148
+ - test/migrations/schema_migrations/002_create_places_table.rb
149
+ - test/migrations/schema_migrations/003_create_things_table.rb
150
+ - test/migrations/skip_data_migrations/001_create_initial_data.rb
151
+ - test/migrations/skip_data_migrations/002_update_initial_data.rb
152
+ - test/models/person.rb
153
+ - test/models/place.rb
154
+ - test/models/thing.rb