active-fixtures 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 54bd0172edecb818492c68d1ee90d4c6c3fc855c
4
+ data.tar.gz: ba2b97ec7dc4c14a95765dfd98dc56e0e85cb017
5
+ SHA512:
6
+ metadata.gz: c41606271c058aa0e688c1aba385054eac7e68609d44ef8f8ebddba15caaf6755a7b1b57b33c848ff9c78a1b7f2938c4521f5560d0677a33387b91b49dfb90f3
7
+ data.tar.gz: 735065ee98e5e34df977c6a607067ea0465b611d89850534195ffdc94f15260cf63b70cca75ede71996d7b1ab85141d893baf5417de1a738e6c68cbdf619884c
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Sergey Tokarenko
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.
@@ -0,0 +1,175 @@
1
+ ActiveFixtures
2
+ ==============
3
+ [![Version](https://badge.fury.io/rb/active-fixtures.svg)](http://badge.fury.io/rb/active-fixtures)
4
+
5
+ ActiveFixtures provides the way how to populate the server state (DB, sessions) as an application user but not as programmer.
6
+
7
+ ## Why?
8
+ The correct question is `why we write the tests at all?`.
9
+ Or even better - `what does the green line means?`.
10
+
11
+ Typical legacy code contains a tons of tests in isolation aka Unit Tests.
12
+ Mocks, stubs, factories & terrible traits, fakers etc etc.
13
+ Many smart things, so called `best practices`, a lot of efforts and time spent to write all of these.
14
+
15
+ And, after all - application just can't run due to the simple misspelling in `routes.rb`.
16
+
17
+ So, what is a really valuable reason to write the tests?
18
+
19
+ We working on the project for somebody personal.
20
+ We creating the web application for web users, we write the new cool library for other programmers.
21
+ The primary goal of automatic tests is to be sure that `our code works right like the target user expects`.
22
+
23
+ Test web application as web user plays with it.
24
+ Test the public methods of your library just like other programmer will use them.
25
+ Test web service endpoints just like the third-party applications will call them.
26
+
27
+ Nobody interested how exactly working the private methods in `Product` class.
28
+ Even more, nobody interested to know that class `Product` exists, it mapped to some database table etc.
29
+
30
+ Single thing is matter - how the application's user thinking about your application,
31
+ which entities he understand, how he affects to such entities.
32
+
33
+ In general case, tests in isolation are at least useless, at max - hurtful.
34
+ Lets write acceptance tests instead!
35
+
36
+ ## requirements
37
+ Currently works with Poltergeist and PostgreSQL.
38
+
39
+ ## Getting started
40
+
41
+ Add to your Gemfile:
42
+
43
+ ```ruby
44
+ gem 'ruby-features', group: :test
45
+ ```
46
+
47
+ Add to your rspec helper:
48
+ ```ruby
49
+ require 'active-fixtures/rspec'
50
+ ```
51
+
52
+ Remove Database Cleaners from your project, ActiveFixtures will take care about database cleanup in additional.
53
+
54
+ ## Usage
55
+ ### Active factory definition
56
+ Lets add the active factory in `spec/active_fixtures/user.rb`:
57
+
58
+ ```ruby
59
+ class AFUser < ActiveFixtures::Resource
60
+ attribute :login, type: String, default: 'admin@lvh.me'
61
+ attribute :password, type: String, default: 'p@ssw0rd'
62
+
63
+ def self.create_initial(attrs = {})
64
+ new(attrs).tap { |user|
65
+ Rake::Task['user:create'].invoke(user.login, user.password)
66
+ Rake::Task['user:create'].reenable
67
+ }
68
+ end
69
+
70
+ def self.create(attrs = {})
71
+ new(attrs).tap { |user|
72
+ af_session(:admin) do
73
+ click_on 'Admins'
74
+
75
+ click_on 'Invite'
76
+ fill_in 'Email', with: user.login
77
+ click_on 'Send an invitation'
78
+ end
79
+
80
+ af_session do
81
+ open_email(user.login)
82
+
83
+ current_email.click_on 'Accept invitation'
84
+ fill_in 'Password', with: user.password
85
+ fill_in 'Password confirmation', with: user.password
86
+ click_on 'Set my password'
87
+ assert_text 'Your password was set successfully. You are now signed in.'
88
+ end
89
+ }
90
+ end
91
+
92
+ def sign_in
93
+ visit '/'
94
+ fill_in 'Email', with: login
95
+ fill_in 'Password', with: password
96
+ click_on 'Sign in'
97
+ assert_text 'Signed in successfully.'
98
+ end
99
+
100
+ end
101
+ ```
102
+
103
+ Implement the resource factory just like the application user will do -
104
+ capybara steps, rake tasks invocation, API calls etc.
105
+
106
+ Any methods available in `it` rspec context will be available in the factory
107
+ (for example `open_email` helper from `capybara-email` gem).
108
+
109
+ `ActiveFixtures::Resource` includes the [ActiveAttr::Model](https://github.com/cgriego/active_attr),
110
+ feel free to use any it's features.
111
+
112
+ `af_session` helper called without parameter will provide the clean capybara session on each call.
113
+ Use it in factory to avoid the influence to rspec's example default session.
114
+
115
+ `af_session` with parameter is a bit tricky, lets recall it later.
116
+
117
+ Now you can use factory in your tests:
118
+ ```ruby
119
+ describe 'Users' do
120
+ let(:admin) { AFUser.create_initial }
121
+
122
+ it 'should pass' do
123
+ admin.sing_in
124
+ end
125
+ end
126
+ ```
127
+
128
+ ### Fixtures definition
129
+ Lets define active fixture in `spec/active_fixtures/_fixtures.rb`:
130
+
131
+ ```ruby
132
+ ActiveFixtures.populate(:default) do
133
+ resource(:admin) { AFUser.create_initial }
134
+ session(:admin) { AFUser[:admin].sign_in }
135
+ resource(:invited_admin) { AFUser.create(login: 'new_user@lvh.me') }
136
+ end
137
+ ```
138
+
139
+ `:default` fixture will be loaded before each rspec example. You can play with resources like that:
140
+ ```ruby
141
+ it 'should pass' do
142
+ AFUser[:invited_admin].sign_in
143
+ end
144
+ ```
145
+
146
+ ### Active sessions
147
+ Once you defined the named session in factory, you can use it by `af_session` helper:
148
+ ```ruby
149
+ it 'should pass' do
150
+ af_session(:admin) do
151
+ # already logged as admin
152
+ end
153
+
154
+ # some code
155
+ af_session(:admin) do
156
+ # second and any further `af_session` call within the same example
157
+ # will drop the session to the state reached right after the session initialization
158
+ # - same current URL, same cookies. This way you don't need to keep in mind
159
+ # what you did with named session before, but can expect the same session state each time.
160
+ end
161
+
162
+ af_session do
163
+ # always clean session, with blank current URL
164
+ end
165
+ end
166
+ ```
167
+
168
+ ### How it works
169
+ ActiveFixtures work fairly, but effectively.
170
+
171
+ Fixture will be populated on the first it's usage, by genuine application's user actions.
172
+ For the next example it will be loaded from cache - no sense to to the same work again.
173
+
174
+ ## License
175
+ MIT License. Copyright (c) 2016 Sergey Tokarenko
@@ -0,0 +1,45 @@
1
+ require 'active-fixtures/version'
2
+
3
+ module ActiveFixtures
4
+ autoload :Resource, 'active-fixtures/resource'
5
+
6
+ autoload :Session, 'active-fixtures/session'
7
+ autoload :StateBuilder, 'active-fixtures/state_builder'
8
+ autoload :State, 'active-fixtures/state'
9
+ autoload :StateDumper, 'active-fixtures/state_dumper'
10
+
11
+ mattr_accessor :state_builders
12
+ self.state_builders = {}
13
+
14
+ class << self
15
+ def populate(name, &block)
16
+ state_builders[name] = StateBuilder.new(block)
17
+ end
18
+
19
+ def init!
20
+ StateDumper.init!
21
+ end
22
+
23
+ def cleanup!
24
+ StateDumper.cleanup!
25
+ end
26
+
27
+ def prepare!(name)
28
+ self.current_state = State.new(name)
29
+ current_state.prepare!(state_builders[name])
30
+ end
31
+
32
+ def thread_storage
33
+ Thread.current[:__active_fixtures] ||= {}
34
+ end
35
+
36
+ def current_state
37
+ thread_storage[:current_state]
38
+ end
39
+
40
+ def current_state=(state)
41
+ thread_storage[:current_state] = state
42
+ end
43
+
44
+ end
45
+ end
@@ -0,0 +1,25 @@
1
+ module ActiveFixtures
2
+ class Resource
3
+ include ActiveAttr::Model
4
+
5
+ delegate :context, to: :class
6
+
7
+ def self.[](name)
8
+ ActiveFixtures.current_state.read_entity(name, self)
9
+ end
10
+
11
+ private
12
+
13
+ def self.context
14
+ RSpec.current_example.example_group_instance
15
+ end
16
+
17
+ def self.method_missing(method, *args, &block)
18
+ context.respond_to?(method) ?
19
+ context.public_send(method, *args, &block) :
20
+ super
21
+ end
22
+
23
+ define_method(:method_missing, &method(:method_missing))
24
+ end
25
+ end
@@ -0,0 +1,19 @@
1
+ Dir[Rails.root.join('spec/active_fixtures/**/*.rb')].each { |f| require f }
2
+
3
+ RSpec.configure do |config|
4
+ config.include ActiveFixtures::Session::Helper
5
+
6
+ config.around(:each) do |example|
7
+ ActiveFixtures.prepare!(:default)
8
+ example.run
9
+ end
10
+
11
+ config.before(:suite) do
12
+ ActiveFixtures.init!
13
+ end
14
+
15
+ config.after(:suite) do
16
+ ActiveFixtures.cleanup!
17
+ end
18
+
19
+ end
@@ -0,0 +1,51 @@
1
+ module ActiveFixtures
2
+ class Session < Resource
3
+ CLEAN_NAME = :__clean
4
+
5
+ module Helper
6
+ def af_session(name = CLEAN_NAME, &block)
7
+ Session[name].perform(block)
8
+ end
9
+ end
10
+
11
+ attribute :name, type: String, default: CLEAN_NAME
12
+ attribute :url, type: String
13
+ attribute :cookies, type: Object, default: []
14
+
15
+ def initialize(attrs = {})
16
+ super
17
+
18
+ cookies.map!(&:symbolize_keys!)
19
+
20
+ if attrs[:block]
21
+ using_session do
22
+ attrs[:block].call
23
+
24
+ self.url = context.current_url
25
+ self.cookies = context.page.driver.cookies.values.map{ |c| c.instance_variable_get(:@attributes).symbolize_keys}
26
+ end
27
+ end
28
+ end
29
+
30
+ def perform(block)
31
+ res = nil
32
+
33
+ using_session do
34
+ context.reset_session!
35
+ cookies.each do |cookie|
36
+ context.page.driver.set_cookie(nil, nil, cookie)
37
+ end
38
+ context.visit(url) if url
39
+ res = block.call
40
+ end
41
+
42
+ res
43
+ end
44
+
45
+ private
46
+
47
+ def using_session(&block)
48
+ context.using_session("__af::#{name}", &block)
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,47 @@
1
+ module ActiveFixtures
2
+ class State
3
+ attr_accessor :name
4
+
5
+ def initialize(_name)
6
+ self.name = _name
7
+ end
8
+
9
+ def prepare!(state_builder)
10
+ entities.clear
11
+
12
+ if StateDumper.exists?(name)
13
+ entities.merge!(StateDumper.load(name))
14
+ else
15
+ StateDumper.load_clean
16
+
17
+ clean_session = Session.new
18
+ write_entity(clean_session.name, clean_session)
19
+
20
+ state_builder.prepare_each do |name, resource|
21
+ write_entity(name, resource)
22
+ end
23
+
24
+ StateDumper.dump(self)
25
+ end
26
+ end
27
+
28
+ def read_entity(name, resource_class)
29
+ entities[normalize_name(name, resource_class)]
30
+ end
31
+
32
+ def entities
33
+ ActiveFixtures.thread_storage[:entities] ||= {}
34
+ end
35
+
36
+ private
37
+
38
+ def write_entity(name, resource)
39
+ entities[normalize_name(name, resource.class)] = resource
40
+ end
41
+
42
+ def normalize_name(name, resource_class)
43
+ "#{resource_class.name}::#{name}"
44
+ end
45
+
46
+ end
47
+ end
@@ -0,0 +1,37 @@
1
+ module ActiveFixtures
2
+ class StateBuilder
3
+ attr_accessor :steps
4
+
5
+ def initialize(block)
6
+ self.steps = []
7
+ instance_eval(&block)
8
+ end
9
+
10
+ def prepare_each
11
+ steps.each do |build_step|
12
+ yield(
13
+ build_step[:name],
14
+ send("build_#{build_step[:type]}", build_step)
15
+ )
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def resource(name, &block)
22
+ steps << {type: :resource, name: name, block: block}
23
+ end
24
+
25
+ def session(name, &block)
26
+ steps << {type: :session, name: name, block: block}
27
+ end
28
+
29
+ def build_resource(build_step)
30
+ build_step[:block].call
31
+ end
32
+
33
+ def build_session(build_step)
34
+ Session.new(build_step)
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,67 @@
1
+ module ActiveFixtures
2
+ module StateDumper
3
+ FIXTURES_PATH = Rails.root.join('spec/fixtures/active').freeze
4
+ DB_NAME = ApplicationRecord.connection_config[:database].freeze
5
+ CLEAN_STATE_NAME = :__clean
6
+
7
+ class << self
8
+ def init!
9
+ FileUtils.mkdir_p(FIXTURES_PATH)
10
+ dump_db(CLEAN_STATE_NAME)
11
+ end
12
+
13
+ def cleanup!
14
+ load_clean
15
+ FileUtils.rm_rf(FIXTURES_PATH) if File.exist?(FIXTURES_PATH)
16
+ end
17
+
18
+ def exists?(state_name)
19
+ File.exists?(dump_db_file(state_name))
20
+ end
21
+
22
+ def load_clean
23
+ load_db(CLEAN_STATE_NAME)
24
+ end
25
+
26
+ def load(state_name)
27
+ load_db(state_name)
28
+
29
+ Hash[*JSON.parse(File.read(dump_entities_file(state_name))).flat_map{ |name, attrs|
30
+ [name, name.gsub(/::[^:]*\z/, '').constantize.new(attrs)]
31
+ }]
32
+ end
33
+
34
+ def dump(state)
35
+ dump_db(state.name)
36
+ File.write(
37
+ dump_entities_file(state.name),
38
+ Hash[*state.entities.flat_map{ |name, entity|
39
+ [name, entity.attributes]
40
+ }].to_json
41
+ )
42
+ end
43
+
44
+ private
45
+
46
+ def dump_db_file(state_name)
47
+ File.join(FIXTURES_PATH, "#{state_name}.db.dump")
48
+ end
49
+
50
+ def dump_entities_file(state_name)
51
+ File.join(FIXTURES_PATH, "#{state_name}.entities.json")
52
+ end
53
+
54
+ def dump_db(state_name)
55
+ args = ['-x', '-O', '-c', '-Fc', '-f', dump_db_file(state_name), DB_NAME]
56
+ Kernel.system('pg_dump', *args)
57
+ end
58
+
59
+ def load_db(state_name)
60
+ args = ['-c', '-d', DB_NAME, dump_db_file(state_name)]
61
+ Kernel.system('pg_restore', *args)
62
+ end
63
+
64
+ end
65
+
66
+ end
67
+ end
@@ -0,0 +1,3 @@
1
+ module ActiveFixtures
2
+ VERSION = '0.0.1'.freeze
3
+ end
metadata ADDED
@@ -0,0 +1,55 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: active-fixtures
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Sergey Tokarenko
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-12-05 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: active-fixtures provides the way how to populate the server state (DB,
14
+ sessions) as an application user but not as programmer.
15
+ email: private.tokarenko.sergey@gmail.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - LICENSE
21
+ - README.md
22
+ - lib/active-fixtures.rb
23
+ - lib/active-fixtures/resource.rb
24
+ - lib/active-fixtures/rspec.rb
25
+ - lib/active-fixtures/session.rb
26
+ - lib/active-fixtures/state.rb
27
+ - lib/active-fixtures/state_builder.rb
28
+ - lib/active-fixtures/state_dumper.rb
29
+ - lib/active-fixtures/version.rb
30
+ homepage: https://github.com/Anadea/active-fixtures
31
+ licenses:
32
+ - MIT
33
+ metadata: {}
34
+ post_install_message:
35
+ rdoc_options: []
36
+ require_paths:
37
+ - lib
38
+ required_ruby_version: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: 2.3.1
43
+ required_rubygems_version: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ requirements: []
49
+ rubyforge_project:
50
+ rubygems_version: 2.5.1
51
+ signing_key:
52
+ specification_version: 4
53
+ summary: active-fixtures provides the way how to populate the server state (DB, sessions)
54
+ as an application user but not as programmer.
55
+ test_files: []