├── .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 {
}[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
--------------------------------------------------------------------------------