├── .rspec ├── .travis.yml ├── lib ├── smart_rspec │ ├── version.rb │ ├── matchers │ │ ├── other_matchers.rb │ │ ├── be_matchers.rb │ │ └── json_api_matchers.rb │ ├── matchers.rb │ ├── macros.rb │ └── support │ │ ├── regexes.rb │ │ ├── controller │ │ └── response.rb │ │ └── model │ │ ├── expectations.rb │ │ └── assertions.rb └── smart_rspec.rb ├── Gemfile ├── Rakefile ├── spec ├── spec_helper.rb ├── fixtures │ ├── response.rb │ ├── users.json │ └── user.rb └── smart_rspec │ ├── macros_spec.rb │ └── matchers_spec.rb ├── .gitignore ├── LICENSE.txt ├── smart_rspec.gemspec └── README.md /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.3.3 4 | -------------------------------------------------------------------------------- /lib/smart_rspec/version.rb: -------------------------------------------------------------------------------- 1 | module SmartRspec 2 | VERSION = '0.2.0'.freeze 3 | end 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in smart_rspec.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rspec/core/rake_task' 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | task default: :spec 6 | 7 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/setup' 2 | require 'rubygems' 3 | require 'smart_rspec' 4 | require 'faker' 5 | require 'fixtures/user' 6 | require 'fixtures/response' 7 | 8 | include Fixtures 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | *.bundle 11 | *.so 12 | *.sw* 13 | *.o 14 | *.a 15 | mkmf.log 16 | -------------------------------------------------------------------------------- /spec/fixtures/response.rb: -------------------------------------------------------------------------------- 1 | module Fixtures 2 | class Response 3 | def initialize 4 | @file = File.read('spec/fixtures/users.json') 5 | end 6 | 7 | def body 8 | @file 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/smart_rspec/matchers/other_matchers.rb: -------------------------------------------------------------------------------- 1 | module SmartRspec 2 | module Matchers 3 | module OtherMatchers 4 | extend RSpec::Matchers::DSL 5 | 6 | matcher :have_error_on do |attr| 7 | match { |actual| actual.errors.keys.include?(attr) } 8 | end 9 | 10 | matcher :include_items do |*items| 11 | match { |actual| (items.flatten(1) - actual).empty? } 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/smart_rspec.rb: -------------------------------------------------------------------------------- 1 | require 'active_support/concern' 2 | require 'rspec/collection_matchers' 3 | require 'rspec/matchers' 4 | 5 | %w(macros matchers support/model/expectations).each { |f| require "smart_rspec/#{f}" } 6 | 7 | include SmartRspec::Matchers 8 | 9 | module SmartRspec 10 | extend ActiveSupport::Concern 11 | 12 | included do 13 | include SmartRspec::Support::Model::Expectations 14 | end 15 | 16 | module ClassMethods 17 | include SmartRspec::Macros 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/smart_rspec/matchers.rb: -------------------------------------------------------------------------------- 1 | require 'smart_rspec/support/regexes' 2 | require 'smart_rspec/support/controller/response' 3 | require 'smart_rspec/matchers/be_matchers' 4 | require 'smart_rspec/matchers/json_api_matchers' 5 | require 'smart_rspec/matchers/other_matchers' 6 | 7 | module SmartRspec 8 | module Matchers 9 | include SmartRspec::Support::Regexes 10 | include SmartRspec::Matchers::BeMatchers 11 | include SmartRspec::Matchers::JsonApiMatchers 12 | include SmartRspec::Matchers::OtherMatchers 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/smart_rspec/macros.rb: -------------------------------------------------------------------------------- 1 | require 'smart_rspec/support/model/assertions' 2 | 3 | module SmartRspec::Macros 4 | include SmartRspec::Support::Model::Assertions 5 | 6 | def belongs_to(*associations) 7 | assert_association :belongs_to, associations 8 | end 9 | 10 | def has_attributes(*attrs) 11 | options = attrs.last.is_a?(Hash) && attrs.last.key?(:type) ? attrs.pop : nil 12 | assert_has_attributes(attrs, options) 13 | end 14 | 15 | def has_one(*associations) 16 | assert_association :has_one, associations 17 | end 18 | 19 | def has_many(*associations) 20 | assert_association :has_many, associations 21 | end 22 | 23 | def fails_validation_of(*attrs, validations) 24 | attrs.each do |attr| 25 | context attr do 26 | validations.keys.each do |key| 27 | validation = validations[key] 28 | validation && send("validates_#{key}_of", attr, validation) 29 | end 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/smart_rspec/matchers/be_matchers.rb: -------------------------------------------------------------------------------- 1 | module SmartRspec 2 | module Matchers 3 | module BeMatchers 4 | extend RSpec::Matchers::DSL 5 | 6 | matcher :be_ascending do 7 | match { |actual| actual == actual.sort } 8 | end 9 | 10 | matcher :be_a_list_of do |klass| 11 | match do |collection| 12 | collection.all? { |e| e.is_a?(klass) } 13 | end 14 | end 15 | 16 | matcher :be_boolean do 17 | match { |actual| [true, false].include?(actual) } 18 | end 19 | 20 | matcher :be_descending do 21 | match do |actual| 22 | actual.each_cons(2).all? { |i, j| i >= j } 23 | end 24 | end 25 | 26 | matcher :be_email do 27 | match { |actual| actual =~ build_regex(:email) } 28 | end 29 | 30 | matcher :be_image_url do |*types| 31 | match { |actual| actual =~ build_regex(:image, types) } 32 | end 33 | 34 | matcher :be_url do 35 | match { |actual| actual =~ build_regex(:uri) } 36 | end 37 | end 38 | end 39 | end 40 | 41 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Tiago Guedes 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /lib/smart_rspec/support/regexes.rb: -------------------------------------------------------------------------------- 1 | module SmartRspec 2 | module Support 3 | module Regexes 4 | @@uri_regexes = 5 | { protocol: /((ht|f)tp[s]?)/i, 6 | uri: %r{^( 7 | (((ht|f)tp[s]?://)|([a-z0-9]+\.))+ 8 | (? 6.0' 21 | spec.add_runtime_dependency 'rspec-collection_matchers', '~> 1.1', '>= 1.1.2' 22 | 23 | spec.add_development_dependency 'bundler', '~> 2.0' 24 | spec.add_development_dependency 'faker', '~> 2.0' 25 | spec.add_development_dependency 'rake', '~> 13.0' 26 | spec.add_development_dependency 'rspec', '~> 3.5' 27 | end 28 | -------------------------------------------------------------------------------- /lib/smart_rspec/support/controller/response.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | 3 | module SmartRspec 4 | module Support 5 | module Controller 6 | module Response 7 | def json(response) 8 | @json ||= JSON.parse(response.body) 9 | self 10 | end 11 | 12 | def error 13 | @error ||= @json['errors'].first 14 | end 15 | 16 | def collection 17 | @collection ||= [@json['data']].flatten 18 | end 19 | 20 | def meta_record_count 21 | @json['meta']['record_count'] 22 | end 23 | 24 | def relationship_data 25 | @relationship_data ||= collection.flat_map do |record| 26 | record['relationships'].flat_map do |_, relation| 27 | [relation['data']].flatten.map { |data| data.slice('type', 'id') } 28 | end.compact 29 | end 30 | end 31 | 32 | def included_data 33 | return [] if @json['included'].nil? 34 | @included_data ||= @json['included'].flat_map do |record| 35 | record.slice('type', 'id') 36 | end 37 | end 38 | 39 | def check_keys_in(member, keys) 40 | collection.all? do |record| 41 | record[member].keys.sort == keys.sort 42 | end 43 | end 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/smart_rspec/matchers/json_api_matchers.rb: -------------------------------------------------------------------------------- 1 | module SmartRspec 2 | module Matchers 3 | module JsonApiMatchers 4 | extend RSpec::Matchers::DSL 5 | include SmartRspec::Support::Controller::Response 6 | 7 | matcher :have_primary_data do |expected| 8 | match do |response| 9 | json(response).collection.all? do |record| 10 | !record['id'].to_s.empty? && record['type'] == expected 11 | end 12 | end 13 | end 14 | 15 | matcher :have_data_attributes do |fields| 16 | match do |response| 17 | json(response).check_keys_in('attributes', fields) 18 | end 19 | end 20 | 21 | matcher :have_relationships do |relationships| 22 | match do |response| 23 | json(response).check_keys_in('relationships', relationships) 24 | end 25 | end 26 | 27 | matcher :have_included_relationships do 28 | match do |response| 29 | json(response) 30 | return false if included_data.empty? || relationship_data.empty? 31 | included_data.size == relationship_data.size && 32 | (included_data - relationship_data).empty? 33 | end 34 | end 35 | 36 | matcher :have_meta_record_count do |count| 37 | match do |response| 38 | json(response).meta_record_count == count 39 | end 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/smart_rspec/support/model/expectations.rb: -------------------------------------------------------------------------------- 1 | module SmartRspec 2 | module Support 3 | module Model 4 | module Expectations 5 | def be_valid_expectation(attr, value = nil, mock = nil) 6 | mock ||= subject 7 | mock.send("#{attr}=", value) 8 | 9 | expect(mock).not_to be_valid 10 | expect(mock).to have_error_on(attr) 11 | end 12 | 13 | def default_expectation(attr, value) 14 | expect(subject.send(attr)).to eq(value) 15 | end 16 | 17 | def enum_expectation(attr, value) 18 | expect(value).to include(subject.send(attr).to_sym) 19 | end 20 | 21 | def type_expectation(attr, value) 22 | assert_type = value != :Boolean ? be_kind_of(Kernel.const_get(value)) : be_boolean 23 | expect(subject.send(attr)).to assert_type 24 | end 25 | 26 | def has_attributes_expectation(attr, options) options.each do |key, value| 27 | send("#{key}_expectation", attr, value) 28 | end 29 | end 30 | 31 | def association_expectation(type, model) 32 | if type == :has_many 33 | expect(subject).to respond_to("#{model.to_s.singularize}_ids") 34 | elsif type == :belongs_to 35 | %W(#{model}= #{model}_id #{model}_id=).each do |method| 36 | expect(subject).to respond_to(method) 37 | end 38 | end 39 | end 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /spec/fixtures/users.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [ 3 | { 4 | "id": "1", 5 | "type": "users", 6 | "links": { 7 | "self": "http://api.myawesomesite.com/users/1" 8 | }, 9 | "attributes": { 10 | "first_name": "Tiago", 11 | "last_name": "Guedes", 12 | "full_name": "Tiago Guedes", 13 | "birthday": "1988-22-12" 14 | }, 15 | "relationships": { 16 | "posts": { 17 | "links": { 18 | "self": "http://api.myawesomesite.com/users/1/relationships/posts", 19 | "related": "http://api.myawesomesite.com/users/1/posts" 20 | }, 21 | "data": [ 22 | { 23 | "type": "posts", 24 | "id": "1" 25 | } 26 | ] 27 | } 28 | } 29 | }, 30 | { 31 | "id": "2", 32 | "type": "users", 33 | "links": { 34 | "self": "http://api.myawesomesite.com/users/2" 35 | }, 36 | "attributes": { 37 | "first_name": "Douglas", 38 | "last_name": "André", 39 | "full_name": "Douglas André", 40 | "birthday": null 41 | }, 42 | "relationships": { 43 | "posts": { 44 | "links": { 45 | "self": "http://api.myawesomesite.com/users/2/relationships/posts", 46 | "related": "http://api.myawesomesite.com/users/2/posts" 47 | }, 48 | "data": [] 49 | } 50 | } 51 | } 52 | ], 53 | "included": [ 54 | { 55 | "id": "1", 56 | "type": "posts", 57 | "links": { 58 | "self": "http://api.myawesomesite.com/posts/1" 59 | }, 60 | "attributes": { 61 | "title": "An awesome post", 62 | "body": "Lorem ipsum dolot sit amet" 63 | }, 64 | "relationships": { 65 | "author": { 66 | "links": { 67 | "self": "http://api.myawesomesite.com/posts/1/relationships/author", 68 | "related": "http://api.myawesomesite.com/posts/1/author" 69 | } 70 | } 71 | } 72 | } 73 | ], 74 | "meta": { 75 | "record_count": 2 76 | }, 77 | "links": { 78 | "first": "http://api.myawesomesite.com/users?include=posts&page%5Blimit%5D=2&page%5Boffset%5D=0", 79 | "last": "http://api.myawesomesite.com/users?include=posts&page%5Blimit%5D=2&page%5Boffset%5D=0" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /spec/smart_rspec/macros_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SmartRspec::Macros do 4 | include SmartRspec 5 | 6 | subject(:user) do 7 | User.create({ 8 | email: Faker::Internet.email, 9 | name: Faker::Name.name, 10 | username: Faker::Internet.user_name 11 | }) 12 | end 13 | 14 | describe '#belongs_to' do 15 | context 'when it receives a single arg' do 16 | belongs_to :system 17 | end 18 | 19 | context 'when it receives multiple args' do 20 | belongs_to :system, :project 21 | end 22 | end 23 | 24 | describe '#has_attributes' do 25 | context 'when it receives a single arg' do 26 | has_attributes :email, :name, :username, type: :String 27 | has_attributes :is_admin, type: :Boolean 28 | has_attributes :score, type: :Integer, default: 0 29 | has_attributes :locale, type: :String, enum: [:en, :pt], default: 'en' 30 | end 31 | 32 | context 'when it receives multiple args' do 33 | has_attributes :name, :username, type: :String 34 | end 35 | end 36 | 37 | describe '#has_one' do 38 | context 'when it receives a single arg' do 39 | has_one :admin 40 | end 41 | 42 | context 'when it receives multiple args' do 43 | has_one :admin, :father, :mother 44 | end 45 | end 46 | 47 | describe '#has_many' do 48 | context 'when it receives a single arg' do 49 | has_one :articles 50 | end 51 | 52 | context 'when it receives multiple args' do 53 | has_one :articles, :rates 54 | end 55 | end 56 | 57 | describe '#fails_validation_of' do 58 | context 'when it receives a single arg' do 59 | new_user = 60 | User.new({ 61 | email: Faker::Internet.email, 62 | name: Faker::Name.name, 63 | username: Faker::Internet.user_name 64 | }) 65 | 66 | fails_validation_of :email, presence: true, email: true, uniqueness: { mock: new_user } 67 | fails_validation_of :name, length: { maximum: 80 } 68 | fails_validation_of :username, uniqueness: { mock: new_user }, exclusion: { in: %w(foo bar) } 69 | fails_validation_of :locale, inclusion: { in: %w(en pt) } 70 | fails_validation_of :father, format: { with: /foo/, mock: 'bar' } 71 | end 72 | 73 | context 'when it receives multiple args' do 74 | fails_validation_of :email, :name, presence: true 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /spec/fixtures/user.rb: -------------------------------------------------------------------------------- 1 | require 'smart_rspec/support/regexes' 2 | 3 | module Fixtures 4 | class User 5 | include SmartRspec::Support::Regexes 6 | 7 | @@last_id = 0 8 | @@collection = [] 9 | @@error_message = { 10 | blank: "can't be blank", 11 | exclusion: "can't use a reserved value", 12 | format: "doesn't match the required pattern", 13 | inclusion: 'value not included', 14 | too_big: "can't be greater than 80 chars", 15 | uniqueness: 'must be unique within the given scope' 16 | } 17 | 18 | attr_accessor :email, :system, :system_id, :project, :project_id, 19 | :name, :username, :is_admin, :score, :admin, :father, 20 | :mother, :articles, :rates, :errors 21 | 22 | attr_reader :id 23 | 24 | attr_writer :locale 25 | 26 | def initialize(attrs = {}) 27 | attrs.each { |key, value| self.send("#{key}=", value) } 28 | set_defaults 29 | end 30 | 31 | class << self 32 | attr_reader :collection 33 | 34 | def create(attrs) 35 | user = User.new(attrs) 36 | @@collection << user && user 37 | end 38 | 39 | def find_by(key, value) 40 | @@collection.find { |e| e.send(key) == value } 41 | end 42 | end 43 | 44 | def save 45 | @@collection << self 46 | end 47 | 48 | def locale 49 | @locale.to_s unless @locale.nil? 50 | end 51 | 52 | def persisted? 53 | User.find_by(:id, id) 54 | end 55 | 56 | def valid? 57 | %w(email father locale name username).each { |e| send("check_#{e}") } 58 | @errors.nil? 59 | end 60 | 61 | private 62 | 63 | def check_email 64 | if !email || (email && email !~ build_regex(:email)) 65 | @errors.merge!({ email: @@error_message[:blank] }) 66 | elsif User.find_by(:email, email) 67 | @errors.merge!({ email: @@error_message[:uniqueness] }) 68 | end 69 | end 70 | 71 | def check_father 72 | if father && father !~ /foo/ 73 | @errors.merge!({ father: @@error_message[:format] }) 74 | end 75 | end 76 | 77 | def check_locale 78 | unless %w(en pt).include?(locale) 79 | @errors.merge!({ locale: @@error_message[:inclusion] }) 80 | end 81 | end 82 | 83 | def check_name 84 | if !name || (name && name.length > 80) 85 | @errors.merge!({ name: @@error_message[(name ? :too_big : :blank)] }) 86 | end 87 | end 88 | 89 | def check_username 90 | if username.to_s.empty? 91 | @errors.merge!({ username: @@error_message[:blank] }) 92 | elsif %w(foo bar).include?(username) 93 | @errors.merge!({ username: @@error_message[:exclusion] }) 94 | elsif User.find_by(:username, username) 95 | @errors.merge!({ username: @@error_message[:uniqueness] }) 96 | end 97 | end 98 | 99 | def set_defaults 100 | @@last_id = @id = @@last_id + 1 101 | attrs = { errors: {}, is_admin: false, score: 0, locale: :en } 102 | attrs.each { |key, value| send("#{key}=", value) } 103 | end 104 | end 105 | end 106 | -------------------------------------------------------------------------------- /lib/smart_rspec/support/model/assertions.rb: -------------------------------------------------------------------------------- 1 | module SmartRspec 2 | module Support 3 | module Model 4 | module Assertions 5 | def validates_email_of(attr, validation) 6 | it 'has an invalid format' do 7 | %w(foobar foobar@ @foobar foo@bar).each do |e| 8 | be_valid_expectation(attr, e, subject.dup) 9 | end 10 | end 11 | end 12 | 13 | def validates_exclusion_of(attr, validation) 14 | it 'has a reserved value' do 15 | be_valid_expectation(attr, validation[:in].sample) 16 | end 17 | end 18 | 19 | def validates_format_of(attr, validation) 20 | it 'does not match the required format' do 21 | mock, with = 22 | validation.values_at(:mock).first, 23 | validation.values_at(:with).first 24 | 25 | if mock && with && with !~ mock 26 | be_valid_expectation(attr, mock) 27 | else 28 | raise ArgumentError, ':with and :mock are required when using the :format validation' 29 | end 30 | end 31 | end 32 | 33 | def validates_inclusion_of(attr, validation) 34 | it 'is out of the scope of possible values' do 35 | begin 36 | value = SecureRandom.hex 37 | end while validation[:in].include?(value) 38 | be_valid_expectation(attr, value) 39 | end 40 | end 41 | 42 | def validates_length_of(attr, validation) 43 | validation.each do |key, value| 44 | next unless [:in, :is, :maximum, :minimum, :within].include?(key) 45 | txt, n = build_length_validation(key, value) 46 | it txt do 47 | be_valid_expectation(attr, 'x' * n) 48 | end 49 | end 50 | end 51 | 52 | def validates_presence_of(attr, validation) 53 | it 'is blank' do 54 | be_valid_expectation(attr, nil, subject.dup) 55 | end 56 | end 57 | 58 | def validates_uniqueness_of(attr, validation) 59 | it 'is already in use' do 60 | if !validation.is_a?(Hash) || !validation.has_key?(:mock) 61 | raise ArgumentError, 'A "mock" must be set when validating the uniqueness of a record' 62 | elsif subject.persisted? || subject.save 63 | mock, scope = validation.values_at(:mock, :scope) 64 | mock.send("#{scope}=", subject.send(scope)) unless scope.to_s.empty? 65 | be_valid_expectation(attr, subject.send(attr), mock) 66 | end 67 | end 68 | end 69 | 70 | def assert_has_attributes(attrs, options) type_str = build_type_str(options) 71 | attrs.each do |attr| 72 | it %Q(has an attribute named "#{attr}"#{type_str}) do 73 | expect(subject).to respond_to(attr) 74 | has_attributes_expectation(attr, options) 75 | end 76 | end 77 | end 78 | 79 | def assert_association(type, associations) 80 | associations.each do |model| 81 | it "#{type.to_s.gsub('_', ' ')} #{model}" do 82 | expect(subject).to respond_to(model) 83 | association_expectation(type, model) 84 | end 85 | end 86 | end 87 | 88 | def build_length_validation(key, value) 89 | case key 90 | when :in, :within then ['is out of the length range', value.max + 1] 91 | when :is, :minimum then ["is #{key == :is ? 'invalid' : 'too short'}", value - 1] 92 | when :maximum then ['is too long', value + 1] 93 | end 94 | end 95 | 96 | def build_type_str(options) 97 | if !options.nil? && options[:type] 98 | " (%s%s%s)" % [ 99 | ('Enumerated ' if options[:enum]), 100 | options[:type], 101 | (", default: #{options[:default]}" if options[:default]) 102 | ] 103 | end 104 | end 105 | 106 | # def scoped_validation?(validation) 107 | # validation.is_a?(Hash) && ([:scope, :mock] - validation.keys).empty? 108 | # end 109 | end 110 | end 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /spec/smart_rspec/matchers_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SmartRspec::Matchers do 4 | describe BeMatchers do 5 | describe '#be_ascending' do 6 | context 'positive assertion' do 7 | it { expect([1, 2, 3, 4]).to be_ascending } 8 | end 9 | 10 | context 'negative assertion' do 11 | it { expect([1, 4, 2, 3]).not_to be_ascending } 12 | end 13 | end 14 | 15 | describe '#be_boolean' do 16 | context 'positive assertion' do 17 | it { expect(true).to be_boolean } 18 | it { expect(false).to be_boolean } 19 | end 20 | 21 | context 'negative assertion' do 22 | it { expect('true').not_to be_boolean } 23 | it { expect(1).not_to be_boolean } 24 | it { expect(%w(foo bar)).not_to be_boolean } 25 | end 26 | end 27 | 28 | describe '#be_descending' do 29 | context 'positive assertion' do 30 | it { expect([4, 3, 2, 1]).to be_descending } 31 | end 32 | 33 | context 'negative assertion' do 34 | it { expect([1, 2, 3, 4]).not_to be_descending } 35 | end 36 | end 37 | 38 | describe '#be_email' do 39 | context 'positive assertion' do 40 | it { expect(Faker::Internet.email).to be_email } 41 | it { expect('tiagopog@gmail.com').to be_email } 42 | it { expect('foo@bar.com.br').to be_email } 43 | end 44 | 45 | context 'negative assertion' do 46 | it { expect('foo@bar').not_to be_email } 47 | it { expect('foo@').not_to be_email } 48 | it { expect('@bar').not_to be_email } 49 | it { expect('@bar.com').not_to be_email } 50 | it { expect('foo bar@bar.com').not_to be_email } 51 | end 52 | end 53 | 54 | describe '#be_url' do 55 | context 'positive assertion' do 56 | it { expect(Faker::Internet.url).to be_url } 57 | it { expect('http://adtangerine.com').to be_url } 58 | it { expect('http://www.facebook.com').to be_url } 59 | it { expect('www.twitflink.com').to be_url } 60 | it { expect('google.com.br').to be_url } 61 | end 62 | 63 | context 'negative assertion' do 64 | it { expect('foobar.bar').not_to be_url } 65 | it { expect('foobar').not_to be_url } 66 | it { expect('foo bar.com.br').not_to be_url } 67 | end 68 | end 69 | 70 | describe '#be_image_url' do 71 | context 'positive assertion' do 72 | it { expect(Faker::Company.logo).to be_image_url } 73 | it { expect('http://foobar.com/foo.jpg').to be_image_url } 74 | it { expect('http://foobar.com/foo.jpg').to be_image_url(:jpg) } 75 | it { expect('http://foobar.com/foo.gif').to be_image_url(:gif) } 76 | it { expect('http://foobar.com/foo.png').to be_image_url(:png) } 77 | it { expect('http://foobar.com/foo.png').to be_image_url([:jpg, :png]) } 78 | it { expect('http://foobar.com/foo/bar?image=foo.jpg').to be_image_url } 79 | end 80 | 81 | context 'negative assertion' do 82 | it { expect('http://foobar.com').not_to be_image_url } 83 | it { expect('http://foobar.com/foo.jpg').not_to be_image_url(:gif) } 84 | it { expect('http://foobar.com/foo.gif').not_to be_image_url(:png) } 85 | it { expect('http://foobar.com/foo.png').not_to be_image_url(:jpg) } 86 | it { expect('http://foobar.com/foo.gif').not_to be_image_url([:jpg, :png]) } 87 | end 88 | end 89 | 90 | describe '#be_a_list_of' do 91 | context 'positive assertion' do 92 | subject { Array.new(3, User.new) } 93 | it { is_expected.to be_a_list_of(User) } 94 | end 95 | 96 | context 'negative assertion' do 97 | subject { Array.new(3, User.new) << nil } 98 | it { is_expected.to_not be_a_list_of(User) } 99 | end 100 | end 101 | end 102 | 103 | describe JsonApiMatchers do 104 | subject(:response) { Fixtures::Response.new } 105 | 106 | describe '#have_primary_data' do 107 | context 'positive assertion' do 108 | it do 109 | expect(response).to have_primary_data('users') 110 | end 111 | end 112 | 113 | context 'negative assertion' do 114 | it do 115 | expect(response).not_to have_primary_data('foobar') 116 | end 117 | end 118 | end 119 | 120 | describe '#have_data_attributes' do 121 | let(:fields) { %w(first_name last_name full_name birthday) } 122 | 123 | context 'positive assertion' do 124 | it do 125 | expect(response).to have_data_attributes(fields) 126 | end 127 | end 128 | 129 | context 'negative assertion' do 130 | it do 131 | expect(response).not_to have_data_attributes(fields + %w(foobar)) 132 | end 133 | end 134 | end 135 | 136 | describe '#have_relationships' do 137 | let(:relationships) { %w(posts) } 138 | 139 | context 'positive assertion' do 140 | it do 141 | expect(response).to have_relationships(relationships) 142 | end 143 | end 144 | 145 | context 'negative assertion' do 146 | it do 147 | expect(response).not_to have_relationships(relationships + %w(foobar)) 148 | end 149 | end 150 | end 151 | 152 | describe '#have_included_relationships' do 153 | let(:relationships) { %w(posts) } 154 | 155 | context 'positive assertion' do 156 | it do 157 | expect(response).to have_included_relationships 158 | end 159 | end 160 | end 161 | 162 | describe '#have_meta_record_count' do 163 | context 'positive assertion' do 164 | it do 165 | expect(response).to have_meta_record_count(2) 166 | end 167 | end 168 | 169 | context 'negative assertion' do 170 | it do 171 | expect(response).not_to have_meta_record_count(3) 172 | end 173 | end 174 | end 175 | end 176 | 177 | describe OtherMatchers do 178 | describe '#have_error_on' do 179 | subject { User.new(email: nil, name: Faker::Name.name) } 180 | 181 | context 'positive assertion' do 182 | it do 183 | subject.valid? 184 | is_expected.to have_error_on(:email) 185 | end 186 | end 187 | 188 | context 'negative assertion' do 189 | it do 190 | subject.valid? 191 | is_expected.not_to have_error_on(:name) 192 | end 193 | end 194 | end 195 | 196 | describe '#include_items' do 197 | context 'positive assertion' do 198 | it { expect(%w(foo bar foobar)).to include_items(%w(foo bar foobar)) } 199 | it { expect(%w(lorem ipsum)).to include_items('lorem', 'ipsum') } 200 | it { expect([1, 'foo', ['bar']]).to include_items([1, 'foo', ['bar']]) } 201 | end 202 | 203 | context 'negative assertion' do 204 | it { expect(%w(foo bar foobar)).not_to include_items(%w(lorem)) } 205 | end 206 | end 207 | end 208 | 209 | describe ::RSpec::CollectionMatchers do 210 | describe '#have' do 211 | context 'positive assertion' do 212 | it { expect([1]).to have(1).item } 213 | it { expect(%w(foo bar)).to have(2).items } 214 | end 215 | 216 | context 'negative assertion' do 217 | it { expect([1]).not_to have(2).items } 218 | it { expect(%w(foo bar)).not_to have(1).item } 219 | end 220 | end 221 | 222 | describe '#have_at_least' do 223 | context 'positive assertion' do 224 | it { expect(%w(foo bar foobar)).to have_at_least(3).items } 225 | end 226 | end 227 | 228 | describe '#have_at_most' do 229 | context 'positive assertion' do 230 | it { expect(%w(foo bar foobar)).to have_at_most(3).items } 231 | end 232 | end 233 | end 234 | end 235 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SmartRspec 2 | 3 | [![Build Status](https://travis-ci.org/tiagopog/smart_rspec.svg)](https://travis-ci.org/tiagopog/smart_rspec) 4 | [![Code Climate](https://codeclimate.com/github/tiagopog/smart_rspec/badges/gpa.svg)](https://codeclimate.com/github/tiagopog/smart_rspec) 5 | [![Dependency Status](https://gemnasium.com/tiagopog/smart_rspec.svg)](https://gemnasium.com/tiagopog/smart_rspec) 6 | [![Gem Version](https://badge.fury.io/rb/smart_rspec.svg)](http://badge.fury.io/rb/smart_rspec) 7 | 8 | It's time to make your specs even more awesome! SmartRspec adds useful macros and matchers into the RSpec's test suite, so you can quickly define specs for your Rails app and get focused on making things turn into green. 9 | 10 | ## Installation 11 | 12 | Compatible with: 13 | 14 | * Ruby 1.9+ 15 | * ActiveRecord (model macros) 16 | 17 | Add this line to your application's Gemfile: 18 | 19 | gem 'smart_rspec' 20 | 21 | Execute: 22 | 23 | $ bundle 24 | 25 | Require the gem at the top of your `spec/rails_helper.rb` (or equivalent): 26 | 27 | ``` ruby 28 | require 'smart_rspec' 29 | ``` 30 | 31 | Then include the SmartRspec module: 32 | 33 | ``` ruby 34 | RSpec.configure do |config| 35 | config.include SmartRspec 36 | end 37 | ``` 38 | 39 | ## Usage 40 | 41 | * [Macros](#macros) 42 | * [has_attributes](#has_attributes) 43 | * [belongs_to, has_one, has_many](#belongs_to-has_one-has_many) 44 | * [fails_validation_of](#fails_validation_of) 45 | * [Matchers](#matchers) 46 | * ["Be" matchers](#be-matchers) 47 | * [be_boolean](#be_boolean) 48 | * [be_email](#be_email) 49 | * [be_url](#be_url) 50 | * [be_image_url](#be_image_url) 51 | * [be_a_list_of](#be_a_list_of) 52 | * [be_ascending](#be_ascending) 53 | * [be_descending](#be_descending) 54 | * ["Have" matchers](#have-matchers) 55 | * [have](#have) 56 | * [have_at_least](#have_at_least) 57 | * [have_at_most](#have_at_most) 58 | * [have_error_on](#have_error_on) 59 | * [Other matchers](#other-matchers) 60 | * [include_items](#include_items) 61 | 62 | ### Macros 63 | 64 | You will just need to define a valid `subject` to start using SmartRspec's macros in your spec file. 65 | 66 | #### has_attributes 67 | 68 | It builds specs for model attributes and test its type, enumerated values and defaults: 69 | ``` ruby 70 | RSpec.describe User, type: :model do 71 | subject { FactoryGirl.build(:user) } 72 | 73 | has_attributes :email, type: :String 74 | has_attributes :is_admin, type: :Boolean 75 | has_attributes :score, type: :Integer, default: 0 76 | has_attributes :locale, type: :String, enum: %i(en pt), default: 'en' 77 | end 78 | ``` 79 | 80 | #### belongs_to, has_one, has_many 81 | 82 | It builds specs and test model associations like `belongs_to`, `has_one` and `has_many`. 83 | ``` ruby 84 | RSpec.describe User, type: :model do 85 | subject { FactoryGirl.build(:user) } 86 | 87 | belongs_to :business 88 | has_one :project 89 | has_many :tasks 90 | end 91 | ``` 92 | 93 | #### fails_validation_of 94 | 95 | It builds specs and forces model validations to fail, meaning that you will only turn specs into green when you specify the corresponding validation in the model. In order to get a nice semantics it's recommended to use the `fails_validation_of` macro within a "when invalid" context, like: 96 | 97 | ``` ruby 98 | RSpec.describe User, type: :model do 99 | subject { FactoryGirl.build(:user) } 100 | 101 | context 'when invalid' do 102 | fails_validation_of :email, presence: true, email: true 103 | fails_validation_of :name, length: { maximum: 80 }, uniqueness: true 104 | fails_validation_of :username, length: { minimum: 10 }, exclusion: { in: %w(foo bar) } 105 | # Other validations... 106 | end 107 | end 108 | ``` 109 | 110 | The `fails_validation_of` implements specs for the following validations: 111 | 112 | - `presence` 113 | - `email` 114 | - `length` 115 | - `exclusion` 116 | - `inclusion` 117 | - `uniqueness` 118 | - `format` 119 | 120 | In two cases it will require a valid mock to be passed so SmartRspec can use it to force the validation to fail properly. 121 | 122 | For uniqueness with scope: 123 | ``` ruby 124 | other_user = FactoryGirl.build(:other_valid_user) 125 | fails_validation_of :username, uniqueness: { scope: :name, mock: other_user } 126 | ``` 127 | 128 | For format: 129 | ``` ruby 130 | fails_validation_of :foo, format: { with: /foo/, mock: 'bar' } 131 | ``` 132 | 133 | ### Matchers 134 | 135 | SmartRspec gathers a collection of custom useful matchers: 136 | 137 | #### Be matchers 138 | 139 | ##### be_boolean 140 | ``` ruby 141 | it { expect(true).to be_boolean } 142 | it { expect('true').not_to be_boolean } 143 | ``` 144 | 145 | ##### be_email 146 | ``` ruby 147 | it { expect('tiagopog@gmail.com').to be_email } 148 | it { expect('tiagopog@gmail').not_to be_email } 149 | ``` 150 | 151 | ##### be_url 152 | ``` ruby 153 | it { expect('http://adtangerine.com').to be_url } 154 | it { expect('adtangerine.com').not_to be_url } 155 | ``` 156 | 157 | ##### be_image_url 158 | ``` ruby 159 | it { expect('http://adtangerine.com/foobar.png').to be_image_url } 160 | it { expect('http://adtangerine.com/foobar.jpg').not_to be_image_url(:gif) } 161 | it { expect('http://adtangerine.com/foo/bar').not_to be_image_url } 162 | ``` 163 | 164 | ##### be_a_list_of 165 | ``` ruby 166 | it { expect(Foo.fetch_api).to be_a_list_of(Foo)) } 167 | ``` 168 | 169 | ##### be_ascending 170 | 171 | ``` ruby 172 | it { expect([1, 2, 3, 4]).to be_ascending } 173 | it { expect([1, 4, 2, 3]).not_to be_ascending } 174 | ``` 175 | 176 | ##### be_descending 177 | ``` ruby 178 | it { expect([4, 3, 2, 1]).to be_descending } 179 | it { expect([1, 2, 3, 4]).not_to be_descending } 180 | ``` 181 | 182 | #### Have matchers 183 | 184 | ##### have(x).items 185 | ``` ruby 186 | it { expect([1]).to have(1).item } 187 | it { expect(%w(foo bar)).to have(2).items } 188 | it { expect(%w(foo bar)).not_to have(1).item } 189 | ``` 190 | 191 | ##### have_at_least 192 | ``` ruby 193 | it { expect(%w(foo bar foobar)).to have_at_least(3).items } 194 | ``` 195 | 196 | ##### have_at_most 197 | ``` ruby 198 | it { expect(%w(foo bar foobar)).to have_at_most(3).items } 199 | ``` 200 | 201 | ##### have_error_on 202 | ``` ruby 203 | subject { User.new(email: nil, name: Faker::Name.name) } 204 | 205 | it 'has an invalid email' do 206 | subject.valid? 207 | is_expected.to have_error_on(:email) 208 | end 209 | ``` 210 | 211 | #### Other matchers 212 | 213 | ##### include_items 214 | 215 | Comparing to array: 216 | 217 | ``` ruby 218 | it { expect(%w(foo bar foobar)).to include_items(%w(foo bar foobar)) } 219 | ``` 220 | 221 | Comparing to multiple arguments: 222 | 223 | ``` ruby 224 | it 'includes all items' do 225 | item1, item2 = 'foo', 'bar' 226 | expect(%w(foo bar)).to include_items(item1, item2)) 227 | end 228 | ``` 229 | 230 | # Credits 231 | 232 | 1. Some of the "have" matchers (precisely `have`, `have_at_least` and `have_at_most`) were taken from the `rspec-collection_matchers` gem. 233 | 2. Some of the macros/matchers were inspired in RSpec helpers that I worked along with two friends ([Douglas André](https://github.com/douglasandre) and [Giovanni Bonetti](https://github.com/giovannibonetti)) at the 234 | [Beauty Date](https://beautydate.com.br) project. 235 | 236 | # TODO 237 | 238 | - Create macros for model scopes; 239 | - Create macros for controllers; 240 | - Add more matchers; 241 | - Take groups of matchers into modules; 242 | - Turn the whole into "A" in Code Climate. 243 | 244 | ## Contributing 245 | 246 | 1. Fork it; 247 | 2. Create your feature branch (`git checkout -b my-new-feature`); 248 | 3. Create your specs and make sure they are passing; 249 | 4. Document your feature in the README.md; 250 | 4. Commit your changes (`git commit -am 'Add some feature'`); 251 | 5. Push to the branch (`git push origin my-new-feature`); 252 | 6. Create new Pull Request. 253 | 254 | --------------------------------------------------------------------------------