with_model 2.2.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/dependabot.yml +22 -6
- data/.github/workflows/ci.yml +64 -29
- data/CHANGELOG.md +19 -0
- data/CONTRIBUTING.md +83 -0
- data/Gemfile +2 -7
- data/LICENSE +1 -1
- data/README.md +116 -22
- data/lib/with_model/invalid_superclass.rb +15 -0
- data/lib/with_model/missing_superclass.rb +14 -0
- data/lib/with_model/model/dsl.rb +5 -2
- data/lib/with_model/model.rb +86 -6
- data/lib/with_model/null_table.rb +98 -0
- data/lib/with_model/table.rb +12 -0
- data/lib/with_model/version.rb +1 -1
- data/lib/with_model.rb +39 -1
- data/spec/active_record_behaviors_spec.rb +4 -1
- data/spec/descendants_tracking_spec.rb +1 -0
- data/spec/readme_spec.rb +61 -12
- data/spec/spec_helper.rb +2 -0
- data/spec/table_false_spec.rb +516 -0
- data/spec/with_model_spec.rb +31 -8
- data/test/declaration_order_test.rb +61 -0
- data/test/spec_dsl_test.rb +48 -0
- data/test/sti_test.rb +68 -0
- data/test/test_helper.rb +2 -0
- data/with_model.gemspec +2 -2
- metadata +14 -6
data/lib/with_model/model.rb
CHANGED
|
@@ -7,6 +7,9 @@ require "English"
|
|
|
7
7
|
require "with_model/constant_stubber"
|
|
8
8
|
require "with_model/descendants_tracker"
|
|
9
9
|
require "with_model/methods"
|
|
10
|
+
require "with_model/invalid_superclass"
|
|
11
|
+
require "with_model/missing_superclass"
|
|
12
|
+
require "with_model/null_table"
|
|
10
13
|
require "with_model/table"
|
|
11
14
|
|
|
12
15
|
module WithModel
|
|
@@ -16,17 +19,43 @@ module WithModel
|
|
|
16
19
|
attr_writer :model_block, :table_block, :table_options
|
|
17
20
|
|
|
18
21
|
# @param [Symbol] name The constant name to assign the model class to.
|
|
19
|
-
# @param
|
|
20
|
-
#
|
|
22
|
+
# @param superclass The superclass for the created class. Either a Class
|
|
23
|
+
# having `ActiveRecord::Base` as an ancestor, a String naming one, or a
|
|
24
|
+
# callable returning one. A String or callable is resolved afresh for
|
|
25
|
+
# every example, which is what allows another `with_model` model - whose
|
|
26
|
+
# constant does not exist when this line is read - to be the superclass.
|
|
21
27
|
def initialize(name, superclass: ActiveRecord::Base)
|
|
22
28
|
@name = name.to_sym
|
|
23
29
|
@model_block = nil
|
|
24
30
|
@table_block = nil
|
|
25
31
|
@table_options = {}
|
|
26
|
-
@
|
|
32
|
+
@table_specified = false
|
|
33
|
+
@skip_table = false
|
|
34
|
+
@superclass_spec = superclass
|
|
27
35
|
end
|
|
28
36
|
|
|
37
|
+
# Records what {WithModel::Model::DSL#table} was asked for, including the
|
|
38
|
+
# fact that it was asked for at all.
|
|
39
|
+
def specify_table(options, block)
|
|
40
|
+
@table_specified = true
|
|
41
|
+
|
|
42
|
+
if options == false
|
|
43
|
+
raise ArgumentError, "table does not take a block when its first argument is falsy" if block
|
|
44
|
+
|
|
45
|
+
@skip_table = true
|
|
46
|
+
else
|
|
47
|
+
@table_options = options
|
|
48
|
+
@table_block = block
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Whether a table was specified at all. A `table` call with no arguments
|
|
53
|
+
# counts, so this cannot be inferred from the options and block alone.
|
|
54
|
+
def table_specified? = @table_specified
|
|
55
|
+
|
|
29
56
|
def create
|
|
57
|
+
@superclass = resolve_superclass
|
|
58
|
+
@table = nil
|
|
30
59
|
table.create
|
|
31
60
|
@model = Class.new(@superclass) do
|
|
32
61
|
extend WithModel::Methods
|
|
@@ -36,22 +65,69 @@ module WithModel
|
|
|
36
65
|
end
|
|
37
66
|
|
|
38
67
|
def destroy
|
|
68
|
+
# Test runners tear down even when setup raised, so `create` may not have
|
|
69
|
+
# reached the point of building the model. Nothing was stubbed and nothing
|
|
70
|
+
# wrote rows, so there is nothing to undo.
|
|
71
|
+
return unless @model
|
|
72
|
+
|
|
73
|
+
# Before `unstub_const`: a teardown that identifies this model's own rows
|
|
74
|
+
# can only do so while the class still has its name.
|
|
75
|
+
table.teardown(@model)
|
|
39
76
|
stubber.unstub_const
|
|
40
77
|
cleanup_descendants_tracking
|
|
41
78
|
reset_dependencies_cache
|
|
42
|
-
table.destroy
|
|
43
79
|
WithModel::DescendantsTracker.clear([@model])
|
|
44
80
|
@model = nil
|
|
45
81
|
end
|
|
46
82
|
|
|
47
83
|
private
|
|
48
84
|
|
|
85
|
+
def resolve_superclass
|
|
86
|
+
spec = @superclass_spec
|
|
87
|
+
spec = spec.call if spec.respond_to?(:call)
|
|
88
|
+
spec = constantize_superclass(spec) if class_name?(spec)
|
|
89
|
+
|
|
90
|
+
unless spec.is_a?(Class) && spec <= ActiveRecord::Base
|
|
91
|
+
raise InvalidSuperclass,
|
|
92
|
+
"superclass must be a Class descending from ActiveRecord::Base, but was #{spec.inspect}. " \
|
|
93
|
+
"To refer to another with_model class, or anything else that is only defined once the " \
|
|
94
|
+
"test is running, name it with a String or a Symbol, or pass a callable returning it."
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
spec
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# A Symbol reads naturally here, since with_model names its own models with
|
|
101
|
+
# them. Anything else has to answer to `to_str`, which only real strings do:
|
|
102
|
+
# every object has a `to_s`, so accepting that would quietly look up a
|
|
103
|
+
# constant named "42".
|
|
104
|
+
def class_name?(spec)
|
|
105
|
+
spec.is_a?(Symbol) || spec.respond_to?(:to_str)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# `to_s` is safe here where `class_name?` has already vouched for the value,
|
|
109
|
+
# and it keeps the Symbol intact for the message below, which reports what was
|
|
110
|
+
# passed in rather than what it was converted to.
|
|
111
|
+
#
|
|
112
|
+
# The NameError is worth quoting rather than replacing: for a namespaced name
|
|
113
|
+
# it reports which segment was missing, which this message cannot work out.
|
|
114
|
+
# Raising inside the rescue leaves it as the `cause` for anything that wants
|
|
115
|
+
# the backtrace.
|
|
116
|
+
def constantize_superclass(name)
|
|
117
|
+
name.to_s.constantize
|
|
118
|
+
rescue NameError => e
|
|
119
|
+
raise MissingSuperclass,
|
|
120
|
+
"superclass #{name.inspect} could not be resolved: #{e.message}. Names are resolved while " \
|
|
121
|
+
"the test is running, so a with_model superclass has to be declared before the models " \
|
|
122
|
+
"that inherit it."
|
|
123
|
+
end
|
|
124
|
+
|
|
49
125
|
def const_name
|
|
50
126
|
@name.to_s.camelize.to_sym
|
|
51
127
|
end
|
|
52
128
|
|
|
53
129
|
def setup_model
|
|
54
|
-
@model
|
|
130
|
+
table.configure(@model)
|
|
55
131
|
@model.class_eval(&@model_block) if @model_block
|
|
56
132
|
@model.reset_column_information
|
|
57
133
|
end
|
|
@@ -72,7 +148,11 @@ module WithModel
|
|
|
72
148
|
end
|
|
73
149
|
|
|
74
150
|
def table
|
|
75
|
-
@table ||=
|
|
151
|
+
@table ||= if @skip_table
|
|
152
|
+
NullTable.new(@superclass, @name)
|
|
153
|
+
else
|
|
154
|
+
Table.new table_name, @table_options, connection: @superclass.connection, &@table_block
|
|
155
|
+
end
|
|
76
156
|
end
|
|
77
157
|
|
|
78
158
|
def table_name
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_record"
|
|
4
|
+
require "with_model/invalid_superclass"
|
|
5
|
+
|
|
6
|
+
module WithModel
|
|
7
|
+
# Stands in for a {WithModel::Table} when a model should inherit its
|
|
8
|
+
# superclass's table instead of getting one of its own, as Rails Single Table
|
|
9
|
+
# Inheritance requires. Selected by `table(false)`.
|
|
10
|
+
#
|
|
11
|
+
# In general, direct use of this class should be avoided. Instead use
|
|
12
|
+
# either the {WithModel high-level API} or {WithModel::Model::DSL low-level API}.
|
|
13
|
+
class NullTable
|
|
14
|
+
# @param [Class] superclass The resolved superclass whose table will be
|
|
15
|
+
# inherited.
|
|
16
|
+
# @param [Symbol, String] name The model's name, so that a refusal can say
|
|
17
|
+
# which model it is talking about.
|
|
18
|
+
def initialize(superclass, name)
|
|
19
|
+
@superclass = superclass
|
|
20
|
+
@name = name
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Creates nothing, but refuses a superclass that cannot support single
|
|
24
|
+
# table inheritance.
|
|
25
|
+
#
|
|
26
|
+
# Refusals describe the model's situation rather than the call that produced
|
|
27
|
+
# it. Any expression that evaluates to false selects a NullTable, so there
|
|
28
|
+
# is no call spelling to quote - and in with_model 3.0, omitting `table`
|
|
29
|
+
# will arrive here too.
|
|
30
|
+
#
|
|
31
|
+
# A superclass whose table does not exist *yet* is deliberately allowed: the
|
|
32
|
+
# table may be created later in the example, and a test may legitimately
|
|
33
|
+
# want a model whose table is missing. Active Record raises a clear
|
|
34
|
+
# `StatementInvalid` naming the table if it never appears, so refusing here
|
|
35
|
+
# would only forbid working setups.
|
|
36
|
+
#
|
|
37
|
+
# What is left is {WithModel::InvalidSuperclass}: both refusals are permanent
|
|
38
|
+
# facts about the superclass passed in, not about when it was looked at, so no
|
|
39
|
+
# amount of waiting makes them work.
|
|
40
|
+
def create
|
|
41
|
+
refuse "#{@superclass} has none to inherit" unless table_name?
|
|
42
|
+
|
|
43
|
+
# Nothing more can be checked until there is a table to look at.
|
|
44
|
+
return unless table_exists?
|
|
45
|
+
return if inheritance_column?
|
|
46
|
+
|
|
47
|
+
refuse "#{@superclass}'s table #{@superclass.table_name.inspect} has no " \
|
|
48
|
+
"#{inheritance_column.inspect} column, so Active Record cannot tell its rows " \
|
|
49
|
+
"apart from a subclass's"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Deliberately does nothing: leaving `table_name` unassigned is what lets
|
|
53
|
+
# Active Record's own inheritance supply the superclass's table.
|
|
54
|
+
def configure(klass)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Removes the rows this model wrote, which would otherwise outlive the
|
|
58
|
+
# constant that names them and make the superclass unloadable
|
|
59
|
+
# (`ActiveRecord::SubclassNotFound`).
|
|
60
|
+
#
|
|
61
|
+
# `unscoped` is required because a `default_scope` on the superclass would
|
|
62
|
+
# otherwise hide rows from the delete; the inheritance-column condition is
|
|
63
|
+
# then reapplied explicitly, since `unscoped` also discards the type
|
|
64
|
+
# condition that keeps this from touching the superclass's own rows.
|
|
65
|
+
#
|
|
66
|
+
# A table that does not exist holds no rows to remove, and failing here
|
|
67
|
+
# would fail an example whose body had already passed.
|
|
68
|
+
def teardown(klass)
|
|
69
|
+
return unless klass.table_exists?
|
|
70
|
+
|
|
71
|
+
klass.unscoped.where(klass.inheritance_column => klass.sti_name).delete_all
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Drops nothing; there is no table of our own to drop.
|
|
75
|
+
def destroy
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def inheritance_column = @superclass.inheritance_column
|
|
81
|
+
|
|
82
|
+
# `ActiveRecord::Base` and abstract classes alike have no `table_name`, so
|
|
83
|
+
# this is what "nothing to inherit" actually looks like - `abstract_class?`
|
|
84
|
+
# is false for `ActiveRecord::Base` and so does not describe both.
|
|
85
|
+
def table_name? = @superclass.table_name.present?
|
|
86
|
+
|
|
87
|
+
def table_exists? = @superclass.table_exists?
|
|
88
|
+
|
|
89
|
+
def inheritance_column?
|
|
90
|
+
inheritance_column.present? && @superclass.columns_hash.key?(inheritance_column)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def refuse(problem)
|
|
94
|
+
raise InvalidSuperclass,
|
|
95
|
+
"with_model #{@name.inspect} has no table of its own, but #{problem}"
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
data/lib/with_model/table.rb
CHANGED
|
@@ -25,6 +25,18 @@ module WithModel
|
|
|
25
25
|
connection.create_table(@name, **@options, &@block)
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
+
# Points the model at this table.
|
|
29
|
+
def configure(klass)
|
|
30
|
+
klass.table_name = @name
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Removes everything this table holds by dropping it. The model is not
|
|
34
|
+
# needed, but is accepted so that {WithModel::NullTable} - which does need
|
|
35
|
+
# it - can stand in here.
|
|
36
|
+
def teardown(_klass)
|
|
37
|
+
destroy
|
|
38
|
+
end
|
|
39
|
+
|
|
28
40
|
def destroy
|
|
29
41
|
connection.drop_table(@name)
|
|
30
42
|
end
|
data/lib/with_model/version.rb
CHANGED
data/lib/with_model.rb
CHANGED
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "active_support/deprecation"
|
|
4
|
+
require "with_model/invalid_superclass"
|
|
5
|
+
require "with_model/missing_superclass"
|
|
3
6
|
require "with_model/model"
|
|
4
7
|
require "with_model/model/dsl"
|
|
8
|
+
require "with_model/null_table"
|
|
5
9
|
require "with_model/table"
|
|
6
10
|
require "with_model/version"
|
|
7
11
|
|
|
8
12
|
module WithModel
|
|
9
13
|
class MiniTestLifeCycle < Module
|
|
10
14
|
def initialize(object)
|
|
15
|
+
# Each with_model includes a fresh module, so the last one declared sits
|
|
16
|
+
# earliest in the ancestor chain. Calling super() first means setup runs
|
|
17
|
+
# in declaration order while teardown unwinds in reverse, which is what
|
|
18
|
+
# lets one with_model refer to another declared above it.
|
|
11
19
|
define_method :before_setup do
|
|
12
|
-
object.create
|
|
13
20
|
super() if defined?(super)
|
|
21
|
+
object.create
|
|
14
22
|
end
|
|
15
23
|
|
|
16
24
|
define_method :after_teardown do
|
|
@@ -32,6 +40,14 @@ module WithModel
|
|
|
32
40
|
@runner ||= :rspec
|
|
33
41
|
end
|
|
34
42
|
|
|
43
|
+
# The deprecator used for with_model's own deprecation warnings. Callers can
|
|
44
|
+
# silence it (`WithModel.deprecator.silenced = true`) or escalate it
|
|
45
|
+
# (`behavior = :raise`) while migrating, and Rails applications can register
|
|
46
|
+
# it in `Rails.application.deprecators`.
|
|
47
|
+
def self.deprecator
|
|
48
|
+
@deprecator ||= ActiveSupport::Deprecation.new("3.0", "with_model")
|
|
49
|
+
end
|
|
50
|
+
|
|
35
51
|
# @param [Symbol] name The constant name to assign the model class to.
|
|
36
52
|
# @param scope Passed to `before`/`after` in the test context. RSpec only.
|
|
37
53
|
# @param options Passed to {WithModel::Model#initialize}.
|
|
@@ -41,6 +57,8 @@ module WithModel
|
|
|
41
57
|
model = Model.new name, **options
|
|
42
58
|
dsl = Model::DSL.new model
|
|
43
59
|
dsl.instance_exec(&block) if block
|
|
60
|
+
# caller_locations(1) is this method's caller: the `with_model` line itself.
|
|
61
|
+
WithModel.warn_omitted_table(name, caller_locations(1)) unless model.table_specified?
|
|
44
62
|
|
|
45
63
|
setup_object(model, scope: scope, runner: runner)
|
|
46
64
|
end
|
|
@@ -56,6 +74,26 @@ module WithModel
|
|
|
56
74
|
setup_object(table, scope: scope, runner: runner)
|
|
57
75
|
end
|
|
58
76
|
|
|
77
|
+
# Warns once per call site, at definition time, rather than once per example.
|
|
78
|
+
# The horizon is stated in the message because `Deprecation#warn` does not
|
|
79
|
+
# interpolate the deprecator's `deprecation_horizon`.
|
|
80
|
+
#
|
|
81
|
+
# The callstack has to be handed in. `ActiveSupport::Deprecation` skips Rails'
|
|
82
|
+
# own frames and the standard library when working out where a warning came
|
|
83
|
+
# from, but with_model's frames look like anyone else's to it, so left to itself
|
|
84
|
+
# it reports this file for every omission in a suite.
|
|
85
|
+
#
|
|
86
|
+
# @param callstack Frames to blame, beginning with the caller to report.
|
|
87
|
+
def self.warn_omitted_table(name, callstack)
|
|
88
|
+
deprecator.warn(
|
|
89
|
+
"with_model #{name.inspect} was called without a `table`, which creates a table with only " \
|
|
90
|
+
"an id column. In with_model 3.0 no table will be created. Call `table` (with no " \
|
|
91
|
+
"arguments or an empty block) to keep a table, or `table(false)` to inherit the " \
|
|
92
|
+
"superclass's table (single table inheritance).",
|
|
93
|
+
callstack
|
|
94
|
+
)
|
|
95
|
+
end
|
|
96
|
+
|
|
59
97
|
private
|
|
60
98
|
|
|
61
99
|
# @param [Object] object The new model object instance to create
|
|
@@ -69,6 +69,7 @@ describe "ActiveRecord behaviors" do
|
|
|
69
69
|
with_table :animals
|
|
70
70
|
|
|
71
71
|
with_model :StuffedAnimal do
|
|
72
|
+
table
|
|
72
73
|
model do
|
|
73
74
|
has_many :tea_cups, as: :pet
|
|
74
75
|
end
|
|
@@ -101,7 +102,9 @@ describe "ActiveRecord behaviors" do
|
|
|
101
102
|
end
|
|
102
103
|
end
|
|
103
104
|
|
|
104
|
-
with_model :Country
|
|
105
|
+
with_model :Country do
|
|
106
|
+
table
|
|
107
|
+
end
|
|
105
108
|
|
|
106
109
|
context "in earlier examples" do
|
|
107
110
|
it "works as normal" do
|
data/spec/readme_spec.rb
CHANGED
|
@@ -2,21 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
require "spec_helper"
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
module MyModule; end
|
|
6
|
+
|
|
7
|
+
# A pre-existing model
|
|
8
|
+
class Car < ActiveRecord::Base
|
|
9
|
+
self.abstract_class = true
|
|
10
|
+
end
|
|
9
11
|
|
|
12
|
+
describe "A blog post" do
|
|
10
13
|
with_model :BlogPost do
|
|
11
|
-
# The table block
|
|
14
|
+
# The table block (and an options hash) is passed to Active Record migration’s `create_table`.
|
|
12
15
|
table do |t|
|
|
13
16
|
t.string :title
|
|
14
17
|
t.timestamps null: false
|
|
15
18
|
end
|
|
16
19
|
|
|
17
|
-
# The model block
|
|
20
|
+
# The model block is the Active Record model’s class body.
|
|
18
21
|
model do
|
|
19
22
|
include MyModule
|
|
23
|
+
|
|
20
24
|
has_many :comments
|
|
21
25
|
validates_presence_of :title
|
|
22
26
|
|
|
@@ -70,15 +74,15 @@ describe "A blog post" do
|
|
|
70
74
|
expect(record.comments.count).to eq 1
|
|
71
75
|
end
|
|
72
76
|
|
|
73
|
-
# with_model classes can have inheritance.
|
|
74
|
-
|
|
75
|
-
|
|
77
|
+
# with_model classes can have inheritance. Car is abstract, so it has no table
|
|
78
|
+
# and Ford gets one of its own. To inherit a concrete superclass's table
|
|
79
|
+
# instead, see "Single table inheritance" below.
|
|
80
|
+
with_model :Ford, superclass: Car do
|
|
81
|
+
table
|
|
76
82
|
end
|
|
77
83
|
|
|
78
|
-
with_model :Ford, superclass: Car
|
|
79
|
-
|
|
80
84
|
it "has a specified superclass" do
|
|
81
|
-
expect(Ford
|
|
85
|
+
expect(Ford.new).to be_a(Car)
|
|
82
86
|
end
|
|
83
87
|
end
|
|
84
88
|
|
|
@@ -116,3 +120,48 @@ describe "with table options" do
|
|
|
116
120
|
expect(WithOptions.columns.map(&:name)).not_to include("id")
|
|
117
121
|
end
|
|
118
122
|
end
|
|
123
|
+
|
|
124
|
+
describe "with_model supports Single Table Inheritance" do
|
|
125
|
+
with_model :Sandwich do
|
|
126
|
+
table do |t|
|
|
127
|
+
t.string "type"
|
|
128
|
+
t.string "bread"
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
with_model :ChunkyBacon, superclass: :Sandwich do
|
|
133
|
+
table(false)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
it "shares the superclass's table" do
|
|
137
|
+
expect(ChunkyBacon.table_name).to eq Sandwich.table_name
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
it "stores its own type" do
|
|
141
|
+
sandwich = ChunkyBacon.create!(bread: "rye")
|
|
142
|
+
|
|
143
|
+
expect(sandwich.reload.type).to eq "ChunkyBacon"
|
|
144
|
+
expect(Sandwich.first).to be_a ChunkyBacon
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
describe "with_model supports foreign keys" do
|
|
149
|
+
with_model :Author do
|
|
150
|
+
table
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
with_model :Book do
|
|
154
|
+
table do |t|
|
|
155
|
+
t.references :author, foreign_key: {to_table: Author.table_name}
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
model do
|
|
159
|
+
belongs_to :author
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
it "has a foreign key" do
|
|
164
|
+
expect { Book.create!(author_id: 0) }
|
|
165
|
+
.to raise_error ActiveRecord::InvalidForeignKey
|
|
166
|
+
end
|
|
167
|
+
end
|