Minitest-Sequel
Coverage: Currently at 99.1%
Minitest assertions to speed-up development and testing of Sequel database setups.
The general hope is that this gem will contain a variety of useful assertions in all areas of testing Sequel database code within your apps, gems, etc.
Please help out with missing features / functionality.
Model Definitions
#assert_have_column (:model, :attribute, :opts, :msg)
spec: _(model).must_have_column(:attribute, :opts, :msg)
Conveniently test your Model definitions as follows:
let(:m) { Post.first }
it { assert_have_column(m, :title, type: :string, db_type: 'varchar(250)', allow_null: :false) }
it { _(m).must_have_column(:title, type: :string, allow_null: :false) }
it { _(m).must_have_column(:title, { type: :string, allow_null: :false }, "Custom messsage") }
# definition of args
# assert_have_column(
# <instance>,
# <column_name>,
# <options>,
# <custom_error_message>
# )
The #assert_have_column()
method first tests if the column name is defined in the Model and then
checks all passed options.
The following options are valid and checked:
:type
:db_type
:allow_null
:max_length
:default
:primary_key
:auto_increment
In the event the specs differ from the actual database implementation an extensive error message with the differing option(s) is provided to help speed up debugging the issue:
Expected Post model to have column: :title with: \
{
type: 'string',
db_type: 'varchar(250)',
allow_null: 'false'
}
but found: { db_type: 'varchar(255)' }
Note
To test options with a value that is either nil
, true
or false
, please use :nil
, :false
or :true
and provide numbers as 'strings' instead, ie: '1'
instead of 1
.
Model Associations
Conveniently test model associations quickly and easily with these Minitest assertions:
#assert_association_one_to_one
#assert_association_one_to_many
#assert_association_many_to_one
#assert_association_many_to_many
#assert_association
:one_to_one
association
A model defined with an association like this:
class Post < Sequel::Model
one_to_one :first_comment, class: :Comment, order: :id
end
Can be easily and quickly tested with #assert_association_one_to_one()
like this:
let(:m) { Post.first }
it { assert_association_one_to_one(m, :first_comment)
# or
it { _(m).must_have_one_to_one_association(:first_comment) }
# definition of args
assert_association_one_to_one(
<model_instance>,
<association_name>, # ie: :first_comment
<options>,
<custom_error_message>
)
In the event of errors an extensive error message is provided:
# example error message
Expected Author to have a :one_to_one association :key_posts but no association ':key_posts' \
was found - available associations are: [ \
{
:attribute=>:posts,
:type=>:one_to_many,
:class=>:Post,
:keys=>[:author_id]},
{
:attribute=>:key_post,
:type=>:one_to_one,
:class=>:Post,
:keys=>[:author_id]
}
]
:one_to_many
association
A model defined with an association like this:
class Post < Sequel::Model
one_to_many :comments
end
Can be easily and quickly tested with #assert_association_one_to_many()
like this:
let(:m) { Post.first }
it { assert_association_one_to_many(m, :comments) }
# or
it { _(m).must_have_one_to_many_association(:comments) }
As above the assertion provides an extensive error message if something is wrong.
:many_to_one
association
A model defined with an association like this:
class Post < Sequel::Model
many_to_one :author
end
Can be easily and quickly tested with #assert_association_many_to_one()
like this:
let(:m) { Post.first }
it { assert_association_many_to_one(m, :author) }
# or
it { _(m).must_have_many_to_one_association(:author) }
As above the assertion provides an extensive error message if something is wrong.
:many_to_many
association
A model defined with an association like this:
class Post < Sequel::Model
many_to_many :categories
end
Can be easily and quickly tested with #assert_association_many_to_many()
like this:
let(:m) { Post.first }
it { assert_association_many_to_many(m, :categories) }
# or
it { _(m).must_have_many_to_many_association(:categories) }
If something is wrong an extensive error message is provided:
Expected Category to have a :many_to_many association :posts with given options: \
{:class_name=>'Posts'} but should be {:class_name=>'Post' }
or
Expected Category to have a :many_to_many association :post but no association ':post' was found \
- available associations are: [ \
{
:attribute=>:posts,
:type=>:many_to_many,
:class=>:Post,
:join_table=>:categories_posts,
:left_keys=>[:category_id],
:right_keys=>[:post_id]
}
]
#assert_association(:model, :type, :attribute, :options, :msg)
spec: _(model).must_have_association(:type, :attribute, :options, :msg)
if the above assertion methods are insufficient, you can use the base
assert_association
method instead.
it "should have a :one_through_one association" do
assert_association(Post, :one_through_one, :author)
# or
_(Post).must_have_association(:one_through_one, :author)
end
# definition of args
assert_association(
<model_class>,
<association_type>,
<association_name>,
<options>,
<custom_error_message>
)
Model Validations
If you are using the recommended :validation_class_methods
plugin in your app, the following
instance validation methods are supported:
#assert_validates_presence()
#assert_validates_exact_length()
#assert_validates_length_range()
#assert_validates_max_length()
#assert_validates_min_length()
#assert_validates_format()
#assert_validates_inclusion()
#assert_validates_integer()
#assert_validates_numericality()
#assert_validates_uniqueness()
#assert_validates_acceptance()
#assert_validates_confirmation()
With all valid options checked
#assert_validates_presence(:model, :attribute, :opts, :msg)
alias: #assert_validates_presence_of(:attribute, :opts, :msg)
Test for validating presence of a model attribute
let(:m) { Post.first }
it { assert_validates_presence(m, :title) }
# or
it { _(m).must_validate_presence_of(:title, { message: '...' }) }
#assert_validates_length(:model, :attribute, :opts, :msg)
alias #assert_validates_length_of
Test for validating the length of a model's attribute.
Available options:
-
:message - The message to use (no default, overrides :nil_message, :too_long, :too_short, and :wrong_length options if present)
-
:nil_message - The message to use use if :maximum option is used and the value is nil (default: 'is not present')
-
:too_long - The message to use use if it the value is too long (default: 'is too long')
-
:too_short - The message to use use if it the value is too short (default: 'is too short')
-
:wrong_length - The message to use use if it the value is not valid (default: 'is the wrong length')
Size related options:
-
:is - The exact size required for the value to be valid (no default)
-
:minimum - The minimum size allowed for the value (no default)
-
:maximum - The maximum size allowed for the value (no default)
-
:within - The array/range that must include the size of the value for it to be valid (no default)
let(:m) { Post.first }
it { assert_validates_length(m, :title, { maximum: 12 }) }
# or
it { _(m).must_validate_length_of(:title, { within: 4..12 }) }
#assert_validates_exact_length(:model, :attribute, :exact_length, :opts, :msg)
alias: #assert_validates_exact_length_of
Test for validating the exact length of a model's attribute.
let(:m) { Post.first }
it { assert_validates_exact_length(m, :title, 12, { message: '...' }) }
# or
it { _(m).must_validate_exact_length_of(:title, 12, { message: '...' }) }
#assert_validates_length_range(:model, :attribute, :range, :opts, :msg)
alias: #assert_validates_length_range_of
Test for validating the exact length of a model's attribute.
let(:m) { Post.first }
it { assert_validates_length_range(m, :title, 4..12, { message: '...' }) }
# or
it { _(m).must_validate_length_range_of(:title, 4..12, { message: '...' }) }
#assert_validates_max_length(:model, :attribute, :max_length, :opts, :msg)
alias: #assert_validates_max_length_of
Test for validating the maximum length of a model's attribute.
let(:m) { Post.first }
it { assert_validates_max_length(m, :title, 12, { message: '...' }) }
# or
it { _(m).must_validate_max_length_of(:title, 12, { message: '...' }) }
#assert_validates_min_length(:model, :attribute, :min_length, :opts, :msg)
alias: #assert_validates_min_length_of
Test for validating the minimum length of a model's attribute.
let(:m) { Post.first }
it { assert_validates_min_length(m, :title, 12, { message: '...' }) }
# or
it { _(m).must_validate_min_length_of(:title, 12, { message: '...' }) }
#assert_validates_format(:model, :attribute, :opts, :msg)
alias: #assert_validates_format_of
Test for validating the format of a model's attribute with a regexp.
let(:m) { Post.first }
it { assert_validates_format(m, :title, { with: /[a-z+]/ }) }
# or
it { _(m).must_validate_format_of(:title, { with: /[a-z]+/ }) }
#assert_validates_inclusion(:model, :attribute, :opts, :msg)
alias: #assert_validates_inclusion_of
Test for validating that a model's attribute is within a specified range or set of values.
let(:m) { Post.first }
it { assert_validates_inclusion(m, :status, { in: [:a, :b, :c] }) }
# or
it { _(m).must_validate_inclusion_of(:status, { in: [:a, :b, :c] }) }
#assert_validates_integer(:model, :attribute, :opts, :msg)
alias: none
Test for validating that a a model's attribute is an integer.
let(:m) { Post.first }
it { assert_validates_integer(m, :author_id, { message: '...' }) }
# or
it { _(m).must_validate_integer_of(:author_id, { message: '...' }) }
#assert_validates_numericality(:model, :attribute, :opts, :msg)
alias: #assert_validates_numericality_of
Test for validating that a model's attribute is numeric (number).
let(:m) { Post.first }
it { assert_validates_numericality(m, :author_id, { message: '...' }) }
# or
it { _(m).must_validate_numericality_of(:author_id, { message: '...' }) }
#assert_validates_uniqueness(:model, :attribute, :opts, :msg)
alias: #assert_validates_uniqueness_of
Test for validating that a model's attribute is unique.
let(:m) { Post.first }
it { assert_validates_uniqueness(m, :urlslug, { message: '...' }) }
# or
it { _(m).must_validate_uniqueness_of(:urlslug, { message: '...' }) }
#assert_validates_acceptance(:model, :attribute, :opts, :msg)
alias: #assert_validates_acceptance_of
Test for validating the acceptance of a model's attribute.
let(:m) { Order.new }
it { assert_validates_acceptance(m, :toc, { message: '...' }) }
# or
it { _(m).must_validate_acceptance_of(:toc, { message: '...' }) }
#assert_validates_confirmation(:model, :attribute, :opts, :msg)
alias: #assert_validates_confirmation_of
Test for validating the confirmation of a model's attribute.
let(:m) { User.new }
it { assert_validates_confirmation(m, :password, { message: '...' }) }
# or
it { _(m).must_validate_confirmation_of(:password, { message: '...' }) }
Each validation assertion have a responding negative test, ie: refute_validate_presence()
Usage Example
A model defined with validations like this:
class Post < Sequel::Model
plugin :validation_helpers
def validate
super
validates_presence(:title)
validates_format(/\w+/, :title)
end
end
Can be quickly tested like this:
# <snip...>
let(:m) { Post.first }
it "should validate presence of :title column" do
assert_validates_presence(m, :title)
# or
_(m).must_validate_presence_of(:title)
end
it "should validate format of :title column with regexp" do
assert_validates_format(m, :title, /\w+/)
# or
_(m).must_validate_format_of(:title, /\w+/)
end
Plugins
This gem also contains a collection of "helpers" that aid working with Sequel models:
#assert_timestamped_model(:model, :opts, :msg)
Quickly test if a model class is timestamped with .plugin(:timestamps)
with
Sequel-Timestamps
Note
The test examples below uses the minitest-assert_errors package.
# Declared locally in the Model
class Comment < Sequel::Model
plugin(:timestamps)
end
assert_no_error { assert_timestamped_model(Comment) }
# on a non-timestamped model
class Post < Sequel::Model; end
msg = /Not a \.plugin\(:timestamps\) model, available plugins are/
assert_error_raised(msg) { assert_timestamped_model(Post) }
Tip
You can also pass attributes to the created model in the tests via the opts
hash like this:
assert_no_error do
assert_timestamped_model(Comment, {body: "I think...", email: "e@email.com"})
end
Timestamps can be declared globally for all models via Sequel::Model.plugin(:timestamps)
before
the models are migrated.
#assert_timestamped_model_instance(:model, :opts, :msg)
Test if a model instance is timestamped with the .plugin(:timestamps) via Sequel-Timestamps
let(:m) { Post.create(title: "Dummy") }
assert_no_error { assert_timestamped_model_instance(m) }
You can also test if an updated record is correctly timestamped
m.title = "Updated"
m.save
assert_no_error do
assert_timestamped_model_instance(m, updated_record: true)
end
Or alternatively test if an updated record is wrongly timestamped
let(:m) { Post.create(title: "Dummy", updated_at: Time.now) }
msg = /expected #.updated_at to be NIL on new record/
assert_error_raised(msg) do
assert_timestamped_model_instance(m, updated_record: false)
end
#assert_paranoid_model(:model, :opts, :msg)
Test if a model class is paranoid with .plugin(:paranoid) via Sequel-Paranoid
# Declared locally in the Model
class Comment < Sequel::Model
plugin(:paranoid)
end
assert_no_error { assert_paranoid_model(Comment) }
# on a non-paranoid model
class Post < Sequel::Model; end
msg = /Not a plugin\(:paranoid\) model, available plugins are/
assert_error_raised(msg) { assert_paranoid_model(Post) }
Tip
You can also pass attributes to the created model in the tests via the opts
hash like this:
assert_no_error do
assert_timestamped_model(Comment, { body: "I think...", email: "e@email.com" })
end
#refute_timestamped_model(:model, :msg)
Test to ensure a model is NOT declared with .plugin(:timestamps) using Sequel-Timestamps
Test if a model class is paranoid with .plugin(:paranoid) via Sequel-Paranoid
class Comment < Sequel::Model
plugin(:timestamps)
end
msg = /expected Comment to NOT be a :timestamped model, but it was/
assert_error_raised(msg) do
refute_timestamped_model(Comment)
end
# on a non-timestamped model
class Post < Sequel::Model; end
it { refute_timestamped_model(Post) }
#refute_paranoid_model(:model, :msg)
Test to ensure a model is NOT declared with .plugin(:paranoid) using Sequel-Paranoid
class Comment < Sequel::Model
plugin(:paranoid)
end
msg = /expected Comment to NOT be a :paranoid model, but it was/
assert_error_raised(msg) { refute_paranoid_model(Comment) }
# on a non-paranoid model
class Post < Sequel::Model; end
it { refute_paranoid_model(Post) }
Miscellaneous Helpers
This gem also contains a collection of "helpers" that aid working with Sequel models:
#ensure_working_CRUD(:model, :attribute)
Enables quick tests to ensure that the basic CRUD functionality is working correctly for a Model
ensure_working_CRUD(User, :name)
Note
- the passed
:model
argument must be the actual Model class and NOT a string or symbol - the passed attribute
:attribute
must be a String attribute or the tests will fail
Dependencies
This test depends upon being able to create a new model instance for each test via using
Sequel Factory's #make()
method
Installation
Add this line to your application's Gemfile:
gem 'minitest-sequel'
And then execute:
bundle
Or install it yourself as:
gem install minitest-sequel
Within Your Project
In your project's spec/spec_helper.rb
or test/test_helper.rb
file ensure
the following code is present:
gem 'minitest'
require 'minitest/autorun'
require 'minitest/sequel' # NB!! must be loaded after minitest/autorun
require 'sqlite3' # using sqlite for tests
# The preferred default validations plugin, which uses class-level methods.
Sequel::Model.plugin(:validation_class_methods)
# connect to database
DB = Sequel.sqlite # :memory
## add migrations and seeds below
DB.create_table(:posts) do
primary_key :id
# <snip...>
end
# <snip...>
Then in your tests you should be good to go when using the sequel assertions.
Development
After checking out the repo, run bundle install
to install all dependencies.
Then, run bundle exec rake spec
to run the tests.
To install this gem onto your local machine, run bundle exec rake install
.
To release a new version:
- update the version number in
version.rb
; - run
bundle exec rake release
, which will create a git tag for the version, - push git commits and tags
- push the
.gem
file to rubygems.org.
Contributing
Bug reports and pull requests are welcome on GitHub at Issues.
This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.
License
The gem is available as open source under the terms of the MIT License.