- Rspec::Grape::Entity
This gem, inspired by rspec-its, provides the its_exposure
and describe_exposure
short-hand to specify the expected configuration of a given GrapeEntity
exposure.
Add this line to your application's Gemfile:
gem 'rspec-grape-entity', group: :test
And then execute:
$ bundle install
Or install it yourself as:
$ gem install rspec-grape-entity
Include the RSpec::Grape::Entity
matchers automatically by defining the described type
as :grape_entity
or by
including the RSpec::Grape::Entity::DSL
directly.
# Automatic
describe MyEntity, type: :grape_entity do
# ... tests
end
# Manually
describe MyEntity do
include RSpec::Grape::Entity::DSL
# ... tests
end
Use the describe_exposure
or its_exposure
methods to generate a nested example group that specifies the expected
exposure of an attribute of the entity using should
, should_not
or is_expected
.
describe_exposure
and its_exposure
accepts a symbol or string.
describe_exposure :id do
#...
end
describe_exposure 'id' do
#...
end
its_exposure(:id) { is_expected.not_to be_nil }
its_exposure('id') { is_expected.not_to be_nil }
You can use a string with dots to specify a nested exposure attribute.
describe_exposure 'status.value' do
#...
end
describe_exposure 'status.changed_at' do
#...
end
its_exposure('status.value') { is_expected.not_to be_nil }
its_exposure('status.changed_at') { is_expected.not_to be_nil }
class TestEntity < Grape::Entity
root "test_items", "test_item"
format_with :iso_timestamp, &:iso8601
expose :id, documentation: { type: Integer, desc: "The record id" }
expose :record_status, as: :status, if: :all
expose :user, safe: true, using: UserEntity, if: { type: :admin }
expose :custom_data, merge: true do |_, _|
{
foo: :bar
}
end
expose :permissions, override: true do
expose :read
expose :update
expose :destroy
end
expose :created_at, format_with: ->(date) { date.iso8601 }, if: ->(instance, _) { instance.has_date }
expose :updated_at, format_with: :iso_timestamp
end
RSpec.describe TestEntity, type: :grape_entity do
let(:object) do
OpenStruct.new id: 1,
record_status: "active",
user: OpenStruct.new,
read: true,
update: false,
destroy: false,
created_at: Time.utc(2022, 1, 1, 15, 0, 0),
updated_at: Time.now,
has_date: true
end
it { expect(described_class).to have_root("test_items").with_singular("test_item") }
context "when using its_exposure" do
let(:object_without_date) { OpenStruct.new has_date: false }
its_exposure(:id) { is_expected.to be_a_delegator_exposure }
its_exposure(:id) { is_expected.to include_documentation type: Integer, desc: "The record id" }
its_exposure(:id) { is_expected.not_to be_safe }
its_exposure(:id) { is_expected.not_to be_merged }
its_exposure(:id) { is_expected.not_to override_exposure }
its_exposure(:record_status) { is_expected.to be_a_delegator_exposure }
its_exposure(:record_status) { is_expected.to have_key :status }
its_exposure(:record_status) { is_expected.to have_conditions_met(object).with_options(all: :something) }
its_exposure(:record_status) { is_expected.to_not have_conditions_met object }
its_exposure(:record_status) { is_expected.not_to be_safe }
its_exposure(:record_status) { is_expected.not_to be_merged }
its_exposure(:record_status) { is_expected.not_to override_exposure }
its_exposure(:user) { is_expected.to be_a_represent_exposure }
its_exposure(:user) { is_expected.to be_using_class UserEntity }
its_exposure(:user) { is_expected.to have_conditions_met(object).with_options(type: :admin) }
its_exposure(:user) { is_expected.not_to have_conditions_met(object).with_options(type: :user) }
its_exposure(:user) { is_expected.not_to have_conditions_met object }
its_exposure(:custom_data) { is_expected.to be_a_block_exposure }
its_exposure(:custom_data) { is_expected.not_to be_safe }
its_exposure(:custom_data) { is_expected.to be_merged }
its_exposure(:custom_data) { is_expected.not_to override_exposure }
its_exposure(:permissions) { is_expected.to be_a_nesting_exposure }
its_exposure(:permissions) { is_expected.not_to be_safe }
its_exposure(:permissions) { is_expected.not_to be_merged }
its_exposure(:permissions) { is_expected.to override_exposure }
its_exposure("permissions.read") { is_expected.to be_a_delegator_exposure }
its_exposure("permissions.read") { is_expected.not_to be_safe }
its_exposure("permissions.read") { is_expected.not_to be_merged }
its_exposure("permissions.read") { is_expected.not_to override_exposure }
its_exposure("permissions.update") { is_expected.to be_a_delegator_exposure }
its_exposure("permissions.update") { is_expected.not_to be_safe }
its_exposure("permissions.update") { is_expected.not_to be_merged }
its_exposure("permissions.update") { is_expected.not_to override_exposure }
its_exposure("permissions.destroy") { is_expected.to be_a_delegator_exposure }
its_exposure("permissions.destroy") { is_expected.not_to be_safe }
its_exposure("permissions.destroy") { is_expected.not_to be_merged }
its_exposure("permissions.destroy") { is_expected.not_to override_exposure }
its_exposure("created_at") { is_expected.to be_a_formatter_block_exposure }
its_exposure("created_at") { is_expected.to have_formatting("2022-01-01T15:00:00Z").with_object(object) }
its_exposure("created_at") { is_expected.not_to be_safe }
its_exposure("created_at") { is_expected.not_to be_merged }
its_exposure("created_at") { is_expected.not_to override_exposure }
its_exposure("created_at") { is_expected.to have_conditions_met object }
its_exposure("created_at") { is_expected.to_not have_conditions_met object_without_date }
end
context "when using describe_exposure" do
shared_examples "has permissions" do |permission|
describe_exposure "permissions.#{permission}" do
it { is_expected.to be_a_delegator_exposure }
it { is_expected.not_to be_safe }
it { is_expected.not_to be_merged }
it { is_expected.not_to override_exposure }
end
end
describe_exposure :id do
it { is_expected.to be_a_delegator_exposure }
it { is_expected.to include_documentation type: Integer, desc: "The record id" }
it { is_expected.not_to be_safe }
it { is_expected.not_to be_merged }
it { is_expected.not_to override_exposure }
end
describe_exposure :record_status do
it { is_expected.to be_a_delegator_exposure }
it { is_expected.to have_key :status }
it { is_expected.to have_conditions_met(object).with_options(all: :something) }
it { is_expected.to_not have_conditions_met object }
it { is_expected.not_to be_safe }
it { is_expected.not_to be_merged }
it { is_expected.not_to override_exposure }
end
describe_exposure :user do
it { is_expected.to be_a_represent_exposure }
it { is_expected.to be_using_class UserEntity }
context "when type is an admin" do
it { is_expected.to have_conditions_met(object).with_options(type: :admin) }
end
context "when type is not an admin" do
it { is_expected.not_to have_conditions_met(object).with_options(type: :user) }
end
context "when no type is declared" do
it { is_expected.not_to have_conditions_met object }
end
end
describe_exposure :custom_data do
it { is_expected.to be_a_block_exposure }
it { is_expected.not_to be_safe }
it { is_expected.to be_merged }
it { is_expected.not_to override_exposure }
end
describe_exposure :permissions do
it { is_expected.to be_a_nesting_exposure }
it { is_expected.not_to be_safe }
it { is_expected.not_to be_merged }
it { is_expected.to override_exposure }
end
it_behaves_like "has permissions", "read"
it_behaves_like "has permissions", "update"
it_behaves_like "has permissions", "destroy"
describe_exposure :created_at do
it { is_expected.to be_a_formatter_block_exposure }
it { is_expected.to have_formatting("2022-01-01T15:00:00Z").with_object(object) }
it { is_expected.not_to be_safe }
it { is_expected.not_to be_merged }
it { is_expected.not_to override_exposure }
context "when has date" do
it { is_expected.to have_conditions_met object }
end
context "when does not have date" do
let(:object) { OpenStruct.new has_date: false }
it { is_expected.not_to have_conditions_met object }
end
end
end
end
Test that only the plural definition is set.
class Entity < Grape::Entity
root "items"
end
RSpec.describe Entity, type: :grape_entity do
it { expect(described_class).to have_root "items" }
end
Chain singular(singular)
to specify the expected singular definition.
class Entity < Grape::Entity
root "items", "item"
end
RSpec.describe Entity, type: :grape_entity do
it { expect(described_class).to have_root("items").singular("item") }
end
Use should
, should_not
, will
, will_not
, is_expected
to specify the expected value of the exposed attribute.
Test that the exposed attribute is a block exposure.
class Entity < Grape::Entity
expose :block do |item, options|
#...something
end
end
RSpec.describe Entity, type: :grape_entity do
describe_exposure :block do
it { is_expected.to be_a_block_exposure }
end
# Or
its_exposure(:block) { is_expected.to be_a_block_exposure }
end
Test that the exposed attribute is a delegator exposure.
class Entity < Grape::Entity
expose :id
end
RSpec.describe Entity, type: :grape_entity do
describe_exposure :id do
it { is_expected.to be_a_delegator_exposure }
end
# Or
its_exposure(:id) { is_expected.to be_a_delegator_exposure }
end
Test that the exposed attribute is a formatter exposure using a symbol.
class Entity < Grape::Entity
expose :created_at, format_with: :iso_timestamp
end
RSpec.describe Entity, type: :grape_entity do
describe_exposure :created_at do
it { is_expected.to be_a_formatter_exposure }
end
# Or
its_exposure(:created_at) { is_expected.to be_a_formatter_exposure }
end
Test that the exposed attribute is a formatter exposure using a proc.
class Entity < Grape::Entity
expose :created_at, format_with: ->(date) { date.iso8601 }
end
RSpec.describe Entity, type: :grape_entity do
describe_exposure :created_at do
it { is_expected.to be_a_formatter_block_exposure }
end
# Or
its_exposure(:created_at) { is_expected.to be_a_formatter_block_exposure }
end
Test that the exposed attribute is a nesting exposure.
class Entity < Grape::Entity
expose :status do
expose :status, as: :value
expose :changed_at, as: :status_changed_at
end
end
RSpec.describe Entity, type: :grape_entity do
describe_exposure :status do
it { is_expected.to be_a_nesting_exposure }
end
# Or
its_exposure(:status) { is_expected.to be_a_nesting_exposure }
end
Test that the exposed attribute is a represented exposure.
class Entity < Grape::Entity
expose :user, using: UserEntity
end
RSpec.describe Entity, type: :grape_entity do
describe_exposure :user do
it { is_expected.to be_a_represent_exposure }
end
# Or
its_exposure(:user) { is_expected.to be_a_represent_exposure }
end
Test that the exposed attribute is merged into the parent.
class Entity < Grape::Entity
expose :user, using: UserEntity, merge: true
end
RSpec.describe Entity, type: :grape_entity do
describe_exposure :user do
it { is_expected.to be_merged }
end
# Or
its_exposure(:user) { is_expected.to be_merged }
end
Test that the exposed attribute is safe.
class Entity < Grape::Entity
expose :user, using: UserEntity, safe: true
end
RSpec.describe Entity, type: :grape_entity do
describe_exposure :user do
it { is_expected.to be_safe }
end
# Or
its_exposure(:user) { is_expected.to be_safe }
end
Test that the exposed attribute uses an entity presenter.
class Entity < Grape::Entity
expose :user, using: UserEntity
end
RSpec.describe Entity, type: :grape_entity do
describe_exposure :user do
it { is_expected.to be_using_class(UserEntity) }
end
# Or
its_exposure(:user) { is_expected.to be_using_class(UserEntity) }
end
Test that the exposed attribute conditions are met with a given object.
class Entity < Grape::Entity
expose :protected, if: ->(instance) { instance.is_a?(Admin) }
end
RSpec.describe Entity, type: :grape_entity do
let(:admin) { Admin.new }
let(:user) { User.new }
describe_exposure :protected do
it { is_expected.to have_conditions_met(admin) }
it { is_expected.not_to have_conditions_met(user) }
end
# Or
its_exposure(:protected) { is_expected.to have_conditions_met(admin) }
its_exposure(:protected) { is_expected.not_to have_conditions_met(user) }
end
Chain with with_options(options)
to pass an options hash to the attribute exposure's conditions.
class Entity < Grape::Entity
expose :status, if: :all
expose :secret, if: { type: :admin }
end
RSpec.describe Entity, type: :grape_entity do
let(:admin) { Admin.new }
describe_exposure :status do
it { is_expected.to have_conditions_met(admin).with_options(all: true) }
it { is_expected.not_to have_conditions_met(admin) }
end
describe_exposure :secret do
it { is_expected.to have_conditions_met(admin).with_options(type: :admin) }
it { is_expected.not_to have_conditions_met(admin).with_options(type: :user) }
it { is_expected.not_to have_conditions_met(admin) }
end
# Or
its_exposure(:status) { is_expected.to have_conditions_met(admin).with_options(all: true) }
its_exposure(:status) { is_expected.not_to have_conditions_met(admin) }
its_exposure(:secret) { is_expected.to have_conditions_met(admin).with_options(type: :admin) }
its_exposure(:secret) { is_expected.not_to have_conditions_met(admin).with_options(type: :user) }
its_exposure(:secret) { is_expected.not_to have_conditions_met(admin) }
end
Test that the exposed attribute uses a given formatter.
class Entity < Grape::Entity
expose :created_at, format_with: :iso_timestamp
end
RSpec.describe Entity, type: :grape_entity do
describe_exposure :created_at do
it { is_expected.to have_formatting :iso_timestamp }
end
# Or
its_exposure(:created_at) { is_expected.to have_formatting :iso_timestamp }
end
Chain with with_object(object)
when the formatter option is a Proc.
class Entity < Grape::Entity
expose :created_at, format_with: ->(date) { |date| date.iso8601 }
end
RSpec.describe Entity, type: :grape_entity do
let(:date) { Time.utc 2022, 1, 22, 17, 0, 0 }
describe_exposure :created_at do
it { is_expected.to have_formatting("2022-01-22T17:00:00Z").with_object(date) }
end
# Or
its_exposure(:created_at) { is_expected.to have_formatting("2022-01-22T17:00:00Z").with_object(date) }
end
Test that the exposed attribute uses an alias.
class Entity < Grape::Entity
expose :to_param, as: :id
end
RSpec.describe Entity, type: :grape_entity do
describe_exposure :to_param do
it { is_expected.to have_key :id }
end
# Or
its_exposure(:to_param) { is_expected.to have_key :id }
end
Test that the exposed attribute has the included documentation.
class Entity < Grape::Entity
expose :id, documentation: { type: Integer, desc: "Entity id" }
end
RSpec.describe Entity, type: :grape_entity do
describe_exposure :id do
it { is_expected.to have_documentation :type, :desc }
it { is_expected.to have_documentation :type }
it { is_expected.to have_documentation :desc }
it { is_expected.to have_documentation type: Integer }
it { is_expected.to have_documentation desc: "Entity id" }
it { is_expected.to have_documentation type: Integer, desc: "Entity id" }
end
# Or
its_exposure(:id) { is_expected.to have_documentation :type, :desc }
its_exposure(:id) { is_expected.to have_documentation :type }
its_exposure(:id) { is_expected.to have_documentation :desc }
its_exposure(:id) { is_expected.to have_documentation type: Integer }
its_exposure(:id) { is_expected.to have_documentation desc: "Entity id" }
its_exposure(:id) { is_expected.to have_documentation type: Integer, desc: "Entity id" }
end
Test that the exposed attribute uses an alias.
class BaseEntity < Grape::Entity
expose :id
end
class Entity < BaseEntity
expose :id, override: true do |instance, options|
#...
end
end
RSpec.describe Entity, type: :grape_entity do
describe_exposure :id do
it { is_expected.to override_exposure }
end
# Or
its_exposure(:to_param) { is_expected.to override_exposure }
end
After checking out the repo, run bin/setup
to install dependencies. Then, run rake spec
to run the tests. You can also run bin/console
for an interactive prompt that will allow you to experiment.
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
, and then run bundle exec rake release
, which will create a git tag for the version, push git commits and the created tag, and push the .gem
file to rubygems.org.
Bug reports and pull requests are welcome on GitHub at https://github.com/jefawks3/rspec-grape-entity.
- Fork it
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Add some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create new Pull Request
The gem is available as open source under the terms of the MIT License.