├── .gitignore ├── .rspec ├── CHANGELOG.md ├── Gemfile ├── LICENSE.txt ├── README.rdoc ├── Rakefile ├── features └── rspec-set.feature ├── lib ├── rspec-set.rb └── version.rb ├── rspec-set.gemspec └── spec ├── db └── migrate │ └── 01_create_active_record_class_example.rb ├── fixtures ├── active_record_class_example.rb └── non_active_record_class.rb ├── rspec_set_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | *.sqlite3 -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format documentation -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # rspec-set CHANGELOG 2 | 3 | ## 0.1.3 4 | 5 | * prevent name conflict by [@jdelStrother][] (PR [#6][]) 6 | * 7 | ## 0.1.2 8 | 9 | * fix reload destroy models by [@pcreux][] 10 | 11 | ## 0.1.1 12 | 13 | * add test suite by [@loganhasson][] (PR [#4][]) 14 | * fix relig destroyed models by [@loganhasson][] (PR [#5][]) 15 | 16 | ## 0.1.0 17 | 18 | * only reload persisted models 19 | * relig destroyed models 20 | * warn when used with non active record objects 21 | * add support for `rspec >= 2.12` by [@21croissants][] 22 | 23 | ## 0.0.1 24 | 25 | * create model in `#before(:all)` statement 26 | * reload model in `#before(:each)` statement 27 | * make model available via `#let()` 28 | 29 | 30 | [#4]: https://github.com/pcreux/rspec-set/issues/4 31 | [#5]: https://github.com/pcreux/rspec-set/issues/5 32 | [#6]: https://github.com/pcreux/rspec-set/issues/6 33 | [@21croissants]: https://github.com/21croissants 34 | [@jdelStrother]: https://github.com/jdelStrother 35 | [@loganhasson]: https://github.com/loganhasson 36 | [@pcreux]: https://github.com/pcreux -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in rspec-set.gemspec 4 | gemspec -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Philippe Creux 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 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = rspec-set {Build Status}[https://travis-ci.org/pcreux/rspec-set] 2 | 3 | #set is a little RSpec helper that speeds-up drastically integration tests that relies on active record objects. 4 | 5 | #set takes advantage of the fact that RSpec rails runs each examples in SQL transactions. 6 | Since all the changes made to the database are rolledback after each example we can create an active record object before all examples 7 | and use it in each examples without any collisions as long as we reload the object from the database before each example. 8 | 9 | #set can be used as a replacement of #let: #set will create the resource before(:all) your examples and will reload the resource before(:each) example. 10 | 11 | You can drastically improve the time spent to run your specs. On an application with 3000 examples we decreased the specs duration by 70%! 12 | 13 | == Usage 14 | 15 | The following code will create one (and only one!) flight before running the examples and reload the flight from the DB before each example. 16 | 17 | require 'spec_helper' 18 | 19 | describe Flight do 20 | set(:flight) do 21 | Flight.create! 22 | end 23 | 24 | it "should be on_time" do 25 | flight.should be_on_time 26 | end 27 | 28 | it "should be cancellable" do 29 | flight.cancel 30 | flight.should be_cancelled 31 | end 32 | 33 | it "should be delayable" do 34 | flight.delay 35 | flight.should be_delayed 36 | end 37 | end 38 | 39 | === How does that work? 40 | 41 | RSpec wraps each example in an SQL transaction which gets rolled back at the end of each example. 42 | 43 | #set creates a flight once before running any example. Each example uses this flight and changes its state. Since RSpec rolls back the SQL transaction, the flight gets back to its initial state before each example. #set takes care of reloading the flight from the DB before each example. Examples won't affect each others then. 44 | 45 | You can find more examples in https://github.com/pcreux/rspec-set/blob/master/features/lib/rspec-set.feature 46 | 47 | == Notes 48 | 49 | * #set works only with ActiveRecord objects saved to the DB so far. 50 | * The record created by #set won't be deleted from the database. I encourage you to use DatabaseCleaner with the :truncation strategy to clean up your database. So far in RSpec 2.0, before(:all) runs before all describe/context blocks while after(:all) runs after every single describe/context/it blocks. Just make sure that you call DatabaseCleaner.clean in a before(:all) or after(:suite) then. :) 51 | * #set does not handle multi-level transactions. 52 | * You will have to call DatabaseCleaner.clean before(:all) specs which rely on having an empty database. #set don't clean the database for you. 53 | 54 | == Install 55 | 56 | Add rspec-set to you Gemfile 57 | 58 | gem 'rspec-set' 59 | 60 | and replace calls to #let creating active record objects by #set. 61 | 62 | == TODO 63 | 64 | * support non saved active record objects (changes made to non saved active record objects won't be rolledback after each example) 65 | * make before(:all) running in a transaction - See: http://rhnh.net/2010/10/06/transactional-before-all-with-rspec-and-datamapper 66 | * support multi-level transactions (combinations of subcontext with set and changes made in before(:all) leads to weird behaviour sometimes 67 | 68 | 69 | == Contributing to rspec-set 70 | 71 | * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet 72 | * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it 73 | * Fork the project 74 | * Start a feature/bugfix branch 75 | * Commit and push until you are happy with your contribution 76 | * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally. 77 | * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it. 78 | 79 | == Copyright 80 | 81 | Copyright (c) 2010 Philippe Creux. See LICENSE.txt for 82 | further details. 83 | 84 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require 'rspec/core/rake_task' 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task :default => :spec 7 | -------------------------------------------------------------------------------- /features/rspec-set.feature: -------------------------------------------------------------------------------- 1 | Feature: set 2 | 3 | As a rails developer willing to speed-up my integration tests 4 | In order to take advantage of examples running in transactions 5 | I want to use #set that store the data before all example and 6 | reload active record objects before each examples 7 | 8 | Scenario: Examples run in transactions (no side effects between examples) 9 | Given a file named "spec/models/widget_spec.rb" with: 10 | """ 11 | require "spec_helper" 12 | 13 | describe Widget do 14 | set(:widget) { Widget.create!(:name => 'widget_1') } 15 | 16 | subject { widget } 17 | 18 | context "when name is changed to 'widget_2" do 19 | before do 20 | widget.update_attributes!(:name => 'widget_2') 21 | end 22 | 23 | its(:name) { should == 'widget_2' } 24 | end 25 | 26 | context "when name is 'widget_1" do 27 | its(:name) { should == 'widget_1' } 28 | end 29 | end 30 | """ 31 | When I run "rspec spec/models/widget_spec.rb" 32 | Then the examples should all pass 33 | 34 | Scenario: We can use sub sub contexts just fine 35 | Given a file named "spec/models/widget_spec.rb" with: 36 | """ 37 | require "spec_helper" 38 | 39 | describe Widget do 40 | set(:widget) { Widget.create(:name => 'apple') } 41 | 42 | subject { widget } 43 | 44 | context "when name is changed to 'banana" do 45 | before do 46 | widget.update_attributes!(:name => 'banana') 47 | end 48 | 49 | its(:name) { should == 'banana' } 50 | 51 | context "when we append ' is good'" do 52 | before do 53 | widget.name << ' is good' 54 | widget.save! 55 | end 56 | 57 | its(:name) { should == 'banana is good' } 58 | end 59 | 60 | context "when we append ' is bad'" do 61 | before do 62 | widget.name << ' is bad' 63 | widget.save! 64 | end 65 | 66 | its(:name) { should == 'banana is bad' } 67 | 68 | context "when we append ' for you'" do 69 | before do 70 | widget.name << ' for you' 71 | widget.save! 72 | end 73 | 74 | its(:name) { should == 'banana is bad for you' } 75 | end 76 | end 77 | end 78 | 79 | context "when name is 'apple" do 80 | its(:name) { should == 'apple' } 81 | end 82 | end 83 | """ 84 | When I run "rspec spec/models/widget_spec.rb" 85 | Then the examples should all pass 86 | 87 | 88 | Scenario: I can delete an object, it will be available in the next example. 89 | Given a file named "spec/models/widget_spec.rb" with: 90 | """ 91 | require "spec_helper" 92 | 93 | describe Widget do 94 | set(:widget) { Widget.create(:name => 'apple') } 95 | 96 | subject { widget } 97 | 98 | context "when I destroy the widget" do 99 | before do 100 | widget.destroy 101 | end 102 | 103 | it "should be destroyed" do 104 | Widget.find_by_id(widget.id).should be_nil 105 | end 106 | end 107 | 108 | context "when name is 'apple" do 109 | its(:name) { should == 'apple' } 110 | end 111 | end 112 | """ 113 | When I run "rspec spec/models/widget_spec.rb" 114 | Then the examples should all pass 115 | 116 | Scenario: I can update a model in a before block 117 | 118 | Scenario: I can use a set model in another set definition 119 | -------------------------------------------------------------------------------- /lib/rspec-set.rb: -------------------------------------------------------------------------------- 1 | require "version" 2 | 3 | module RSpec 4 | module Core 5 | module RSpecSet 6 | module ClassMethods 7 | # Set @variable_name in a before(:all) block and give access to it 8 | # via let(:variable_name) 9 | # 10 | # Example: 11 | # 12 | # set(:transaction) { Factory(:address) } 13 | # 14 | # it "should be valid" do 15 | # transaction.should be_valid 16 | # end 17 | # 18 | def set(variable_name, &block) 19 | before(:all) do 20 | # Create model 21 | self.class.send(:class_variable_set, "@@__rspec_set_#{variable_name}".to_sym, instance_eval(&block)) 22 | end 23 | 24 | before(:each) do 25 | model = send(variable_name) 26 | 27 | if model.is_a?(ActiveRecord::Base) 28 | if model.destroyed? 29 | # Reset destroyed model 30 | self.class.send(:class_variable_set, "@@__rspec_set_#{variable_name}".to_sym, model.class.find(model.id)) 31 | elsif !model.new_record? 32 | # Reload saved model 33 | model.reload 34 | end 35 | else 36 | warn "#{variable_name} is a #{model.class} - rspec-set works with ActiveRecord models only" 37 | end 38 | end 39 | 40 | define_method(variable_name) do 41 | self.class.send(:class_variable_get, "@@__rspec_set_#{variable_name}".to_sym) 42 | end 43 | end # set() 44 | 45 | end # ClassMethods 46 | 47 | def self.included(mod) # :nodoc: 48 | mod.extend ClassMethods 49 | end 50 | end # RSpecSet 51 | 52 | class ExampleGroup 53 | include RSpecSet 54 | end # ExampleGroup 55 | 56 | end # Core 57 | end # RSpec 58 | -------------------------------------------------------------------------------- /lib/version.rb: -------------------------------------------------------------------------------- 1 | module Rspec 2 | module RSpecSet 3 | VERSION = "0.1.3" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /rspec-set.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "rspec-set" 8 | spec.version = Rspec::RSpecSet::VERSION 9 | spec.authors = ["Philippe Creux"] 10 | spec.email = ["pcreux@gmail.com"] 11 | spec.description = "#set(), speed-up your specs" 12 | spec.summary = "#set() is a helper for RSpec which setup active record 13 | objects before all tests and restore them to there original state 14 | before each test" 15 | spec.homepage = "http://github.com/pcreux/rspec-set" 16 | spec.license = "MIT" 17 | 18 | spec.files = `git ls-files`.split($/) 19 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 20 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 21 | spec.require_paths = ["lib"] 22 | 23 | spec.add_development_dependency "bundler", "~> 1.3" 24 | spec.add_development_dependency "rake" 25 | spec.add_development_dependency "rspec", "~> 2.14.1" 26 | spec.add_development_dependency "database_cleaner" 27 | spec.add_development_dependency "activerecord" 28 | spec.add_development_dependency "sqlite3" 29 | end 30 | -------------------------------------------------------------------------------- /spec/db/migrate/01_create_active_record_class_example.rb: -------------------------------------------------------------------------------- 1 | class CreateActiveRecordClassExample < ActiveRecord::Migration 2 | def change 3 | create_table :active_record_class_examples do |t| 4 | t.string :name 5 | t.integer :age 6 | end 7 | end 8 | end -------------------------------------------------------------------------------- /spec/fixtures/active_record_class_example.rb: -------------------------------------------------------------------------------- 1 | class ActiveRecordClassExample < ActiveRecord::Base 2 | end -------------------------------------------------------------------------------- /spec/fixtures/non_active_record_class.rb: -------------------------------------------------------------------------------- 1 | class NonActiveRecordClass 2 | attr_accessor :name 3 | 4 | def initialize(attributes) 5 | self.name = attributes[:name] 6 | end 7 | 8 | def self.create(attributes) 9 | new(attributes) 10 | end 11 | end -------------------------------------------------------------------------------- /spec/rspec_set_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'including Set' do 4 | it 'adds the ::set method to RSpec::Core::ExampleGroup' do 5 | expect(RSpec::Core::ExampleGroup).to respond_to(:set) 6 | end 7 | end 8 | 9 | describe 'without an ActiveRecord model' do 10 | setup_for_error_checking(NonActiveRecordClass) 11 | 12 | it "warns the user that Set only works with AR models" do 13 | $stderr.rewind 14 | expect($stderr.string.chomp).to eq( 15 | "my_object is a NonActiveRecordClass - rspec-set works with ActiveRecord models only" 16 | ) 17 | end 18 | end 19 | 20 | describe 'with an ActiveRecord model' do 21 | setup_for_error_checking(ActiveRecordClassExample) 22 | 23 | it "doesn't give a warning to the user" do 24 | $stderr.rewind 25 | expect($stderr.string.chomp).to be_empty 26 | end 27 | 28 | it 'creates a method based on the argument to ::set' do 29 | expect(self).to respond_to(:my_object) 30 | end 31 | end 32 | 33 | describe 'with a destroyed ActiveRecord model' do 34 | set(:my_destroyed_object) do 35 | ActiveRecordClassExample.create(name: 'Alfred', age: 77) 36 | end 37 | 38 | it 'allows us to destroy a model' do 39 | my_destroyed_object.destroy 40 | expect( 41 | ActiveRecordClassExample.find_by(id: my_destroyed_object.id) 42 | ).to be_nil 43 | end 44 | 45 | it 'reloads a destroyed model' do 46 | expect(my_destroyed_object.reload.name).to eq('Alfred') 47 | end 48 | end 49 | 50 | describe 'with a stale model' do 51 | set(:my_stale_object) do 52 | ActiveRecordClassExample.create(name: 'Old Name', age: 18) 53 | end 54 | 55 | it 'allows us to play with the model' do 56 | my_stale_object.update(name: 'New Name') 57 | expect(ActiveRecordClassExample.find(my_stale_object.id).name).to eq( 58 | 'New Name' 59 | ) 60 | end 61 | 62 | it 'reloads the stale model' do 63 | expect(my_stale_object.name).to eq('Old Name') 64 | end 65 | end 66 | 67 | 68 | describe ActiveRecordClassExample do 69 | set(:ar_class_example) { ActiveRecordClassExample.create(name: 'ex_1') } 70 | 71 | subject { ar_class_example } 72 | 73 | context "when name is changed to 'ex_2" do 74 | before do 75 | ar_class_example.update(name: 'ex_2') 76 | end 77 | 78 | it 'updates the name' do 79 | expect(subject.name).to eq('ex_2') 80 | end 81 | end 82 | 83 | context "when name is 'ex_1" do 84 | it 'reloads the original name' do 85 | expect(subject.name).to eq('ex_1') 86 | end 87 | end 88 | end 89 | 90 | describe 'sub sub contexts' do 91 | set(:ar_class_example) { ActiveRecordClassExample.create(name: 'apple') } 92 | 93 | subject { ar_class_example } 94 | 95 | context "when name is changed to 'banana'" do 96 | before do 97 | ar_class_example.update(name: 'banana') 98 | end 99 | 100 | it 'updates the name' do 101 | expect(subject.name).to eq('banana') 102 | end 103 | 104 | context "when we append ' is good'" do 105 | before do 106 | ar_class_example.name << ' is good' 107 | ar_class_example.save 108 | end 109 | 110 | it 'updates the appended name' do 111 | expect(subject.name).to eq('banana is good') 112 | end 113 | end 114 | 115 | context "when we append ' is bad'" do 116 | before do 117 | ar_class_example.name << ' is bad' 118 | ar_class_example.save 119 | end 120 | 121 | it 'also updates the appended name' do 122 | expect(subject.name).to eq('banana is bad') 123 | end 124 | 125 | context "when we append ' for you'" do 126 | before do 127 | ar_class_example.name << ' for you' 128 | ar_class_example.save 129 | end 130 | 131 | it 'contains the full sentence' do 132 | expect(subject.name).to eq('banana is bad for you') 133 | end 134 | end 135 | end 136 | end 137 | 138 | context "when name is 'apple'" do 139 | it 'reloads the original name' do 140 | expect(subject.name).to eq('apple') 141 | end 142 | end 143 | end 144 | 145 | describe 'deleting an object' do 146 | set(:ar_class_example) { ActiveRecordClassExample.create(name: 'apple') } 147 | 148 | subject { ar_class_example } 149 | 150 | context "when I destroy the ar_class_example" do 151 | before do 152 | ar_class_example.destroy 153 | end 154 | 155 | it "is destroyed" do 156 | expect(ActiveRecordClassExample.find_by_id(ar_class_example.id)).to be_nil 157 | end 158 | end 159 | 160 | context "when name is 'apple'" do 161 | it 'is reloaded from the database' do 162 | expect(subject.name).to eq('apple') 163 | end 164 | end 165 | end 166 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rspec-set' 2 | require 'database_cleaner' 3 | require 'active_record' 4 | 5 | require_relative './fixtures/non_active_record_class' 6 | require_relative './fixtures/active_record_class_example' 7 | 8 | RSpec.configure do |config| 9 | config.treat_symbols_as_metadata_keys_with_true_values = true 10 | config.run_all_when_everything_filtered = true 11 | config.filter_run :focus 12 | 13 | config.before(:suite) do 14 | setup_database 15 | end 16 | 17 | config.after(:suite) do 18 | destroy_database 19 | end 20 | 21 | config.before(:each) do 22 | DatabaseCleaner.strategy = :transaction 23 | DatabaseCleaner.start 24 | end 25 | 26 | config.after(:each) do 27 | DatabaseCleaner.clean 28 | end 29 | 30 | config.order = 'random' 31 | end 32 | 33 | def db 34 | ActiveRecord::Base.connection 35 | end 36 | 37 | def setup_database 38 | ActiveRecord::Base.establish_connection( 39 | :adapter => 'sqlite3', 40 | :database => 'spec/db/rspec-set-test.sqlite3' 41 | ) 42 | 43 | db.tables.each do |table| 44 | db.execute("DROP TABLE #{table}") 45 | end 46 | 47 | Dir[File.join(File.dirname(__FILE__), "db/migrate", "*.rb")].each do |f| 48 | require f 49 | migration = Kernel.const_get(f.split("/").last.split(".rb").first.gsub(/\d+/, "").split("_").collect{|w| w.strip.capitalize}.join()) 50 | migration.migrate(:up) 51 | end 52 | end 53 | 54 | def destroy_database 55 | db.tables.each do |table| 56 | db.execute("DROP TABLE #{table}") 57 | end 58 | end 59 | 60 | def setup_for_error_checking(klass) 61 | before do 62 | @orig_stderr = $stderr 63 | $stderr = StringIO.new 64 | end 65 | 66 | set(:my_object) { klass.create(name: 'Test Name') } 67 | 68 | after do 69 | $stderr = @orig_stderr 70 | end 71 | end --------------------------------------------------------------------------------