saharspec 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 69a3d4d52b75ffe4622413b927337a741b810c57
4
- data.tar.gz: 7da973ac28887c0fbd3c65a7aaad6bb6a5140e63
3
+ metadata.gz: a363bdb21050423225b98a8dae3d2f8861947426
4
+ data.tar.gz: 3ace51cf8d457d79caa8fe2c3b12e144c1a5a50a
5
5
  SHA512:
6
- metadata.gz: 02fb0e99a0b46ace2311deed7d21132582c11f8d5c04727d211f28d5725615eed36ce9e942ec7a31a9767b23b137918eac650598869ed6e01fce79bf503ab43a
7
- data.tar.gz: b112f9ea6892c954de423ce07076c530f46c66549d2201d06fe4233b18f933a6505110f148ca4b14ad139e357c0ca3ef26fb106437749d9f23ca584f7be6c987
6
+ metadata.gz: a6b1f390d030eeebbaa9ece4efbc64176e24cfd32d748e044a1d47fd8f7862b5a966a83fdcb64474b28496f0d9ea97150e70d725c0b99d9ed47d74d26d4f60b2
7
+ data.tar.gz: fbf40cbc81e264aeb3f78d14c5c934a3c501e3586e851c32718e4cd2be9eaedd58f5f3e2fc5c4664159bb1abec90b065fb907589bde3395a4b62ae89083d8180
data/CHANGELOG.md ADDED
@@ -0,0 +1,30 @@
1
+ # Saharspec history
2
+
3
+ ## 0.0.3 -- 2017-11-06
4
+
5
+ * Introduce new `its`-family addition: `its_call(*args)`
6
+ ```ruby
7
+ let(:array) { %i[a b c] }
8
+ subject { array.method(:delete_at) }
9
+ its_call(1) { is_expected.to change(array, :length).by(-1) }
10
+ ```
11
+ * Introduce new `ret` matcher ("block is expected to return"), useful in context when block is your
12
+ primary subject:
13
+
14
+ ```ruby
15
+ let(:array) { %i[a b c] }
16
+ subject { array.method(:delete_at) }
17
+ its_call(1) { is_expected.to ret(:b).and change(array, :length).by(-1) }
18
+ ```
19
+ * Introduce experimental `dont` matcher combiner (instead of `RSpec.define_negated_matcher`), used
20
+ as `expect { something }.to dont.change { anything }`;
21
+ * **Breaking**: rename `its_call`→`its_block` (because the name was needed for other means).
22
+
23
+ ## 0.0.2 -- 2017-09-01
24
+
25
+ * Make `send_message` composable;
26
+ * Add `Util#multiline`.
27
+
28
+ ## 0.0.1 -- 2017-08-14
29
+
30
+ Initial!
data/README.md CHANGED
@@ -50,6 +50,58 @@ its_call { is_expected.to send_message(Net::HTTP, :get).with('http://google.com'
50
50
  Note: there is [reasons](https://github.com/rspec/rspec-expectations/issues/934) why it is not in rspec-mocks, though, not very persuative for
51
51
  me.
52
52
 
53
+ #### `expect { block }.to ret(value)` matcher
54
+
55
+ Checks whether `#call`-able subject (block, method, command object), when called, return value matching
56
+ to expected.
57
+
58
+ Useful when this callable subject is your primary one:
59
+
60
+ ```ruby
61
+ # before: option 1. subject is value
62
+ subject { 2 + x }
63
+
64
+ context 'when numeric' do
65
+ let(:x) { 3 }
66
+ it { is_expected.to eq 5 } # DRY
67
+ end
68
+
69
+ context 'when incompatible' do
70
+ let(:x) { '3' }
71
+ it { expect { subject }.to raise_error } # not DRY
72
+ end
73
+
74
+ # option 2. subject is block
75
+ subject { -> {2 + x } }
76
+
77
+ context 'when incompatible' do
78
+ let(:x) { '3' }
79
+ it { is_expected.to raise_error } # DRY
80
+ end
81
+
82
+ context 'when numeric' do
83
+ let(:x) { 3 }
84
+ it { expect(subject.call).to eq 5 } # not DRY
85
+ end
86
+
87
+ # after
88
+ require 'saharspec/matchers/ret'
89
+
90
+ subject { -> { 2 + x } }
91
+
92
+ context 'when numeric' do
93
+ let(:x) { 3 }
94
+ it { is_expected.to ret 5 } # DRY: notice `ret`
95
+ end
96
+
97
+ context 'when incompatible' do
98
+ let(:x) { '3' }
99
+ it { is_expected.to raise_error } # DRY
100
+ end
101
+ ```
102
+
103
+ Plays really well with `its_call` shown below.
104
+
53
105
  #### `eq_multiline(text)` matcher
54
106
 
55
107
  Dedicated to checking some multiline text generators.
@@ -81,6 +133,23 @@ require 'saharspec/matchers/eq_multiline'
81
133
  ```
82
134
  (empty lines before/after are removed, text deindented up to `|` sign)
83
135
 
136
+ ### `dont`: matcher negation
137
+
138
+ Another (exprimental) attempt to get rid of `define_negated_matcher`. `dont` is not 100% grammatically
139
+ correct, yet short and readable enought. It just negates attached matcher.
140
+
141
+ ```ruby
142
+ # before
143
+ RSpec.define_negated_matcher :not_change, :change
144
+
145
+ it { expect { code }.to do_stuff.and not_change(obj, :attr) }
146
+
147
+ # after: no `define_negated_matcher` needed
148
+ require 'saharspec/matchers/dont'
149
+
150
+ it { expect { code }.to do_stuff.and dont.change(obj, :attr) }
151
+ ```
152
+
84
153
  ### `its`-addons
85
154
 
86
155
  **Notice**: There are different opinions on usability/reasonability of `its(:attribute)` syntax,
@@ -104,18 +173,59 @@ require 'saharspec/its/map'
104
173
  its_map(:text) { are_expected.to all not_be_empty }
105
174
  ```
106
175
 
107
- #### `its_call`
176
+ #### `its_block`
177
+
178
+ Allows to DRY-ly refer to "block that calculates subject".
108
179
 
109
180
  ```ruby
110
181
  subject { some_operation_that_may_fail }
111
182
 
112
183
  # before
113
- it { expect { subject }.to raise_error(...) }
184
+ context 'success' do
185
+ it { is_expected.to eq 123 }
186
+ end
187
+
188
+ context 'fail' do
189
+ it { expect { subject }.to raise_error(...) }
190
+ end
191
+
192
+ # after
193
+ require 'saharspec/its/block'
194
+
195
+ its_block { is_expected.to raise_error(...) }
196
+ ```
197
+
198
+ #### `its_call`
199
+
200
+ Allows to DRY-ly test callable object with different arguments. Plays well with forementioned `ret`
201
+ matcher.
202
+
203
+ Before:
204
+
205
+ ```ruby
206
+ # before
207
+ describe '#delete_at' do
208
+ let(:array) { %i[a b c] }
209
+
210
+ it { expect(array.delete_at(1) }.to eq :b }
211
+ it { expect(array.delete_at(8) }.to eq nil }
212
+ it { expect { array.delete_at(1) }.to change(array, :length).by(-1) }
213
+ it { expect { array.delete_at(:b) }.to raise_error TypeError }
214
+ end
114
215
 
115
216
  # after
116
217
  require 'saharspec/its/call'
117
218
 
118
- its_call { is_expected.to raise_error(...) }
219
+ describe '#delete_at' do
220
+ let(:array) { %i[a b c] }
221
+
222
+ subject { array.method(:delete_at) }
223
+
224
+ its_call(1) { is_expected.to ret :b }
225
+ its_call(1) { is_expected.to change(array, :length).by(-1) }
226
+ its_call(8) { is_expected.to ret nil }
227
+ its_call(:b) { is_expected.to raise_error TypeError }
228
+ end
119
229
  ```
120
230
 
121
231
  ## State & future
data/lib/saharspec/its.rb CHANGED
@@ -1,10 +1,15 @@
1
1
  module Saharspec
2
2
  # Wrapper module for all `its_*` RSpec additions.
3
3
  #
4
- # See {Map#its_map #its_map} and {Call#its_call #its_call}
4
+ # See:
5
+ #
6
+ # * {Map#its_map #its_map}
7
+ # * {Block#its_block #its_block}
8
+ # * {Call#its_call #its_call}
5
9
  module Its
6
10
  end
7
11
  end
8
12
 
9
13
  require_relative 'its/map'
14
+ require_relative 'its/block'
10
15
  require_relative 'its/call'
@@ -0,0 +1,56 @@
1
+ module Saharspec
2
+ module Its
3
+ module Block
4
+ # Creates nested example which converts current subject to a block-subject.
5
+ #
6
+ # @example
7
+ #
8
+ # subject { calc_something(params) }
9
+ #
10
+ # # without its_block
11
+ # context 'with this params' do
12
+ # it { expect { subject }.to change(some, :value).by(1) }
13
+ # end
14
+ #
15
+ # context 'with that params' do
16
+ # it { expect { subject }.to raise_error(SomeError) }
17
+ # end
18
+ #
19
+ # # with its_block
20
+ # context 'with this params' do
21
+ # its_block { is_expected.to change(some, :value).by(1) }
22
+ # end
23
+ #
24
+ # context 'with that params' do
25
+ # its_block { is_expected.to raise_error(SomeError) }
26
+ # end
27
+ #
28
+ # @param options Options (metadata) that can be passed to usual RSpec example.
29
+ # @param block [Proc] The test itself. Inside it, `is_expected` is a synonom
30
+ # for `expect { subject }`.
31
+ #
32
+ def its_block(*options, &block)
33
+ # rubocop:disable Lint/NestedMethodDefinition
34
+ describe('as block') do
35
+ let(:__call_subject) do
36
+ -> { subject }
37
+ end
38
+
39
+ def is_expected
40
+ expect(__call_subject)
41
+ end
42
+
43
+ example(nil, *options, &block)
44
+ end
45
+ # rubocop:enable Lint/NestedMethodDefinition
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ RSpec.configure do |rspec|
52
+ rspec.extend Saharspec::Its::Block
53
+ rspec.backtrace_exclusion_patterns << %r{/lib/saharspec/its/block}
54
+ end
55
+
56
+ RSpec::SharedContext.send(:include, Saharspec::Its::Block)
@@ -1,46 +1,41 @@
1
1
  module Saharspec
2
2
  module Its
3
3
  module Call
4
- # Creates nested example which converts current subject to a block-subject.
4
+ # For `#call`-able subject, creates nested example where subject is called with arguments
5
+ # provided, allowing to apply block matchers like `.to change(something)` or `.to raise_error`
6
+ # to different calls in a DRY way.
5
7
  #
6
- # @example
7
- #
8
- # subject { calc_something(params) }
8
+ # Also, plays really well with {RSpec::Matchers#ret #ret} block matcher.
9
9
  #
10
- # # without its_call
11
- # context 'with this params'
12
- # it { expect { subject }.to change(some, :value).by(1) }
13
- # end
10
+ # @example
11
+ # let(:array) { %i[a b c] }
14
12
  #
15
- # context 'with that params'
16
- # it { expect { subject }.to raise_error(SomeError) }
17
- # end
13
+ # describe '#[]' do
14
+ # subject { array.method(:[]) }
18
15
  #
19
- # # with its_call
20
- # context 'with this params'
21
- # its_call { is_expected.to change(some, :value).by(1) }
16
+ # its_call(1) { is_expected.to ret :b }
17
+ # its_call(1..-1) { is_expected.to ret %i[b c] }
18
+ # its_call('foo') { is_expected.to raise_error TypeError }
22
19
  # end
23
20
  #
24
- # context 'with that params'
25
- # its_call { is_expected.to raise_error(SomeError) }
21
+ # describe '#push' do
22
+ # subject { array.method(:push) }
23
+ # its_call(5) { is_expected.to change(array, :length).by(1) }
26
24
  # end
27
25
  #
28
- # @param options Other options that can be passed to usual RSpec example.
29
- # @param block [Proc] The test itself. Inside it, `is_expected` (or `are_expected`) is analog of
30
- # `expect { subject }`.
31
- #
32
- def its_call(*options, &block)
26
+ def its_call(*args, &block)
33
27
  # rubocop:disable Lint/NestedMethodDefinition
34
28
  describe('call') do
35
29
  let(:__call_subject) do
36
- -> { subject }
30
+ warn 'No need to use its_call without arguments, just it {} will work' if args.empty?
31
+ -> { subject.call(*args) }
37
32
  end
38
33
 
39
34
  def is_expected
40
35
  expect(__call_subject)
41
36
  end
42
37
 
43
- example(nil, *options, &block)
38
+ example(nil, &block)
44
39
  end
45
40
  # rubocop:enable Lint/NestedMethodDefinition
46
41
  end
@@ -13,7 +13,7 @@ module Saharspec
13
13
  # # with attribute chain
14
14
  # its_map(:'reverse.upcase') { is_expected.to eq %w[TSET EM ESAELP] }
15
15
  #
16
- # # with Hash (or any other object re sponding to `#[]`)
16
+ # # with Hash (or any other object responding to `#[]`)
17
17
  # subject {
18
18
  # [
19
19
  # {title: 'Slaughterhouse Five', author: {first: 'Kurt', last: 'Vonnegut'}},
@@ -1,7 +1,12 @@
1
1
  module Saharspec
2
2
  # All Saharspec matchers, when required, included into `RSpec::Matchers` namespace.
3
3
  #
4
- # See {RSpec::Matchers#send_message #send_message} and {RSpec::Matchers#eq_multiline #eq_multiline}.
4
+ # See:
5
+ #
6
+ # * {RSpec::Matchers#dont #dont}
7
+ # * {RSpec::Matchers#send_message #send_message}
8
+ # * {RSpec::Matchers#eq_multiline #eq_multiline}
9
+ # * {RSpec::Matchers#ret #ret}
5
10
  #
6
11
  module Matchers
7
12
  end
@@ -9,3 +14,4 @@ end
9
14
 
10
15
  require_relative 'matchers/eq_multiline'
11
16
  require_relative 'matchers/send_message'
17
+ require_relative 'matchers/ret'
@@ -0,0 +1,66 @@
1
+ module Saharspec
2
+ module Matchers
3
+ # @private
4
+ class Not < RSpec::Matchers::BuiltIn::BaseMatcher
5
+ def initialize
6
+ @delegator = Delegator.new
7
+ end
8
+
9
+ def description
10
+ "not #{@matcher.description}"
11
+ end
12
+
13
+ def match(_expected, actual)
14
+ @matcher or
15
+ fail(ArgumentError, '`dont` matcher used without any matcher to negate. Usage: dont.other_matcher(args)')
16
+ !@matcher.matches?(actual)
17
+ end
18
+
19
+ def supports_block_expectations?
20
+ @matcher.supports_block_expectations?
21
+ end
22
+
23
+ def method_missing(m, *a, &b) # rubocop:disable Style/MethodMissing
24
+ if @matcher
25
+ @matcher.send(m, *a, &b)
26
+ else
27
+ @matcher = @delegator.send(m, *a, &b)
28
+ end
29
+
30
+ self
31
+ end
32
+
33
+ def respond_to_missing?(method, include_private = false)
34
+ if @matcher
35
+ @matcher.respond_to?(method, include_private)
36
+ else
37
+ @delegator.respond_to_missing?(method, include_private)
38
+ end
39
+ end
40
+
41
+ class Delegator
42
+ include RSpec::Matchers
43
+ end
44
+ end
45
+ end
46
+ end
47
+
48
+ module RSpec
49
+ module Matchers
50
+ # Negates attached matcher, allowing creating negated matchers on the fly.
51
+ #
52
+ # While not being 100% grammatically correct, seems to be readable enough.
53
+ #
54
+ # @example
55
+ # # before
56
+ # RSpec.define_negated_matcher :not_change, :change
57
+ # it { expect { code }.to do_stuff.and not_change(obj, :attr) }
58
+ #
59
+ # # after: no `define_negated_matcher` needed
60
+ # it { expect { code }.to do_stuff.and dont.change(obj, :attr) }
61
+ #
62
+ def dont
63
+ Saharspec::Matchers::Not.new
64
+ end
65
+ end
66
+ end
@@ -32,7 +32,7 @@ module RSpec
32
32
  # |end
33
33
  # })
34
34
  #
35
- # @param [String]
35
+ # @param expected [String]
36
36
  def eq_multiline(expected)
37
37
  Saharspec::Matchers::EqMultiline.new(expected)
38
38
  end
@@ -0,0 +1,82 @@
1
+ module Saharspec
2
+ module Matchers
3
+ # @private
4
+ class Ret
5
+ include RSpec::Matchers::Composable
6
+
7
+ def initialize(expected)
8
+ @expected = expected
9
+ end
10
+
11
+ def matches?(subject)
12
+ @subject = subject
13
+ return false unless subject.respond_to?(:call)
14
+ @actual = subject.call
15
+ @expected === @actual # rubocop:disable Style/CaseEquality
16
+ end
17
+
18
+ def supports_block_expectations?
19
+ true
20
+ end
21
+
22
+ def description
23
+ "return #{@expected.respond_to?(:description) ? @expected.description : @expected.inspect}"
24
+ end
25
+
26
+ def failure_message
27
+ "expected to #{description}, " +
28
+ (@subject.respond_to?(:call) ? "but returned #{@actual.inspect}" : 'but was not callable')
29
+ end
30
+
31
+ def failure_message_when_negated
32
+ "expected not to #{description}, but returned it"
33
+ end
34
+ end
35
+ end
36
+ end
37
+
38
+ module RSpec
39
+ module Matchers
40
+ # `ret` (short for `return`) checks if provided block **returns** value specified.
41
+ #
42
+ # It should be considered instead of simple value matchers (like `eq`) in the situations:
43
+ #
44
+ # 1. Several block behaviors tested in the same test, joined with `.and`, or in separate tests
45
+ # 2. You test what some block or method returns with arguments, using {Call#its_call #its_call}
46
+ #
47
+ # Values are tested with `===`, which allows chaining other matchers and patterns to the check.
48
+ #
49
+ # @note
50
+ # There is a case when `ret` fails: when it is _not the first_ in a chain of matchers joined
51
+ # by `.and`. That's not exactly the matchers bug, that's how RSpec works (loses block's return
52
+ # value passing the block between matchers)
53
+ #
54
+ # @example
55
+ # # case 1: block is a subject
56
+ # subject { -> { do_something } }
57
+ #
58
+ # it { is_expected.not_to raise_error }
59
+ # it { is_expected.to change(some, :value).by(1) }
60
+ # it { is_expected.to ret 8 }
61
+ #
62
+ # # or, joined:
63
+ # specify {
64
+ # expect { do_something }.to ret(8).and change(some, :value).by(1)
65
+ # }
66
+ #
67
+ # # case 2: with arguments
68
+ # subject { %i[a b c].method(:[]) }
69
+ #
70
+ # its_call(1) { is_expected.to ret :b }
71
+ # its_call(1..-1) { is_expected.to ret %i[b c] }
72
+ # its_call('foo') { is_expected.to raise_error TypeError }
73
+ #
74
+ # # Note, that values are tested with ===, which means all other matchers could be chained:
75
+ # its_call(1) { is_expected.to ret instance_of(Symbol) }
76
+ # its_call(1..-1) { is_expected.to ret instance_of(Array).and have_attributes(length: 2) }
77
+ #
78
+ def ret(expected)
79
+ Saharspec::Matchers::Ret.new(expected)
80
+ end
81
+ end
82
+ end
@@ -32,7 +32,7 @@ module Saharspec
32
32
  end
33
33
 
34
34
  def times
35
- raise NoMethodError unless @times
35
+ fail NoMethodError unless @times
36
36
  self
37
37
  end
38
38
 
@@ -44,6 +44,11 @@ module Saharspec
44
44
  exactly(2)
45
45
  end
46
46
 
47
+ def ordered
48
+ @ordered = true
49
+ self
50
+ end
51
+
47
52
  # Matching
48
53
  def matches?(subject)
49
54
  run(subject)
@@ -78,8 +83,8 @@ module Saharspec
78
83
 
79
84
  def run(subject)
80
85
  @target.respond_to?(@method, true) or
81
- raise NoMethodError,
82
- "undefined method `#{@method}' for#{@target.inspect}:#{@target.class}"
86
+ fail NoMethodError,
87
+ "undefined method `#{@method}' for#{@target.inspect}:#{@target.class}"
83
88
  allow(@target).to allower
84
89
  subject.call
85
90
  end
@@ -95,6 +100,7 @@ module Saharspec
95
100
  have_received(@method).tap do |e|
96
101
  e.with(*@arguments) if @arguments
97
102
  e.exactly(@times).times if @times
103
+ e.ordered if @ordered
98
104
  end
99
105
  end
100
106
  end
@@ -115,12 +121,14 @@ module RSpec
115
121
  #
116
122
  # # after:
117
123
  # require 'saharspec/matchers/send_message'
124
+ #
118
125
  # it { expect { code_being_tested }.to send_message(double, :fetch).with(something) }
119
- # # after + its_call
120
- # require 'saharspec/its/call'
121
- # subject { code_being_tested }
122
- # its_call { is_expected.to send_message(double, :fetch).with(something) }
123
126
  #
127
+ # # after + its_block
128
+ # require 'saharspec/its/block'
129
+ #
130
+ # subject { code_being_tested }
131
+ # its_block { is_expected.to send_message(double, :fetch).with(something) }
124
132
  #
125
133
  # @param target Object which expects message, double or real object
126
134
  # @param method [Symbol] Message being expected
@@ -0,0 +1,9 @@
1
+ module Saharspec
2
+ # @private
3
+ MAJOR = 0
4
+ # @private
5
+ MINOR = 0
6
+ # @private
7
+ PATCH = 3
8
+ VERSION = [MAJOR, MINOR, PATCH].join('.')
9
+ end
data/saharspec.gemspec CHANGED
@@ -1,6 +1,8 @@
1
+ require_relative 'lib/saharspec/version'
2
+
1
3
  Gem::Specification.new do |s|
2
4
  s.name = 'saharspec'
3
- s.version = '0.0.2'
5
+ s.version = Saharspec::VERSION
4
6
  s.authors = ['Victor Shepelev']
5
7
  s.email = 'zverok.offline@gmail.com'
6
8
  s.homepage = 'https://github.com/zverok/saharspec'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: saharspec
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Victor Shepelev
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-09-01 00:00:00.000000000 Z
11
+ date: 2017-11-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rubocop
@@ -116,17 +116,22 @@ extra_rdoc_files: []
116
116
  files:
117
117
  - ".rubocop_todo.yml"
118
118
  - ".yardopts"
119
+ - CHANGELOG.md
119
120
  - LICENSE.txt
120
121
  - README.md
121
122
  - lib/saharspec.rb
122
123
  - lib/saharspec/its.rb
124
+ - lib/saharspec/its/block.rb
123
125
  - lib/saharspec/its/call.rb
124
126
  - lib/saharspec/its/map.rb
125
127
  - lib/saharspec/matchers.rb
128
+ - lib/saharspec/matchers/dont.rb
126
129
  - lib/saharspec/matchers/eq_multiline.rb
127
130
  - lib/saharspec/matchers/request_webmock.rb
131
+ - lib/saharspec/matchers/ret.rb
128
132
  - lib/saharspec/matchers/send_message.rb
129
133
  - lib/saharspec/util.rb
134
+ - lib/saharspec/version.rb
130
135
  - saharspec.gemspec
131
136
  homepage: https://github.com/zverok/saharspec
132
137
  licenses: