├── .gitignore ├── LICENSE ├── README.md ├── gems.locked ├── gems.rb ├── lib └── active_record │ ├── base_class_fix.rb │ ├── entity.rb │ └── repository.rb └── spec ├── integration └── basic_model_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.rbc 2 | capybara-*.html 3 | .rspec 4 | /log 5 | /tmp 6 | /db/*.sqlite3 7 | /db/*.sqlite3-journal 8 | /public/system 9 | /coverage/ 10 | /spec/tmp 11 | *.orig 12 | rerun.txt 13 | pickle-email-*.html 14 | 15 | # TODO Comment out this rule if you are OK with secrets being uploaded to the repo 16 | config/initializers/secret_token.rb 17 | config/master.key 18 | 19 | # Only include if you have production secrets in this file, which is no longer a Rails default 20 | # config/secrets.yml 21 | 22 | # dotenv 23 | # TODO Comment out this rule if environment variables can be committed 24 | .env 25 | 26 | ## Environment normalization: 27 | /.bundle 28 | /vendor/bundle 29 | 30 | # these should all be checked in to normalize the environment: 31 | # Gemfile.lock, .ruby-version, .ruby-gemset 32 | 33 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 34 | .rvmrc 35 | 36 | # if using bower-rails ignore default bower_components path bower.json files 37 | /vendor/assets/bower_components 38 | *.bowerrc 39 | bower.json 40 | 41 | # Ignore pow environment settings 42 | .powenv 43 | 44 | # Ignore Byebug command history file. 45 | .byebug_history 46 | 47 | # Ignore node_modules 48 | node_modules/ 49 | 50 | # Spec example timings 51 | spec/examples.txt 52 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Craig Buchek 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ActiveRecord Repository 2 | ======================= 3 | 4 | This is an implementation of the repository pattern for ActiveRecord. 5 | Using this allows splitting the domain model and persistence classes. 6 | 7 | 8 | WARNING: This is currently merely a proof of concept. This means that testing 9 | and refactoring have often been put on the back burner. 10 | 11 | 12 | TODO 13 | ---- 14 | 15 | * Loading 16 | * Loading relations 17 | * Avoiding N+1 queries 18 | * Saving 19 | * Saving relations 20 | * Keep a registry of the repository classes 21 | * Indexed by the classes they map to 22 | * Use this to find repository class for relations 23 | * Caveat: What if there are 2 repositories for a relation we're looking for? 24 | * Need a way to specify that somewhere 25 | * In the repository is the only place that makes sense 26 | * Probably need to restrict other calls to Repository 27 | * `User::Repository.create` 28 | * Entity#initialize and #update should basically be the same 29 | * Maybe the only difference is that initialize will set things to `nil` 30 | * They could use a lot of refactoring 31 | * Relations between entities 32 | * belongs_to 33 | * has_many 34 | * has_one 35 | * has_many through: 36 | * Cascading deletions 37 | * Entity#changed? 38 | * Identity mapping 39 | * Use module builder pattern in ActiveModel.entity 40 | * Add more options 41 | * Use module factory (and module builder) pattern in ActiveRecord.repository 42 | * Add options 43 | * Setting entity class 44 | * Setting table_name 45 | * Setting a different primary key than `id` 46 | * Maybe define indexes 47 | * More validations 48 | -------------------------------------------------------------------------------- /gems.locked: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | activemodel (5.2.2) 5 | activesupport (= 5.2.2) 6 | activerecord (5.2.2) 7 | activemodel (= 5.2.2) 8 | activesupport (= 5.2.2) 9 | arel (>= 9.0) 10 | activesupport (5.2.2) 11 | concurrent-ruby (~> 1.0, >= 1.0.2) 12 | i18n (>= 0.7, < 2) 13 | minitest (~> 5.1) 14 | tzinfo (~> 1.1) 15 | arel (9.0.0) 16 | coderay (1.1.2) 17 | concurrent-ruby (1.1.3) 18 | diff-lcs (1.3) 19 | i18n (1.2.0) 20 | concurrent-ruby (~> 1.0) 21 | method_source (0.9.2) 22 | minitest (5.11.3) 23 | pry (0.12.2) 24 | coderay (~> 1.1.0) 25 | method_source (~> 0.9.0) 26 | rspec (3.8.0) 27 | rspec-core (~> 3.8.0) 28 | rspec-expectations (~> 3.8.0) 29 | rspec-mocks (~> 3.8.0) 30 | rspec-core (3.8.0) 31 | rspec-support (~> 3.8.0) 32 | rspec-expectations (3.8.2) 33 | diff-lcs (>= 1.2.0, < 2.0) 34 | rspec-support (~> 3.8.0) 35 | rspec-mocks (3.8.0) 36 | diff-lcs (>= 1.2.0, < 2.0) 37 | rspec-support (~> 3.8.0) 38 | rspec-support (3.8.0) 39 | sqlite3 (1.3.13) 40 | thread_safe (0.3.6) 41 | tzinfo (1.2.5) 42 | thread_safe (~> 0.1) 43 | 44 | PLATFORMS 45 | ruby 46 | 47 | DEPENDENCIES 48 | activerecord (~> 5.2) 49 | pry (~> 0.11) 50 | rspec (~> 3.7) 51 | sqlite3 (~> 1.3) 52 | 53 | BUNDLED WITH 54 | 1.17.1 55 | -------------------------------------------------------------------------------- /gems.rb: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "activerecord", "~> 5.2" 4 | 5 | gem "pry", "~> 0.11", groups: %i[development test] 6 | gem "rspec", "~> 3.7", groups: %i[development test] 7 | 8 | gem "sqlite3", "~> 1.3", groups: %i[test] 9 | -------------------------------------------------------------------------------- /lib/active_record/base_class_fix.rb: -------------------------------------------------------------------------------- 1 | require "active_record/base" 2 | 3 | 4 | module ActiveRecord 5 | 6 | # We have to override Inheritance.ClassMethods.base_class, because we don't directly subclass from ActiveRecord::Base. 7 | module BaseClassFix 8 | def base_class 9 | self 10 | end 11 | 12 | def self.extended(mod) 13 | mod.class.class_eval { 14 | def abstract_class? 15 | false 16 | end 17 | } 18 | 19 | def connection_specification_name 20 | "primary" # FIXME: This should be the default, but should be overridable, like normal ActiveRecord. 21 | end 22 | 23 | def connection 24 | ActiveRecord::Base.connection 25 | end 26 | end 27 | end 28 | 29 | end 30 | -------------------------------------------------------------------------------- /lib/active_record/entity.rb: -------------------------------------------------------------------------------- 1 | require "active_record/base" 2 | require "active_record/base_class_fix" 3 | 4 | 5 | module ActiveModel 6 | 7 | def self.entity(datestamps: true) 8 | modules = [::ActiveModel::Entity] 9 | modules << ::ActiveModel::Entity::DateStamps if datestamps 10 | composite_module(modules) 11 | end 12 | 13 | def self.composite_module(modules) 14 | Module.new { 15 | @modules = modules 16 | 17 | def self.included(entity_module) 18 | @modules.each do |mod| 19 | entity_module.class_eval { 20 | include mod 21 | } 22 | end 23 | end 24 | } 25 | end 26 | 27 | module Entity 28 | 29 | def self.included(mod) 30 | mod.extend ActiveModel::Model # Includes AttributeAssignment, Validations, Conversion, Naming, Translation. 31 | mod.include ActiveModel::AttributeMethods 32 | mod.include ActiveModel::Attributes 33 | mod.include ActiveModel::Validations 34 | # mod.include ActiveSupport::Callbacks 35 | # mod.include AttributeAssignment # Has `assign_attributes`. 36 | # mod.include Attributes # Has `attribute`. 37 | # mod.include AttributeDecorators 38 | # mod.include AttributeMethods # Has `primary_key`, which requires a schema and a connection. Requires `Core.initialize_generated_modules`. 39 | # mod.include Core 40 | # mod.include Transactions # Requires active_model/callbacks 41 | # mod.include Integration 42 | # mod.include Validations 43 | # mod.include Timestamp 44 | # mod.include Associations 45 | # mod.include NestedAttributes 46 | 47 | # def mod._default_attributes 48 | # [] 49 | # end 50 | 51 | # def mod.column_names 52 | # attributes_to_define_after_schema_loads.keys 53 | # end 54 | 55 | mod.define_attribute_methods(mod.attribute_types.keys.map(&:to_sym)) 56 | 57 | def mod.attribute(name, type, options = {}) 58 | super(name, type) 59 | validates name, presence: true if options.fetch(:required){ true } 60 | end 61 | 62 | end 63 | 64 | def initialize(attrs = {}) 65 | attribute_keys = attrs.keys.map(&:to_sym) 66 | allowed_attributes = self.class.attribute_types.keys.map(&:to_sym) 67 | extra_attribute_keys = attribute_keys - allowed_attributes - [:id] 68 | # Oddly, AR only names the first unknown attribute it sees. 69 | fail ActiveRecord::UnknownAttributeError.new(self, extra_attribute_keys.first) unless extra_attribute_keys.empty? || attrs[:ignore_extra_attributes] 70 | attrs = attrs.reject{ |k, _v| extra_attribute_keys.include?(k.to_sym) } 71 | @attributes = ActiveModel::AttributeSet.new((allowed_attributes + [:id]).map{ |k, _v| [k.to_s, ActiveModel::Attribute.from_user(k.to_sym, attrs.fetch(k.to_sym, nil), self.class.attribute_types[k.to_s], attrs.fetch(k.to_sym, nil))] }.to_h) 72 | end 73 | 74 | def update(attrs = {}) 75 | attrs.each do |k,v| 76 | if attrs[:ignore_extra_attributes] 77 | begin 78 | self.__send__("#{k}=".to_sym, v) 79 | rescue NoMethodError 80 | nil 81 | end 82 | else 83 | self.__send__("#{k}=".to_sym, v) 84 | end 85 | end 86 | end 87 | 88 | def persisted? 89 | # TODO: Handle other primary keys. 90 | attributes["id"].present? 91 | end 92 | 93 | # NOTE: These are for troubleshooting only. They cause some tests to fail. 94 | # def respond_to_missing?(_method_name, *_args) 95 | # true 96 | # end 97 | # def method_missing(method_name, *args) 98 | # puts "method called: #{method_name}(#{args})" 99 | # end 100 | 101 | module DateStamps 102 | end 103 | 104 | end 105 | 106 | end 107 | -------------------------------------------------------------------------------- /lib/active_record/repository.rb: -------------------------------------------------------------------------------- 1 | require "active_record/base" 2 | require "active_record/base_class_fix" 3 | 4 | 5 | module ActiveRecord 6 | 7 | def self.repository(model: nil, table_name: nil) 8 | ::ActiveRecord::Repository 9 | end 10 | 11 | module Repository 12 | 13 | MODULES_EXTENDED = [ 14 | ActiveModel::Naming, 15 | ActiveSupport::Benchmarkable, 16 | ActiveSupport::DescendantsTracker, 17 | ConnectionHandling, 18 | QueryCache::ClassMethods, 19 | Querying, 20 | Translation, 21 | DynamicMatchers, 22 | Explain, 23 | Enum, 24 | Delegation::DelegateCache, 25 | CollectionCacheKey, 26 | ] 27 | 28 | MODULES_INCLUDED_FIRST = [ 29 | Core, 30 | Persistence, 31 | ReadonlyAttributes, 32 | ModelSchema, 33 | Inheritance, 34 | ] 35 | 36 | MODULES_INCLUDED = [ 37 | Core, 38 | Persistence, 39 | ReadonlyAttributes, 40 | ModelSchema, 41 | Inheritance, 42 | Scoping, 43 | Sanitization, 44 | AttributeAssignment, 45 | ActiveModel::Conversion, 46 | Integration, 47 | CounterCache, 48 | Attributes, 49 | AttributeDecorators, 50 | Locking::Optimistic, 51 | Locking::Pessimistic, 52 | DefineCallbacks, 53 | AttributeMethods, 54 | Timestamp, 55 | Associations, 56 | ActiveModel::SecurePassword, 57 | AutosaveAssociation, 58 | Aggregations, 59 | Transactions, 60 | Reflection, 61 | Serialization, 62 | Store, 63 | SecureToken, 64 | Suppressor, 65 | # Validations, 66 | # Callbacks, 67 | # NestedAttributes, 68 | # TouchLater, 69 | # NoTouching, 70 | ] 71 | 72 | def self.included(mod) 73 | MODULES_EXTENDED.each do |m| 74 | mod.extend(m) 75 | end 76 | 77 | MODULES_INCLUDED_FIRST.each do |m| 78 | mod.include(m) 79 | end 80 | 81 | # Unfortunately, this has to go between some of the inclusions. 82 | mod.extend(BaseClassFix) 83 | 84 | MODULES_INCLUDED.each do |m| 85 | mod.include(m) 86 | end 87 | 88 | # mod.initialize_generated_modules # AttributeMethods 89 | mod.initialize_relation_delegate_cache # Delegation::DelegateCache 90 | mod.initialize_find_by_cache # Core 91 | mod.__send__(:initialize_load_schema_monitor) # ModelSchema 92 | 93 | # TODO: Make this more robust. Allow passing table_name as module parameter. 94 | mod.table_name = mod.name.split("::").first.tableize 95 | 96 | class << mod 97 | 98 | # NOTE: Requires AttributeMethods. 99 | def save(entity) 100 | raise ArgumentError unless entity.is_a? ActiveModel::Entity 101 | 102 | mirror_object = new(entity.attributes.transform_keys(&:to_sym)) 103 | mirror_object.save 104 | entity.id = mirror_object.id 105 | end 106 | 107 | # NOTE: Requires AttributeMethods. 108 | def save!(entity) 109 | mirror_object = new(entity.attributes.transform_keys(&:to_sym)) 110 | mirror_object.save! 111 | entity.id = mirror_object.id 112 | end 113 | 114 | # NOTE: These should probably be protected. 115 | 116 | def find(id) 117 | where(id: id).first 118 | end 119 | 120 | protected 121 | 122 | def where(*args, **kwargs, &block) 123 | # TODO: Make this more robust. Allow passing model's class as module parameter. 124 | model_class = name.split("::").first.constantize 125 | super.map{ |x| model_class.new(x.attributes.transform_keys(&:to_sym)) } 126 | end 127 | 128 | # Don't let anyone new up a Repository themselves. 129 | private :new 130 | 131 | end 132 | 133 | end 134 | 135 | end 136 | 137 | end 138 | -------------------------------------------------------------------------------- /spec/integration/basic_model_spec.rb: -------------------------------------------------------------------------------- 1 | require "./spec/spec_helper" 2 | require "pry" 3 | require "active_support" 4 | require "active_record" 5 | require "active_record/entity" 6 | require "active_record/repository" 7 | 8 | 9 | # Whoa. I just learned about ":memory:" from https://blog.teamtreehouse.com/active-record-without-rails-app. 10 | # TODO: Add that to my PRO TIPS. 11 | ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:") 12 | connection_spec = ActiveRecord::ConnectionAdapters::ConnectionSpecification.new("primary", {adapter: "sqlite3", database: ":memory:"}, "sqlite3_connection") 13 | ActiveRecord::ConnectionAdapters::ConnectionPool.new(connection_spec) 14 | 15 | ActiveRecord::Schema.define do 16 | self.verbose = false 17 | 18 | create_table :users, force: true do |t| 19 | t.string :name, nil: false 20 | t.date :date_of_birth, nil: false 21 | t.integer :ssn, nil: true 22 | t.boolean :active, nil: false 23 | end 24 | end 25 | 26 | 27 | class User 28 | include ActiveModel.entity(datestamps: true) 29 | attribute :name, :string 30 | attribute :date_of_birth, :date # Gives you a validation for free 31 | attribute :ssn, :integer #, required: false # , range: 000_00_0000..999_99_9999 32 | attribute :active, :boolean 33 | validates :ssn, numericality: { greater_than_or_equal_to: 000_00_0000, less_than_or_equal_to: 999_99_9999, allow_nil: true } 34 | # has_many :children 35 | # belongs_to :company 36 | end 37 | 38 | class User::Repository 39 | include ActiveRecord.repository(model: User, table_name: "users") 40 | scope :active, -> { where(active: true) } 41 | end 42 | 43 | # class User::RepositoryWithMissingField 44 | # include ActiveRecord.repostitory(model: User, table_name: "users_with_missing_field") 45 | # scope :active, -> (){ where(active: true) } 46 | # end 47 | 48 | 49 | RSpec.describe ActiveModel::Entity do 50 | it "allows initializing with defined attributes" do 51 | expect { 52 | User.new(id: 1, name: "Craig", active: true, date_of_birth: Date.parse("1970-12-23")) 53 | }.not_to raise_exception 54 | end 55 | 56 | it "does NOT allow initializing with values for attributes that are not defined" do 57 | expect { 58 | User.new(id: 1, name: "Craig", active: true, date_of_birth: Date.parse("1970-12-23"), undefined_field: 123) 59 | }.to raise_exception(ActiveModel::UnknownAttributeError) 60 | end 61 | 62 | it "DOES allow initializing with values for attributes that are not defined, if we pass `ignore_extra_attributes: true`" do 63 | expect { 64 | User.new(id: 1, name: "Craig", active: true, date_of_birth: Date.parse("1970-12-23"), undefined_field: 123, ignore_extra_attributes: true) 65 | }.not_to raise_exception 66 | end 67 | 68 | it "allows updating attributes" do 69 | user = User.new(id: 1, name: "Craig", active: true, date_of_birth: Date.parse("1970-12-23")) 70 | expect { 71 | user.name = "Craig Buchek" 72 | expect(user.name).to eq("Craig Buchek") 73 | user.active = false 74 | expect(user.active).to eq(false) 75 | user.update(name: "Bob", active: true) 76 | expect(user.name).to eq("Bob") 77 | expect(user.active).to eq(true) 78 | }.not_to raise_exception 79 | end 80 | 81 | it "does NOT allow updating attributes that are not defined" do 82 | user = User.new(id: 1, name: "Craig", active: true, date_of_birth: Date.parse("1970-12-23")) 83 | expect { 84 | user.undefined_field = "nice try!" 85 | }.to raise_exception(NoMethodError) 86 | expect { 87 | user.update(undefined_field: "nice try!") 88 | }.to raise_exception(NoMethodError) 89 | end 90 | 91 | it "DOES allow updating attributes that are not defined, if we pass `ignore_extra_attributes: true`" do 92 | user = User.new(id: 1, name: "Craig", active: true, date_of_birth: Date.parse("1970-12-23")) 93 | expect { 94 | user.update(undefined_field: "nice try!", ignore_extra_attributes: true) 95 | }.not_to raise_exception 96 | end 97 | 98 | it "implements #persisted properly" do 99 | user = User.new(name: "Craig", active: true, date_of_birth: Date.parse("1970-12-23")) 100 | expect(user.persisted?).to be(false) 101 | user.update(id: 123) 102 | expect(user.persisted?).to be(true) 103 | end 104 | 105 | it "allows accessing 'has_many' relations" 106 | it "allows accessing 'belongs_to' relations" 107 | 108 | describe "validations" do 109 | 110 | it "requires setting all required attributes" do 111 | user = User.new(id: 1, active: true, date_of_birth: nil) 112 | expect(user).to_not be_valid 113 | user = User.new(id: 1, name: "Craig", active: true, date_of_birth: Date.parse("1970-12-23"), ssn: 123_45_6789) 114 | expect(user).to be_valid 115 | end 116 | 117 | it "allows specifying `validates`" do 118 | user = User.new(id: 1, name: "Craig", active: true, date_of_birth: Date.parse("1970-12-23")) 119 | user.update(ssn: 123_45_6789) 120 | expect(user).to be_valid 121 | end 122 | 123 | it "validates types" do 124 | user = User.new(id: 1, name: 12345, active: true, date_of_birth: "NOT A DATE", ssn: 123_45_6789) 125 | expect(user).not_to be_valid 126 | expect(user.errors[:date_of_birth]).to include("can't be blank") 127 | end 128 | 129 | it "allows `required: false` in `attribute` declaration" 130 | 131 | it "validates ranges specified in `attribute` declaration" 132 | 133 | end 134 | 135 | end 136 | 137 | 138 | RSpec.describe ActiveRecord::Repository do 139 | let(:new_user) { User.new(name: "Craig", active: true) } 140 | 141 | xit "errors out if the repostitory fields don't include all the model attributes" do 142 | expect { 143 | User::RepositoryWithMissingField.find(1) 144 | }.to raise_exception(ActiveRecord::UnknownAttributeError) 145 | end 146 | 147 | it "allows saving an entity" do 148 | expect { 149 | user = User.new(id: 1, name: "Craig", active: true, date_of_birth: Date.parse("1970-12-23"), ssn: 123_45_6789) 150 | User::Repository.save(user) 151 | }.not_to raise_exception 152 | end 153 | 154 | it "updates an entity's ID on save" do 155 | user = User.new(name: "Craig", active: false, date_of_birth: Date.parse("1970-12-23"), ssn: 123_45_6789) 156 | User::Repository.save(user) 157 | expect(user.id).not_to be_nil 158 | end 159 | 160 | it "allows getting an entity by ID" do 161 | User::Repository.save(new_user) 162 | user = User::Repository.find(1) 163 | expect(user).to be_a(User) 164 | expect(user.name).to eq("Craig") 165 | end 166 | 167 | it "allows getting entities by scope" do 168 | User::Repository.save(new_user) 169 | active_users = User::Repository.active 170 | expect(active_users.methods).to include(:each) 171 | expect(active_users.length).to eq(1) 172 | # expect(active_users.first).to be(new_user) 173 | end 174 | 175 | it { expect { User::Repository.save(1) }.to raise_error(ArgumentError) } 176 | 177 | it "DOES NOT allow newing up an object" do 178 | expect { 179 | User::Repository.new 180 | }.to raise_error(NoMethodError) 181 | end 182 | 183 | it "does not allow directly accessing collections that should be accessed via scopes" do 184 | expect { 185 | User::Repository.where(active: true) 186 | }.to raise_exception(NoMethodError) 187 | end 188 | end 189 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # This file was generated by the `rspec --init` command. Conventionally, all 2 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. 3 | # The generated `.rspec` file contains `--require spec_helper` which will cause 4 | # this file to always be loaded, without a need to explicitly require it in any 5 | # files. 6 | # 7 | # Given that it is always loaded, you are encouraged to keep this file as 8 | # light-weight as possible. Requiring heavyweight dependencies from this file 9 | # will add to the boot time of your test suite on EVERY test run, even for an 10 | # individual file that may not need all of that loaded. Instead, consider making 11 | # a separate helper file that requires the additional dependencies and performs 12 | # the additional setup, and require it from the spec files that actually need 13 | # it. 14 | # 15 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 16 | RSpec.configure do |config| 17 | config.around do |example| 18 | ActiveRecord::Base.transaction do 19 | example.run 20 | raise ActiveRecord::Rollback 21 | end 22 | end 23 | 24 | # rspec-expectations config goes here. You can use an alternate 25 | # assertion/expectation library such as wrong or the stdlib/minitest 26 | # assertions if you prefer. 27 | config.expect_with :rspec do |expectations| 28 | # This option will default to `true` in RSpec 4. It makes the `description` 29 | # and `failure_message` of custom matchers include text for helper methods 30 | # defined using `chain`, e.g.: 31 | # be_bigger_than(2).and_smaller_than(4).description 32 | # # => "be bigger than 2 and smaller than 4" 33 | # ...rather than: 34 | # # => "be bigger than 2" 35 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 36 | end 37 | 38 | # rspec-mocks config goes here. You can use an alternate test double 39 | # library (such as bogus or mocha) by changing the `mock_with` option here. 40 | config.mock_with :rspec do |mocks| 41 | # Prevents you from mocking or stubbing a method that does not exist on 42 | # a real object. This is generally recommended, and will default to 43 | # `true` in RSpec 4. 44 | mocks.verify_partial_doubles = true 45 | end 46 | 47 | # This option will default to `:apply_to_host_groups` in RSpec 4 (and will 48 | # have no way to turn it off -- the option exists only for backwards 49 | # compatibility in RSpec 3). It causes shared context metadata to be 50 | # inherited by the metadata hash of host groups and examples, rather than 51 | # triggering implicit auto-inclusion in groups with matching metadata. 52 | config.shared_context_metadata_behavior = :apply_to_host_groups 53 | 54 | # This allows you to limit a spec run to individual examples or groups 55 | # you care about by tagging them with `:focus` metadata. When nothing 56 | # is tagged with `:focus`, all examples get run. RSpec also provides 57 | # aliases for `it`, `describe`, and `context` that include `:focus` 58 | # metadata: `fit`, `fdescribe` and `fcontext`, respectively. 59 | config.filter_run_when_matching :focus 60 | 61 | # Allows RSpec to persist some state between runs in order to support 62 | # the `--only-failures` and `--next-failure` CLI options. We recommend 63 | # you configure your source control system to ignore this file. 64 | config.example_status_persistence_file_path = "spec/examples.txt" 65 | 66 | # Limits the available syntax to the non-monkey patched syntax that is 67 | # recommended. For more details, see: 68 | # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/ 69 | # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ 70 | # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode 71 | config.disable_monkey_patching! 72 | 73 | # This setting enables warnings. It's recommended, but in some cases may 74 | # be too noisy due to issues in dependencies. 75 | config.warnings = true 76 | 77 | # Many RSpec users commonly either run the entire suite or an individual 78 | # file, and it's useful to allow more verbose output when running an 79 | # individual spec file. 80 | if config.files_to_run.one? 81 | # Use the documentation formatter for detailed output, 82 | # unless a formatter has already been configured 83 | # (e.g. via a command-line flag). 84 | config.default_formatter = "doc" 85 | end 86 | 87 | # Print the 10 slowest examples and example groups at the 88 | # end of the spec run, to help surface which specs are running 89 | # particularly slow. 90 | config.profile_examples = 10 91 | 92 | # Run specs in random order to surface order dependencies. If you find an 93 | # order dependency and want to debug it, you can fix the order by providing 94 | # the seed, which is printed after each run. 95 | # --seed 1234 96 | config.order = :random 97 | 98 | # Seed global randomization in this process using the `--seed` CLI option. 99 | # Setting this allows you to use `--seed` to deterministically reproduce 100 | # test failures related to randomization by passing the same `--seed` value 101 | # as the one that triggered the failure. 102 | Kernel.srand config.seed 103 | end 104 | --------------------------------------------------------------------------------