minispec 0.0.1

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.
Files changed (83) hide show
  1. checksums.yaml +7 -0
  2. data/.pryrc +2 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE +22 -0
  5. data/README.md +2140 -0
  6. data/Rakefile +11 -0
  7. data/bin/minispec +4 -0
  8. data/lib/minispec.rb +175 -0
  9. data/lib/minispec/api.rb +2 -0
  10. data/lib/minispec/api/class.rb +195 -0
  11. data/lib/minispec/api/class/after.rb +49 -0
  12. data/lib/minispec/api/class/around.rb +54 -0
  13. data/lib/minispec/api/class/before.rb +101 -0
  14. data/lib/minispec/api/class/helpers.rb +116 -0
  15. data/lib/minispec/api/class/let.rb +44 -0
  16. data/lib/minispec/api/class/tests.rb +33 -0
  17. data/lib/minispec/api/instance.rb +158 -0
  18. data/lib/minispec/api/instance/mocks/doubles.rb +36 -0
  19. data/lib/minispec/api/instance/mocks/mocks.rb +319 -0
  20. data/lib/minispec/api/instance/mocks/spies.rb +17 -0
  21. data/lib/minispec/api/instance/mocks/stubs.rb +105 -0
  22. data/lib/minispec/helpers.rb +1 -0
  23. data/lib/minispec/helpers/array.rb +56 -0
  24. data/lib/minispec/helpers/booleans.rb +108 -0
  25. data/lib/minispec/helpers/generic.rb +24 -0
  26. data/lib/minispec/helpers/mocks/expectations.rb +29 -0
  27. data/lib/minispec/helpers/mocks/spies.rb +36 -0
  28. data/lib/minispec/helpers/raise.rb +44 -0
  29. data/lib/minispec/helpers/throw.rb +29 -0
  30. data/lib/minispec/mocks.rb +11 -0
  31. data/lib/minispec/mocks/expectations.rb +77 -0
  32. data/lib/minispec/mocks/stubs.rb +178 -0
  33. data/lib/minispec/mocks/validations.rb +80 -0
  34. data/lib/minispec/mocks/validations/amount.rb +63 -0
  35. data/lib/minispec/mocks/validations/arguments.rb +161 -0
  36. data/lib/minispec/mocks/validations/caller.rb +43 -0
  37. data/lib/minispec/mocks/validations/order.rb +47 -0
  38. data/lib/minispec/mocks/validations/raise.rb +111 -0
  39. data/lib/minispec/mocks/validations/return.rb +74 -0
  40. data/lib/minispec/mocks/validations/throw.rb +91 -0
  41. data/lib/minispec/mocks/validations/yield.rb +141 -0
  42. data/lib/minispec/proxy.rb +201 -0
  43. data/lib/minispec/reporter.rb +185 -0
  44. data/lib/minispec/utils.rb +139 -0
  45. data/lib/minispec/utils/differ.rb +325 -0
  46. data/lib/minispec/utils/pretty_print.rb +51 -0
  47. data/lib/minispec/utils/raise.rb +123 -0
  48. data/lib/minispec/utils/throw.rb +140 -0
  49. data/minispec.gemspec +27 -0
  50. data/test/mocks/expectations/amount.rb +67 -0
  51. data/test/mocks/expectations/arguments.rb +126 -0
  52. data/test/mocks/expectations/caller.rb +55 -0
  53. data/test/mocks/expectations/generic.rb +35 -0
  54. data/test/mocks/expectations/order.rb +46 -0
  55. data/test/mocks/expectations/raise.rb +166 -0
  56. data/test/mocks/expectations/return.rb +71 -0
  57. data/test/mocks/expectations/throw.rb +113 -0
  58. data/test/mocks/expectations/yield.rb +109 -0
  59. data/test/mocks/spies/amount.rb +68 -0
  60. data/test/mocks/spies/arguments.rb +57 -0
  61. data/test/mocks/spies/generic.rb +61 -0
  62. data/test/mocks/spies/order.rb +38 -0
  63. data/test/mocks/spies/raise.rb +158 -0
  64. data/test/mocks/spies/return.rb +71 -0
  65. data/test/mocks/spies/throw.rb +113 -0
  66. data/test/mocks/spies/yield.rb +101 -0
  67. data/test/mocks/test__doubles.rb +98 -0
  68. data/test/mocks/test__expectations.rb +27 -0
  69. data/test/mocks/test__mocks.rb +197 -0
  70. data/test/mocks/test__proxies.rb +61 -0
  71. data/test/mocks/test__spies.rb +43 -0
  72. data/test/mocks/test__stubs.rb +427 -0
  73. data/test/proxified_asserts.rb +34 -0
  74. data/test/setup.rb +53 -0
  75. data/test/test__around.rb +58 -0
  76. data/test/test__assert.rb +510 -0
  77. data/test/test__before_and_after.rb +117 -0
  78. data/test/test__before_and_after_all.rb +71 -0
  79. data/test/test__helpers.rb +197 -0
  80. data/test/test__raise.rb +104 -0
  81. data/test/test__skip.rb +41 -0
  82. data/test/test__throw.rb +103 -0
  83. metadata +196 -0
@@ -0,0 +1,80 @@
1
+ module MiniSpec
2
+ module Mocks
3
+ class Validations
4
+ include MiniSpec::Utils
5
+
6
+ def initialize base, object, context, *expected_messages
7
+ expected_messages.empty? && raise(ArgumentError, 'Wrong number of arguments (3 for 4+)')
8
+ expected_messages.all? {|m| m.is_a?(Symbol)} || raise(ArgumentError, 'Only symbols accepted')
9
+ @base, @object, @context, @failed = base, object, context, false
10
+ @expected_messages = expected_messages.freeze
11
+ @messages = expected_and_received.freeze
12
+ validate_received_messages!
13
+ end
14
+
15
+ private
16
+ # selecting only expected messages in the order they was received.
17
+ # @param expected_messages [Array]
18
+ # @return [Hash]
19
+ def expected_and_received
20
+ @base.__ms__mocks__instance_messages(@context[:left_object]).inject({}) do |map,msg|
21
+ @expected_messages.include?(msg[:method]) && (map[msg[:method]] ||= []).push(msg)
22
+ map
23
+ end
24
+ end
25
+
26
+ def validate_received_messages!
27
+ @expected_messages.each do |m|
28
+ @context[:negation] ?
29
+ @messages.keys.include?(m) && message_validation_error!(m, true) :
30
+ @messages.keys.include?(m) || message_validation_error!(m)
31
+ end
32
+ end
33
+
34
+ def message_validation_error! message, negation = false
35
+ fail_with('%sExpected %s to receive %s message' % [
36
+ negation ? 'NOT ' : '',
37
+ pp(@object),
38
+ pp(message),
39
+ ])
40
+ end
41
+
42
+ def single_message_expected?
43
+ @expected_messages.size == 1
44
+ end
45
+
46
+ # checks whether correct number of arguments given.
47
+ # in any situation, at least one argument required.
48
+ # if multiple messages expected, number of arguments should be equal to one
49
+ # or to the number of expected messages.
50
+ def assert_given_arguments_match_received_messages *args, &block
51
+ if block
52
+ args.empty? || raise(ArgumentError, 'Both arguments and block given. Please use either one.')
53
+ return true # if block given, no arguments accepted, so nothing to validate
54
+ end
55
+
56
+ # single argument acceptable for any number of expected messages
57
+ return if args.size == 1
58
+
59
+ # when a single message expected, any number of arguments accepted
60
+ return if @expected_messages.size == 1
61
+
62
+ # on multiple messages, number of arguments should match number of expected messages
63
+ return if args.size == @expected_messages.size
64
+
65
+ raise(ArgumentError, 'wrong number of arguments (%i for 1..%i)' % [
66
+ args.size,
67
+ @expected_messages.size
68
+ ], caller[1..-1])
69
+ end
70
+
71
+ def fail_with message
72
+ return unless @failed = message
73
+ @base.fail(message)
74
+ self
75
+ end
76
+ end
77
+ end
78
+ end
79
+
80
+ Dir[File.expand_path('../validations/**/*.rb', __FILE__)].each {|f| require(f)}
@@ -0,0 +1,63 @@
1
+ class MiniSpec::Mocks::Validations
2
+
3
+ # assure expected message(s) was received a specific amount of times
4
+ #
5
+ # @example expect `a` to be received exactly 2 times
6
+ # expect(obj).to_receive(:a).count(2)
7
+ #
8
+ # @example expect `a` to be received 2 or more times
9
+ # expect(obj).to_receive(:a).count {|a| a >= 2}
10
+ #
11
+ # @example expect `a` and `b` to be received 2 times each
12
+ # expect(obj).to_receive(:a, :b).count(2)
13
+ #
14
+ # @example expect `a` to be received 2 times and `b` 3 times
15
+ # expect(obj).to_receive(:a, :b).count(2, 3)
16
+ #
17
+ # @example expect both `a` and `b` to be received more than 2 times
18
+ # expect(obj).to_receive(:a, :b).count {|a,b| a > 2 && b > 2}
19
+ #
20
+ def count *expected, &block
21
+ return self if @failed
22
+ assert_given_arguments_match_received_messages(*expected, &block)
23
+ received = received_amounts
24
+
25
+ if block
26
+ return @base.instance_exec(*received.values, &block) ||
27
+ amount_error!(@expected_messages, block, received)
28
+ end
29
+
30
+ expected = zipper(@expected_messages, expected)
31
+ received.each_pair do |message,amount|
32
+ # each message should be received expected amount of times
33
+ amount == expected[message] ||
34
+ amount_error!(message, expected[message], amount)
35
+ end
36
+ self
37
+ end
38
+ alias times count
39
+
40
+ def once; count(1); end
41
+ def twice; count(2); end
42
+
43
+ private
44
+ # returns a Hash of messages each with amount of times it was called.
45
+ # basically it does the same as `@messages.values.map(&:size)`
46
+ # except it returns the messages in the order they are expected.
47
+ def received_amounts
48
+ @expected_messages.inject({}) do |map,msg|
49
+ map.merge(msg => (@messages[msg] || []).size)
50
+ end
51
+ end
52
+
53
+ def amount_error! messages, expected, received
54
+ fail_with("%s received %s message(s) wrong amount of times.\nExpected: %s\nActual: %s" % [
55
+ pp(@object),
56
+ pp(messages),
57
+ expected.is_a?(Proc) ?
58
+ ('to be validated at %s' % pp(source(expected))) :
59
+ Array(expected).map {|x| pp(x)}*', ',
60
+ pp(received)
61
+ ])
62
+ end
63
+ end
@@ -0,0 +1,161 @@
1
+ class MiniSpec::Mocks::Validations
2
+
3
+ # validates received arguments against expected ones
4
+ #
5
+ # @example
6
+ # expect(obj).to_receive(:a).with(1)
7
+ # obj.a(1)
8
+ #
9
+ # @example
10
+ # expect(obj).to_receive(:a).with(1, 2)
11
+ # obj.a(1, 2)
12
+ #
13
+ # @example
14
+ # expect(obj).to_receive(:a).with(1, [:a, :b, :c])
15
+ # obj.a(1, [:a, :b, :c])
16
+ #
17
+ # @example
18
+ # expect(obj).to_receive(:a).with {|x| x[0] == [1, 2, 3] && x[1] == [:x, [:y], 'z']}
19
+ # obj.a(1, 2, 3)
20
+ # obj.a(:x, [:y], 'z')
21
+ #
22
+ # @example
23
+ # expect(obj).to_receive(:a, :b, :c).with(1)
24
+ # obj.a(1)
25
+ # obj.b(1)
26
+ # obj.c(1)
27
+ #
28
+ # @example
29
+ # expect(obj).to_receive(:a, :b, :c).with(1, 2, 3)
30
+ # obj.a(1)
31
+ # obj.b(2)
32
+ # obj.c(3)
33
+ #
34
+ # @example
35
+ # expect(obj).to_receive(:a, :b, :c).with([1, 2, 3])
36
+ # obj.a(1, 2, 3)
37
+ # obj.b(1, 2, 3)
38
+ # obj.c(1, 2, 3)
39
+ #
40
+ # @example
41
+ # expect(obj).to_receive(:a, :b, :c).with([1, 2], [:x, :y], :z)
42
+ # obj.a(1, 2)
43
+ # obj.b(:x, :y)
44
+ # obj.c(:z)
45
+ #
46
+ # @example
47
+ # expect(obj).to_receive(:a, :b, :c).with([[1, 2]], [[:x, :y]], [:z])
48
+ # obj.a([1, 2])
49
+ # obj.b([:x, :y])
50
+ # obj.c([:z])
51
+ #
52
+ # @example
53
+ # expect(obj).to_receive(:a, :b, :c).with do |a,b,c|
54
+ # a == [[1, 2]] &&
55
+ # b == [[:x, :y]] &&
56
+ # c == [:z]
57
+ # end
58
+ # obj.a(1, 2)
59
+ # obj.b(:x, :y)
60
+ # obj.c(:z)
61
+ #
62
+ # @example
63
+ # expect(obj).to_receive(:a, :b, :c).with do |a,b,c|
64
+ # a == [[1, 2], [3, 4]] &&
65
+ # b == [[:x, :y], [2]] &&
66
+ # c == [[:z], [[:a, :b], :c]]
67
+ # end
68
+ # obj.a(1, 2)
69
+ # obj.a(3, 4)
70
+ # obj.b(:x, :y)
71
+ # obj.b(2)
72
+ # obj.c(:z)
73
+ # obj.c([:a, :b], :c)
74
+ #
75
+ def with *expected, &block
76
+ return self if @failed
77
+ assert_given_arguments_match_received_messages(*expected, &block)
78
+ received = received_arguments
79
+
80
+ if block
81
+ return @base.instance_exec(*received.values, &block) ||
82
+ arguments_error!(@expected_messages, block, received)
83
+ end
84
+
85
+ single_message_expected? ?
86
+ validate_arguments(expected, received) :
87
+ validate_arguments_list(expected, received)
88
+ self
89
+ end
90
+
91
+ def without_arguments
92
+ return self if @failed
93
+ received_arguments.each_pair do |msg,args|
94
+ # each message should be called without arguments at least once
95
+ args.any?(&:empty?) || arguments_error!(msg, [], msg => args)
96
+ end
97
+ self
98
+ end
99
+ alias without_any_arguments without_arguments
100
+
101
+ private
102
+ # returns a Hash of received messages,
103
+ # each with a list of arguments it was called with.
104
+ #
105
+ # @example
106
+ # obj.a(:x)
107
+ # obj.a([:x])
108
+ # obj.a(:y, [:z])
109
+ # => { a: [ [:x], [[:x]], [:y, [:z]] ] }
110
+ #
111
+ def received_arguments
112
+ @expected_messages.inject({}) do |map,msg|
113
+ map.merge(msg => @messages[msg] ? @messages[msg].map {|m| m[:arguments]} : [])
114
+ end
115
+ end
116
+
117
+ def validate_arguments expected, received
118
+ message = @expected_messages[0]
119
+ arguments = received[message]
120
+ return if arguments.any? {|x| x == expected}
121
+ arguments_error!(message, expected, message => arguments)
122
+ end
123
+
124
+ def validate_arguments_list expected, received
125
+ expected = zipper(@expected_messages, expected)
126
+ received.each_pair do |msg,args|
127
+ next if args.any? {|x| x == [expected[msg]]}
128
+ arguments_error!(msg, expected[msg], msg => args)
129
+ end
130
+ end
131
+
132
+ def stringify_expected_arguments expected
133
+ return 'to be validated at %s' % pp(source(expected)) if expected.is_a?(Proc)
134
+ expected = Array(expected)
135
+ return 'to be called without arguments' if expected.empty?
136
+ expected.map {|a| pp(a)}*', '
137
+ end
138
+
139
+ def stringify_received_arguments received
140
+ received.is_a?(Hash) || raise(ArgumentError, 'expected a Hash')
141
+ received.map do |msg,args|
142
+ '%s called %s' % [
143
+ pp(msg),
144
+ args.map do |arr|
145
+ arr.empty? ?
146
+ 'without arguments' :
147
+ 'with %s' % arr.map {|a| pp(a)}.join(', ')
148
+ end*' then '
149
+ ]
150
+ end*"\n "
151
+ end
152
+
153
+ def arguments_error! message, expected, received
154
+ fail_with("%s received %s message(s) with unexpected arguments.\nExpected: %s\nActual: %s" % [
155
+ pp(@object),
156
+ pp(message),
157
+ stringify_expected_arguments(expected),
158
+ stringify_received_arguments(received)
159
+ ])
160
+ end
161
+ end
@@ -0,0 +1,43 @@
1
+ class MiniSpec::Mocks::Validations
2
+
3
+ def with_caller *expected, &block
4
+ return self if @failed
5
+ assert_given_arguments_match_received_messages(*expected, &block)
6
+ received = received_callers
7
+
8
+ if block
9
+ return @base.instance_exec(*received.values, &block) ||
10
+ caller_error!(@expected_messages, block)
11
+ end
12
+
13
+ expected = zipper(@expected_messages, expected)
14
+ received.each_pair do |msg,callers|
15
+ # each message should be called from expected caller at least once
16
+ callers.any? {|line| caller_match?(line, expected[msg])} ||
17
+ caller_error!(msg, expected[msg])
18
+ end
19
+ self
20
+ end
21
+
22
+ private
23
+ def received_callers
24
+ @expected_messages.inject({}) do |map,msg|
25
+ map.merge(msg => @messages[msg] ? @messages[msg].map {|m| m[:caller]} : [])
26
+ end
27
+ end
28
+
29
+ def caller_match? line, pattern
30
+ regexp = pattern.is_a?(Regexp) ? pattern : Regexp.new(Regexp.escape(pattern))
31
+ line.any? {|l| l =~ regexp}
32
+ end
33
+
34
+ def caller_error! message, expected
35
+ fail_with("%s received %s message(s) from wrong location.\nCaller does not %s" % [
36
+ pp(@object),
37
+ pp(message),
38
+ expected.is_a?(Proc) ?
39
+ ('pass validation at %s' % pp(source(expected))) :
40
+ ('match %s' % pp(expected))
41
+ ])
42
+ end
43
+ end
@@ -0,0 +1,47 @@
1
+ class MiniSpec::Mocks::Validations
2
+
3
+ # checks whether expected messages was received in a specific order
4
+ #
5
+ # @note this method will work only when multiple messages expected.
6
+ # that's it, unlike RSpec, it wont work like this:
7
+ # `expect(obj).to_receive(:a).ordered`
8
+ # `expect(obj).to_receive(:b).ordered`
9
+ #
10
+ # instead it will work like this:
11
+ # `expect(obj).to_receive(:a, :b).ordered`
12
+ #
13
+ # @example
14
+ # expect(obj).to_receive(:a, :b, :c).ordered
15
+ #
16
+ # @example expect for same sequence N times
17
+ # expect(obj).to_receive(:a, :b).ordered(2)
18
+ # # for this to pass `obj.a` and `obj.b` should be both called twice in same order
19
+ #
20
+ def ordered n = 1, &block
21
+ block && raise(ArgumentError, '#ordered does not accept a block')
22
+ n.is_a?(Integer) || raise(ArgumentError, '#ordered expects a single Integer argument')
23
+ single_message_expected? && raise(ArgumentError, '#ordered works only with multiple messages')
24
+ received_in_expected_order?(n)
25
+ end
26
+
27
+ private
28
+ def received_in_expected_order? n
29
+ x = 0
30
+ messages_in_received_order.each_cons(@expected_messages.size) {|c| x += 1 if c == @expected_messages}
31
+ x == n || ordered_error!(n, x)
32
+ end
33
+
34
+ # returns an Array of all messages in the order they was received
35
+ def messages_in_received_order
36
+ @base.__ms__mocks__instance_messages(@context[:left_object]).map {|m| m[:method]}
37
+ end
38
+
39
+ def ordered_error! expected, received
40
+ fail_with("Expected %s to receive %s sequence %s times.\nInstead it was received %s times." % [
41
+ pp(@object),
42
+ pp(@expected_messages),
43
+ expected,
44
+ received
45
+ ])
46
+ end
47
+ end
@@ -0,0 +1,111 @@
1
+ class MiniSpec::Mocks::Validations
2
+
3
+ # expect received message(s) to raise a exception.
4
+ #
5
+ # if no args given any raised exception accepted.
6
+ # if a class given it checks whether raised exception is of given type.
7
+ # if a string or regexp given it checks whether raised message matches it.
8
+ #
9
+ # @example expect `a` to raise something
10
+ # expect(obj).to_receive(:a).and_raise
11
+ #
12
+ # @example expect `a` to raise ArgumentError
13
+ # expect(obj).to_receive(:a).and_raise(ArgumentError)
14
+ #
15
+ # @example raised exception should be of ArgumentError type and match /something/
16
+ # expect(obj).to_receive(:a).and_raise([ArgumentError, /something/])
17
+ #
18
+ # @example expect `a` to raise ArgumentError and `b` to raise RuntimeError
19
+ # expect(obj).to_receive(:a, :b).and_raise(ArgumentError, RuntimeError)
20
+ #
21
+ # @example expect `a` to raise ArgumentError matching /something/ and `b` to raise RuntimeError
22
+ # expect(obj).to_receive(:a, :b).and_raise([ArgumentError, /something/], RuntimeError)
23
+ #
24
+ def and_raise *expected, &block
25
+ return self if @failed
26
+ # `and_raise` can be called without arguments
27
+ expected.empty? || assert_given_arguments_match_received_messages(*expected, &block)
28
+ received = raised_exceptions
29
+
30
+ if block
31
+ return @base.instance_exec(*received.values, &block) ||
32
+ exception_error!(@expected_messages, block, received)
33
+ end
34
+
35
+ expected = single_message_expected? ?
36
+ {@expected_messages[0] => expected} :
37
+ zipper(@expected_messages, expected)
38
+ context = @context.merge(negation: nil, right_proc: nil) # do NOT alter @context
39
+ received.each_pair do |msg,calls|
40
+ # each message should raise as expected at least once
41
+ calls.any? {|c| exception_raised?(c, context, *expected[msg]) == true} ||
42
+ exception_error!(msg, expected[msg], msg => calls)
43
+ end
44
+ self
45
+ end
46
+ alias and_raised and_raise
47
+ alias and_raised? and_raise
48
+
49
+ # make sure received message(s) does not raise any exception
50
+ #
51
+ # @example
52
+ # expect(obj).to_receive(:a).without_raise
53
+ #
54
+ def without_raise
55
+ return self if @failed
56
+ raised_exceptions.each_pair do |msg,calls|
57
+ calls.any? {|r| r.is_a?(Exception)} && unexpected_exception_error!(msg, calls)
58
+ end
59
+ self
60
+ end
61
+
62
+ private
63
+ def raised_exceptions
64
+ @expected_messages.inject({}) do |map,msg|
65
+ map.merge(msg => @messages[msg] ? @messages[msg].map {|m| m[:raised]} : [])
66
+ end
67
+ end
68
+
69
+ def unexpected_exception_error! message, received
70
+ fail_with("%s received %s message and raised an unexpected error.\nExpected: %s\nActual: %s" % [
71
+ pp(@object),
72
+ pp(message),
73
+ 'nothing to be raised',
74
+ stringify_received_exception(message => received)
75
+ ])
76
+ end
77
+
78
+ def exception_error! message, expected, received
79
+ fail_with("%s received %s message(s) but did not raise accordingly.\nExpected: %s\nActual: %s" % [
80
+ pp(@object),
81
+ pp(message),
82
+ stringify_expected_exception(expected),
83
+ stringify_received_exception(received)
84
+ ])
85
+ end
86
+
87
+ def stringify_expected_exception expected
88
+ return 'any exception to be raised' unless expected
89
+ return 'raised exception to be validated at %s' % pp(source(expected)) if expected.is_a?(Proc)
90
+ Array(expected).map(&method(:pp))*':'
91
+ end
92
+
93
+ def stringify_received_exception received
94
+ received.is_a?(Hash) || raise(ArgumentError, 'a Hash expected')
95
+ received.map do |msg,calls|
96
+ calls.each_with_index.map do |call,i|
97
+ '%s call #%s raised %s' % [
98
+ pp(msg),
99
+ i + 1,
100
+ call.is_a?(Exception) ?
101
+ stringify_exception(call) :
102
+ 'nothing'
103
+ ]
104
+ end*"\n "
105
+ end*"\n "
106
+ end
107
+
108
+ def stringify_exception exception
109
+ [exception.class, exception.message].map(&method(:pp))*':'
110
+ end
111
+ end