fixturama 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +29 -0
  3. data/README.md +32 -4
  4. data/fixturama.gemspec +1 -1
  5. data/lib/fixturama.rb +48 -20
  6. data/lib/fixturama/changes.rb +71 -0
  7. data/lib/fixturama/changes/base.rb +28 -0
  8. data/lib/fixturama/changes/chain.rb +64 -0
  9. data/lib/fixturama/changes/chain/actions.rb +31 -0
  10. data/lib/fixturama/changes/chain/arguments.rb +59 -0
  11. data/lib/fixturama/changes/chain/raise_action.rb +35 -0
  12. data/lib/fixturama/changes/chain/return_action.rb +33 -0
  13. data/lib/fixturama/changes/const.rb +30 -0
  14. data/lib/fixturama/changes/request.rb +91 -0
  15. data/lib/fixturama/changes/request/response.rb +62 -0
  16. data/lib/fixturama/changes/request/responses.rb +22 -0
  17. data/lib/fixturama/changes/seed.rb +39 -0
  18. data/lib/fixturama/config.rb +5 -0
  19. data/lib/fixturama/fixture_error.rb +31 -0
  20. data/lib/fixturama/loader.rb +1 -0
  21. data/lib/fixturama/loader/context.rb +1 -0
  22. data/lib/fixturama/loader/value.rb +1 -0
  23. data/spec/fixturama/seed_fixture/_spec.rb +1 -0
  24. data/spec/fixturama/seed_fixture/seed.yml +0 -5
  25. metadata +34 -34
  26. data/lib/fixturama/seed.rb +0 -15
  27. data/lib/fixturama/stubs.rb +0 -79
  28. data/lib/fixturama/stubs/chain.rb +0 -90
  29. data/lib/fixturama/stubs/chain/actions.rb +0 -39
  30. data/lib/fixturama/stubs/chain/actions/raise.rb +0 -21
  31. data/lib/fixturama/stubs/chain/actions/return.rb +0 -23
  32. data/lib/fixturama/stubs/chain/arguments.rb +0 -86
  33. data/lib/fixturama/stubs/const.rb +0 -43
  34. data/lib/fixturama/stubs/request.rb +0 -98
  35. data/lib/fixturama/stubs/request/response.rb +0 -43
  36. data/lib/fixturama/stubs/request/responses.rb +0 -20
  37. data/lib/fixturama/utils.rb +0 -39
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5bfb62ba99d427f8a3f03b6d46a0fb7e6712dd31bfff6da7c17fb075cfa930f0
4
- data.tar.gz: fa1b0359c0681a73723a915dd499945602b350b3b802b68e6962a66753a5d208
3
+ metadata.gz: ed87b651886697aaea8b0a79ddbf5b875372ca382926f3dc07d8f3bec2a4d07d
4
+ data.tar.gz: 76248c5bb6b75d335c80387d11b6446cee23829bf1ca8f6fb319c12a1b5f0e52
5
5
  SHA512:
6
- metadata.gz: 987b0cf303e78879b18278d85ace7cf26261e6c76000b0081571cb40a3dcf763435cb5560de122bf3a466b9c781684c8ed8d183697fbd1b84aa619c8409e34c8
7
- data.tar.gz: d85486458d90dbff1e3f2136c5862e081aa1a1861bb5e6320fb68bbc258d11ce1e978de5603c900080ef719809e97a4d0bf793a46c8f90af33e7a6626d258835
6
+ metadata.gz: 20607f5c8bc2ed14ac8bef4bf207495946a02f4a39e2d6d77661b4fa154c01b3f4072e62aaf9083509369eb4b9fe01b2c9dfe1aa4e87b7bf2bb9a3b31b0a1827
7
+ data.tar.gz: baf0b2716a960d8885dda3dde622009ebf8a1335912358898283b42b7beea04137e9d2ee1bcc58d305b8cdf14c21f3bdfa4442674a4dbb780aad61486b0e2682
@@ -5,6 +5,34 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](http://keepachangelog.com/)
6
6
  and this project adheres to [Semantic Versioning](http://semver.org/).
7
7
 
8
+ ## [0.2.0] - [2020-02-17]
9
+
10
+ ### Added
11
+
12
+ - Stubbing and seeding from the same source file via the `call_fixture` method (nepalez)
13
+
14
+ ```yaml
15
+ # ./changes.yml
16
+ ---
17
+ - type: user
18
+ params:
19
+ id: 1
20
+
21
+ - const: DEFAULT_USER_ID
22
+ value: 1
23
+
24
+ - url: https://example.com/users/default
25
+ method: get
26
+ responses:
27
+ - body:
28
+ id: 1
29
+ name: Andrew
30
+ ```
31
+
32
+ ```ruby
33
+ before { call_fixture "#{__dir__}/changes.yml" }
34
+ ```
35
+
8
36
  ## [0.1.0] - [2020-02-09]
9
37
 
10
38
  ### Added
@@ -200,3 +228,4 @@ This is a first public release with features extracted from production app.
200
228
  [0.0.6]: https://github.com/nepalez/fixturama/compare/v0.0.5...v0.0.6
201
229
  [0.0.7]: https://github.com/nepalez/fixturama/compare/v0.0.6...v0.0.7
202
230
  [0.1.0]: https://github.com/nepalez/fixturama/compare/v0.0.7...v0.1.0
231
+ [0.2.0]: https://github.com/nepalez/fixturama/compare/v0.1.0...v0.2.0
data/README.md CHANGED
@@ -124,16 +124,17 @@ Use the `count: 2` key to create more objects at once.
124
124
 
125
125
  ### Stubbing
126
126
 
127
- Another opinionated format we use for stubs (`stub_fixture`). The gem supports stubbing both message chains and constants.
127
+ The gem supports stubbing message chains, constants and http requests with the following keys.
128
128
 
129
129
  For message chains:
130
130
 
131
131
  - `class` for stubbed class
132
132
  - `chain` for messages chain
133
133
  - `arguments` (optional) for specific arguments
134
- - `actions` for an array of actions for consecutive invocations of the chain
135
-
136
- Every action either `return` some value, or `raise` some exception
134
+ - `actions` for an array of actions for consecutive invocations of the chain with keys
135
+ - `return` for a value to be returned
136
+ - `raise` for an exception to be risen
137
+ - `repeate` for a number of invocations with this action
137
138
 
138
139
  For constants:
139
140
 
@@ -152,6 +153,7 @@ For http requests:
152
153
  - `status`
153
154
  - `body`
154
155
  - `headers`
156
+ - `repeate` for the number of times this response should be returned before switching to the next one
155
157
 
156
158
  ```yaml
157
159
  # ./stubs.yml
@@ -179,6 +181,7 @@ For http requests:
179
181
  - <%= profile_id %>
180
182
  actions:
181
183
  - return: true
184
+ repeate: 1 # this is the default value
182
185
  - raise: ActiveRecord::RecordNotFound
183
186
 
184
187
  - const: NOTIFIER_TIMEOUT_SEC
@@ -192,6 +195,7 @@ For http requests:
192
195
  password: bar
193
196
  responses:
194
197
  - status: 200 # for the first call
198
+ repeate: 1 # this is the default value, but you can set another one
195
199
  - status: 404 # for any other call
196
200
 
197
201
  - uri: htpps://example.com/foo # exact string!
@@ -231,6 +235,30 @@ I find it especially helpful when I need to check different edge cases. Instead
231
235
 
232
236
  Looking at the spec I can easily figure out the "structure" of expectation, while looking at fixtures I can check the concrete corner cases.
233
237
 
238
+ ## Single Source of Changes
239
+
240
+ If you will, you can list all stubs and seeds at the one single file like
241
+
242
+ ```yaml
243
+ # ./changes.yml
244
+ ---
245
+ - type: user
246
+ params:
247
+ id: 1
248
+ name: Andrew
249
+
250
+ - const: DEFAULT_USER_ID
251
+ value: 1
252
+ ```
253
+
254
+ This fixture can be applied via `call_fixture` method just like we did above with `seed_fixture` and `stub_fixture`:
255
+
256
+ ```ruby
257
+ before { call_fixture "#{__dir__}/changes.yml" }
258
+ ```
259
+
260
+ In fact, since the `v0.2.0` all those methods are just the aliases of the `call_fixture`.
261
+
234
262
  ## License
235
263
 
236
264
  The gem is available as open source under the terms of the [MIT License][license].
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |gem|
2
2
  gem.name = "fixturama"
3
- gem.version = "0.1.0"
3
+ gem.version = "0.2.0"
4
4
  gem.author = "Andrew Kozin (nepalez)"
5
5
  gem.email = "andrew.kozin@gmail.com"
6
6
  gem.homepage = "https://github.com/nepalez/fixturama"
@@ -6,44 +6,72 @@ require "rspec"
6
6
  require "webmock/rspec"
7
7
  require "yaml"
8
8
 
9
+ #
10
+ # A set of helpers to prettify specs with fixtures
11
+ #
9
12
  module Fixturama
13
+ require_relative "fixturama/fixture_error"
10
14
  require_relative "fixturama/config"
11
- require_relative "fixturama/utils"
12
15
  require_relative "fixturama/loader"
13
- require_relative "fixturama/stubs"
14
- require_relative "fixturama/seed"
16
+ require_relative "fixturama/changes"
15
17
 
18
+ # Set the initial value for database-generated IDs
19
+ # @param [#to_i] value
20
+ # @return [Fixturama]
16
21
  def self.start_ids_from(value)
17
22
  Config.start_ids_from(value)
23
+ self
18
24
  end
19
25
 
20
- def stub_fixture(path, **opts)
21
- items = load_fixture(path, **opts)
22
- raise "The fixture should contain an array" unless items.is_a?(Array)
26
+ # @!method read_fixture(path, options)
27
+ # Read the text content of the fixture
28
+ # @param [#to_s] path The path to the fixture file
29
+ # @param [Hash<Symbol, _>] options
30
+ # The list of options to be accessible in the fixture
31
+ # @return [String]
32
+ def read_fixture(path, **options)
33
+ content = File.read(path)
34
+ hashie = Hashie::Mash.new(options)
35
+ bindings = hashie.instance_eval { binding }
23
36
 
24
- items.each { |item| fixturama_stubs.add(item) }
25
- fixturama_stubs.apply(self)
37
+ ERB.new(content).result(bindings)
26
38
  end
27
39
 
28
- def seed_fixture(path, **opts)
29
- Array(load_fixture(path, **opts)).each { |item| Seed.call(item) }
40
+ # @!method load_fixture(path, options)
41
+ # Load data from a fixture
42
+ # @param (see #read_fixture)
43
+ # @return [Object]
44
+ def load_fixture(path, **options)
45
+ Loader.new(path, options).call
30
46
  end
31
47
 
32
- def load_fixture(path, **opts)
33
- Loader.new(path, opts).call
48
+ # @!method call_fixture(path, options)
49
+ # Stub different objects and seed the database from a fixture
50
+ # @param (see #read_fixture)
51
+ # @return [RSpec::Core::Example] the current example
52
+ def call_fixture(path, **options)
53
+ items = Array load_fixture(path, **options)
54
+ items.each { |item| changes.add(item) }
55
+ tap { changes.call(self) }
56
+ rescue FixtureError => err
57
+ raise err.with_file(path)
34
58
  end
35
59
 
36
- def read_fixture(path, **opts)
37
- content = File.read(path)
38
- hashie = Hashie::Mash.new(opts)
39
- bindings = hashie.instance_eval { binding }
60
+ # @!method seed_fixture(path, options)
61
+ # The alias for the +call_fixture+
62
+ # @param (see #call_fixture)
63
+ # @return (see #call_fixture)
64
+ alias seed_fixture call_fixture
40
65
 
41
- ERB.new(content).result(bindings)
42
- end
66
+ # @!method stub_fixture(path, options)
67
+ # The alias for the +call_fixture+
68
+ # @param (see #call_fixture)
69
+ # @return (see #call_fixture)
70
+ alias stub_fixture call_fixture
43
71
 
44
72
  private
45
73
 
46
- def fixturama_stubs
47
- @fixturama_stubs ||= Stubs.new
74
+ def changes
75
+ @changes ||= Changes.new
48
76
  end
49
77
  end
@@ -0,0 +1,71 @@
1
+ module Fixturama
2
+ #
3
+ # @private
4
+ # Registry of changes (stubs and seeds)
5
+ #
6
+ class Changes
7
+ require_relative "changes/base"
8
+ require_relative "changes/chain"
9
+ require_relative "changes/const"
10
+ require_relative "changes/request"
11
+ require_relative "changes/seed"
12
+
13
+ # Match option keys to the type of an item
14
+ TYPES = {
15
+ actions: Chain,
16
+ arguments: Chain,
17
+ basic_auth: Request,
18
+ body: Request,
19
+ chain: Chain,
20
+ class: Chain,
21
+ const: Const,
22
+ count: Seed,
23
+ headers: Request,
24
+ http_method: Request,
25
+ object: Chain,
26
+ params: Seed,
27
+ query: Request,
28
+ response: Request,
29
+ responses: Request,
30
+ traits: Seed,
31
+ type: Seed,
32
+ uri: Request,
33
+ url: Request,
34
+ value: Const,
35
+ }.freeze
36
+
37
+ # Adds new change to the registry
38
+ # @param [Hash] options
39
+ # @return [Fixturama::Changes]
40
+ # @raise [Fixturama::FixtureError] if the options cannot be processed
41
+ def add(options)
42
+ options = Hash(options).transform_keys(&:to_sym)
43
+ types = options.keys.map { |key| TYPES[key] }.compact.uniq
44
+ raise "Wrong count" unless types.count == 1
45
+
46
+ @changes << types.first.new(options)
47
+ self
48
+ rescue FixtureError => err
49
+ raise err
50
+ rescue StandardError => err
51
+ raise FixtureError.new("an operation", options, err)
52
+ end
53
+
54
+ # Apply all registered changes to the RSpec example
55
+ # @param [RSpec::Core::Example] example
56
+ # @return [self]
57
+ def call(example)
58
+ @changes
59
+ .group_by(&:key)
60
+ .values
61
+ .map { |changes| changes.reduce :merge }
62
+ .each { |change| change.call(example) }
63
+ end
64
+
65
+ private
66
+
67
+ def initialize
68
+ @changes = []
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,28 @@
1
+ class Fixturama::Changes
2
+ #
3
+ # @private
4
+ # @abstract
5
+ # Base class for changes downloaded from a fixture
6
+ #
7
+ class Base
8
+ # @!attribute [r] key The key identifier of the change
9
+ # @return [String]
10
+ alias key hash
11
+
12
+ # Merge the other change into the current one
13
+ # @param [Fixturama::Changes::Base] other
14
+ # @return [Fixturama::Changes::Base]
15
+ def merge(other)
16
+ # By default just take the other change if applicable
17
+ other.class == self.class && other.key == key ? other : self
18
+ end
19
+
20
+ # @abstract
21
+ # Call the corresponding change (either a stub or a seed)
22
+ # @param [RSpec::Core::Example] _example The RSpec example
23
+ # @return [self]
24
+ def call(_example)
25
+ self
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,64 @@
1
+ class Fixturama::Changes
2
+ #
3
+ # @private
4
+ # Stub a chain of messages
5
+ #
6
+ class Chain < Base
7
+ require_relative "chain/raise_action"
8
+ require_relative "chain/return_action"
9
+ require_relative "chain/actions"
10
+ require_relative "chain/arguments"
11
+
12
+ def key
13
+ @key ||= ["chain", @receiver.name, *@messages].join(".")
14
+ end
15
+
16
+ def merge(other)
17
+ return self unless other.class == self.class && other.key == key
18
+
19
+ tap { @arguments = (other.arguments | arguments).sort_by(&:order) }
20
+ end
21
+
22
+ def call(example)
23
+ call_action = example.send(:receive_message_chain, *@messages) do |*real|
24
+ action = arguments.find { |expected| expected.match?(*real) }
25
+ action ? action.call : raise("Unexpected arguments: #{real}")
26
+ end
27
+
28
+ example.send(:allow, @receiver).to call_action
29
+ end
30
+
31
+ protected
32
+
33
+ attr_reader :arguments
34
+
35
+ private
36
+
37
+ def initialize(**options)
38
+ @receiver = receiver_from(options)
39
+ @messages = messages_from(options)
40
+ @arguments = [Arguments.new(options)]
41
+ end
42
+
43
+ def receiver_from(options)
44
+ case options.slice(:class, :object).keys
45
+ when %i[class] then Kernel.const_get(options[:class])
46
+ when %i[object] then Object.send(:eval, options[:object])
47
+ else raise
48
+ end
49
+ rescue StandardError => err
50
+ raise Fixturama::FixtureError.new("a stabbed object", options, err)
51
+ end
52
+
53
+ def messages_from(options)
54
+ case value = options[:chain]
55
+ when Array then value.map(&:to_sym)
56
+ when String then [value.to_sym]
57
+ when Symbol then value
58
+ else raise
59
+ end
60
+ rescue StandardError => err
61
+ raise Fixturama::FixtureError.new("a messages chain", options, err)
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,31 @@
1
+ class Fixturama::Changes::Chain
2
+ #
3
+ # @private
4
+ # Keep arguments of a message chain along with the corresponding actions
5
+ #
6
+ class Actions
7
+ def next
8
+ @list.count > 1 ? @list.pop : @list.first
9
+ end
10
+
11
+ private
12
+
13
+ def initialize(*list)
14
+ list = [{ return: nil }] if list.empty?
15
+
16
+ @list = list.flatten.reverse.flat_map do |item|
17
+ action = build(item)
18
+ [action] * action.repeat
19
+ end
20
+ end
21
+
22
+ def build(item)
23
+ item = Hash(item).transform_keys(&:to_sym)
24
+ case item.slice(:return, :raise).keys
25
+ when %i[return] then ReturnAction.new(item)
26
+ when %i[raise] then RaiseAction.new(item)
27
+ else raise Fixturama::FixtureError.new("an action", item)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,59 @@
1
+ class Fixturama::Changes::Chain
2
+ #
3
+ # @private
4
+ # Keep a set of arguments along with the corresponding actions to be done
5
+ #
6
+ class Arguments
7
+ # @return [Array<Object>] the collection of arguments
8
+ attr_reader :arguments
9
+
10
+ # Order of comparing this set of arguments with the actual ones
11
+ # @return [Integer]
12
+ def order
13
+ -arguments.count
14
+ end
15
+
16
+ # Compare definitions by sets of arguments
17
+ # @param [Fixturama::Changes::Chain::Arguments] other
18
+ # @return [Boolean]
19
+ def ==(other)
20
+ other.arguments == arguments
21
+ end
22
+
23
+ # If actual arguments are covered by the current ones
24
+ def match?(*args, **opts)
25
+ return false if arguments.count > args.count + 1
26
+
27
+ arguments.first(args.count).zip(args).each do |(expected, actual)|
28
+ return false unless actual == expected
29
+ end
30
+
31
+ if arguments.count > args.count
32
+ Hash(arguments.last).transform_keys(&:to_sym).each do |k, v|
33
+ return false unless opts[k] == v
34
+ end
35
+ end
36
+
37
+ true
38
+ end
39
+
40
+ # Call the corresponding action if actual arguments are matched
41
+ def call
42
+ @actions.next.call
43
+ end
44
+
45
+ private
46
+
47
+ def initialize(options)
48
+ @arguments = extract(options, :arguments)
49
+ @actions = Actions.new(*extract(options, :actions))
50
+ end
51
+
52
+ def extract(options, key)
53
+ return [] unless options.key?(key)
54
+
55
+ source = options[key]
56
+ source.is_a?(Array) ? source : [source]
57
+ end
58
+ end
59
+ end