smartest 0.3.3.alpha1 → 0.3.3.alpha2

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
  SHA256:
3
- metadata.gz: 8e7e25cbb3432e62d6264a3308b1b8f01406ed44362f742356b31aa31629926a
4
- data.tar.gz: cbb07c4ae5e9532836d505543f1af85ea222fd013d65b3fa79d0de0228fac1e6
3
+ metadata.gz: 396af151370001b4762c6c1a71426fef7c2e0773626080af62496bc068afdc02
4
+ data.tar.gz: 0b5b6e9e76582dfd91df63fe7e2928fbbd6d0a90101e8f3d9834a91d7b94c109
5
5
  SHA512:
6
- metadata.gz: 5d8cc3fde5695e83649858389212a849ded911ff350689747a62a53ae4c0ce7da5c800e8d6866fb46b04756d0f890efaa3107e9e0bed642092cf2f42aa01bb63
7
- data.tar.gz: dbd5c2b3fab3d0ab182c78d2ee8f60dbb71efe61e0779646bea2194d1e2710bc3baf6c307b046500dcb293446bcd49a1a1c119c09df22703a9805f83992b7780
6
+ metadata.gz: 320397233b8b2e0c46b5d44d6a8bc892d44bfccfc9b63a1db31f868fdc3ac7da7c500902f93b36124f61119dc23b05a3238d3032d7e814defed8c7f5b57f9ef1
7
+ data.tar.gz: 0b507415bf8e31472e4117371474226fbec62802be8a41d002cc38403967d2a877e60d2a7fb0d0695490e416e1fcf208928f3a6882620208c4d694f425138b37
data/DEVELOPMENT.md CHANGED
@@ -45,6 +45,7 @@ smartest/
45
45
  expectations.rb
46
46
  expectation_target.rb
47
47
  matchers.rb
48
+ simple_stub.rb
48
49
 
49
50
  runner.rb
50
51
  test_result.rb
data/README.md CHANGED
@@ -600,6 +600,43 @@ end
600
600
 
601
601
  Register cleanup immediately after acquiring the resource, before later setup steps that may fail.
602
602
 
603
+ ## Stubs
604
+
605
+ Use simple stub helpers when a fixture needs to temporarily replace a Ruby
606
+ method and reset it during cleanup:
607
+
608
+ ```ruby
609
+ class PaymentFixture < Smartest::Fixture
610
+ fixture :payment_gateway_stub do
611
+ simple_stub_any_instance_of(PaymentGateway, :charge) { :approved }
612
+ end
613
+ end
614
+ ```
615
+
616
+ The stub affects existing instances and new instances of the target class in
617
+ the current Fiber until it is reset. Other Fibers and Threads continue to see
618
+ the original method unless they apply their own stub. Tests can request the
619
+ fixture to make the side effect explicit:
620
+
621
+ ```ruby
622
+ test("checkout succeeds") do |payment_gateway_stub:|
623
+ expect(Checkout.call).to eq(:paid)
624
+ end
625
+ ```
626
+
627
+ Use `simple_stub(Time, :now) { fixed_time }` for singleton methods such as class
628
+ methods. Both helpers call `Smartest::SimpleStub` internally, apply the stub,
629
+ register `cleanup { stub.reset }`, and return the stub object. They are
630
+ available inside `Smartest::Fixture` fixture blocks because they need cleanup to
631
+ tie the stub lifetime to the fixture scope.
632
+
633
+ `Smartest::SimpleStub#apply` and `#reset` are idempotent in the current Fiber.
634
+ `apply!` raises
635
+ `Smartest::SimpleStub::AlreadyAppliedError` when the stub is already active in
636
+ the current Fiber, and `reset!` raises
637
+ `Smartest::SimpleStub::NotAppliedError` when it is not active there. See
638
+ [Stubs](documentation/docs/stubs.md).
639
+
603
640
  ## Logged-in client example
604
641
 
605
642
  ```ruby
@@ -825,6 +862,7 @@ Smartest currently focuses on a small runner API:
825
862
  - fixture dependencies through keyword arguments
826
863
  - fixture cleanup
827
864
  - suite-scoped fixtures through `suite_fixture`
865
+ - fixture-scoped method stubs through `simple_stub_any_instance_of` and `simple_stub`
828
866
  - suite hooks with `around_suite`
829
867
  - test hooks with `around_test`
830
868
  - skipped and pending tests through `skip` and `pending`
@@ -65,6 +65,20 @@ module Smartest
65
65
  @fixture_set.add_cleanup(&block)
66
66
  end
67
67
 
68
+ def simple_stub_any_instance_of(klass, method_name, &block)
69
+ apply_simple_stub(SimpleStub.new(klass, method_name, &block))
70
+ end
71
+
72
+ def simple_stub(object, method_name, &block)
73
+ apply_simple_stub(SimpleStub.new(object.singleton_class, method_name, &block))
74
+ end
75
+
76
+ def apply_simple_stub(stub)
77
+ stub.apply!
78
+ cleanup { stub.reset }
79
+ stub
80
+ end
81
+
68
82
  def method_missing(method_name, *args, &block)
69
83
  return super if RESERVED_CONTEXT_METHODS.include?(method_name)
70
84
 
@@ -0,0 +1,178 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+
5
+ module Smartest
6
+ class SimpleStub
7
+ STORAGE_KEY = :__smartest_simple_stub
8
+
9
+ class AlreadyAppliedError < Smartest::Error; end
10
+ class NotAppliedError < Smartest::Error; end
11
+
12
+ class << self
13
+ def implementation_for(klass_key, method_name)
14
+ current_stubs&.fetch(stub_key(klass_key, method_name), nil)
15
+ end
16
+
17
+ def active_stubs
18
+ Thread.current[STORAGE_KEY] ||= {}
19
+ end
20
+
21
+ def clear_active_stubs_if_empty
22
+ Thread.current[STORAGE_KEY] = nil if current_stubs&.empty?
23
+ end
24
+
25
+ def ensure_dispatcher_method(klass, klass_key, method_name)
26
+ dispatcher_mutex.synchronize do
27
+ mod = dispatcher_module_for(klass, klass_key)
28
+ next if mod.method_defined?(method_name) || mod.private_method_defined?(method_name)
29
+
30
+ mod.define_method(method_name) do |*args, **kwargs, &block|
31
+ implementation = SimpleStub.implementation_for(klass_key, method_name)
32
+
33
+ if implementation
34
+ SimpleStub.call_implementation(self, implementation, args, kwargs, block)
35
+ elsif kwargs.empty?
36
+ super(*args, &block)
37
+ else
38
+ super(*args, **kwargs, &block)
39
+ end
40
+ end
41
+ end
42
+ end
43
+
44
+ def stub_key(klass_key, method_name)
45
+ [klass_key, method_name]
46
+ end
47
+
48
+ def current_stubs
49
+ Thread.current[STORAGE_KEY]
50
+ end
51
+
52
+ def call_implementation(receiver, implementation, args, kwargs, block)
53
+ return call_implementation_with_block(receiver, implementation, args, kwargs, block) if block
54
+
55
+ if kwargs.empty?
56
+ receiver.instance_exec(*args, &implementation)
57
+ else
58
+ receiver.instance_exec(*args, **kwargs, &implementation)
59
+ end
60
+ end
61
+
62
+ def call_implementation_with_block(receiver, implementation, args, kwargs, block)
63
+ method_name = next_call_method_name
64
+ singleton_class = receiver.singleton_class
65
+ singleton_class.__send__(:define_method, method_name, &implementation)
66
+
67
+ if kwargs.empty?
68
+ receiver.__send__(method_name, *args, &block)
69
+ else
70
+ receiver.__send__(method_name, *args, **kwargs, &block)
71
+ end
72
+ ensure
73
+ if singleton_class &&
74
+ (singleton_class.method_defined?(method_name) || singleton_class.private_method_defined?(method_name))
75
+ singleton_class.__send__(:remove_method, method_name)
76
+ end
77
+ end
78
+
79
+ private
80
+
81
+ def dispatcher_module_for(klass, klass_key)
82
+ const_name = dispatcher_module_name(klass_key)
83
+
84
+ if const_defined?(const_name, false)
85
+ const_get(const_name, false)
86
+ else
87
+ Module.new.tap do |mod|
88
+ const_set(const_name, mod)
89
+ klass.prepend(mod)
90
+ end
91
+ end
92
+ end
93
+
94
+ def dispatcher_module_name(klass_key)
95
+ "StubDispatcher#{klass_key}"
96
+ end
97
+
98
+ def dispatcher_mutex
99
+ @dispatcher_mutex ||= Mutex.new
100
+ end
101
+
102
+ def next_call_method_name
103
+ call_mutex.synchronize do
104
+ @call_sequence ||= 0
105
+ @call_sequence += 1
106
+ :"__smartest_simple_stub_call_#{@call_sequence}"
107
+ end
108
+ end
109
+
110
+ def call_mutex
111
+ @call_mutex ||= Mutex.new
112
+ end
113
+ end
114
+
115
+ def initialize(klass, method_name, &implementation)
116
+ raise ArgumentError, "klass must be a Class. #{klass.class} specified." unless klass.is_a?(Class)
117
+ raise ArgumentError, "method name must be a Symbol." unless method_name.is_a?(Symbol)
118
+
119
+ @klass = klass
120
+ @method_name = method_name
121
+ @implementation = implementation
122
+ end
123
+
124
+ def apply
125
+ return if stub_defined?
126
+
127
+ apply_stub
128
+ end
129
+
130
+ def apply!
131
+ raise AlreadyAppliedError, "stub for #{@klass}##{@method_name} is already applied" if stub_defined?
132
+
133
+ apply_stub
134
+ end
135
+
136
+ def reset
137
+ return unless stub_defined?
138
+
139
+ reset_stub
140
+ end
141
+
142
+ def reset!
143
+ raise NotAppliedError, "stub for #{@klass}##{@method_name} is not applied" unless stub_defined?
144
+
145
+ reset_stub
146
+ end
147
+
148
+ private
149
+
150
+ def apply_stub
151
+ raise ArgumentError, "block must be given for applying stub" unless @implementation
152
+
153
+ self.class.ensure_dispatcher_method(@klass, klass_key, @method_name)
154
+ active_stubs[stub_key] = @implementation
155
+ end
156
+
157
+ def reset_stub
158
+ active_stubs.delete(stub_key)
159
+ self.class.clear_active_stubs_if_empty
160
+ end
161
+
162
+ def active_stubs
163
+ self.class.active_stubs
164
+ end
165
+
166
+ def stub_key
167
+ self.class.stub_key(klass_key, @method_name)
168
+ end
169
+
170
+ def klass_key
171
+ @klass_key ||= Digest::SHA256.hexdigest(@klass.object_id.to_s)
172
+ end
173
+
174
+ def stub_defined?
175
+ self.class.current_stubs&.key?(stub_key)
176
+ end
177
+ end
178
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Smartest
4
- VERSION = "0.3.3.alpha1"
4
+ VERSION = "0.3.3.alpha2"
5
5
  end
data/lib/smartest.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "smartest/version"
4
4
  require_relative "smartest/errors"
5
+ require_relative "smartest/simple_stub"
5
6
  require_relative "smartest/parameter_extractor"
6
7
  require_relative "smartest/test_case"
7
8
  require_relative "smartest/test_registry"
@@ -0,0 +1,273 @@
1
+ # frozen_string_literal: true
2
+
3
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
4
+
5
+ require "smartest/autorun"
6
+ require "stringio"
7
+
8
+ module SimpleStubSelfTest
9
+ module_function
10
+
11
+ def test_case(name, block)
12
+ Smartest::TestCase.new(
13
+ name: name,
14
+ metadata: {},
15
+ block: block,
16
+ location: caller_locations(1, 1).first
17
+ )
18
+ end
19
+
20
+ def run_suite(suite)
21
+ output = StringIO.new
22
+ status = Smartest::Runner.new(suite: suite, reporter: Smartest::Reporter.new(output)).run
23
+
24
+ [status, output.string]
25
+ end
26
+
27
+ def capture_error(expected_error)
28
+ yield
29
+ rescue Exception => error
30
+ raise if Smartest.fatal_exception?(error)
31
+
32
+ unless error.is_a?(expected_error)
33
+ raise Smartest::AssertionFailed, "expected #{expected_error}, but raised #{error.class}: #{error.message}"
34
+ end
35
+
36
+ error
37
+ else
38
+ raise Smartest::AssertionFailed, "expected #{expected_error}, but nothing was raised"
39
+ end
40
+ end
41
+
42
+ class SimpleStubSelfTestSubject
43
+ def initialize(name)
44
+ @name = name
45
+ end
46
+
47
+ def name
48
+ "original #{@name}"
49
+ end
50
+
51
+ def greeting(prefix)
52
+ "#{prefix}, #{@name}"
53
+ end
54
+
55
+ def yielding_greeting(prefix)
56
+ yield "#{prefix}, #{@name}"
57
+ end
58
+ end
59
+
60
+ class SimpleStubSelfTestClock
61
+ def self.now
62
+ :original_now
63
+ end
64
+ end
65
+
66
+ test("simple stub stubs instance methods until reset") do
67
+ existing = SimpleStubSelfTestSubject.new("Alice")
68
+ stub = Smartest::SimpleStub.new(SimpleStubSelfTestSubject, :name) { "stubbed" }
69
+
70
+ stub.apply!
71
+
72
+ begin
73
+ expect(existing.name).to eq("stubbed")
74
+ expect(SimpleStubSelfTestSubject.new("Bob").name).to eq("stubbed")
75
+ ensure
76
+ stub.reset
77
+ end
78
+
79
+ expect(existing.name).to eq("original Alice")
80
+ expect(SimpleStubSelfTestSubject.new("Bob").name).to eq("original Bob")
81
+ end
82
+
83
+ test("simple stub can be reset from a fresh stub object") do
84
+ Smartest::SimpleStub.new(SimpleStubSelfTestSubject, :greeting) { |prefix| "#{prefix}, stubbed" }.apply!
85
+
86
+ begin
87
+ expect(SimpleStubSelfTestSubject.new("Alice").greeting("Hi")).to eq("Hi, stubbed")
88
+ ensure
89
+ Smartest::SimpleStub.new(SimpleStubSelfTestSubject, :greeting).reset!
90
+ end
91
+
92
+ expect(SimpleStubSelfTestSubject.new("Alice").greeting("Hi")).to eq("Hi, Alice")
93
+ end
94
+
95
+ test("simple_stub_any_instance_of applies and resets from fixture cleanup") do
96
+ fixture_class = Class.new(Smartest::Fixture) do
97
+ fixture :stubbed_name do
98
+ simple_stub_any_instance_of(SimpleStubSelfTestSubject, :name) { "fixture #{@name}" }
99
+ :stubbed_name
100
+ end
101
+ end
102
+
103
+ suite = Smartest::Suite.new
104
+ suite.fixture_classes.add(fixture_class)
105
+ suite.tests.add(
106
+ SimpleStubSelfTest.test_case(
107
+ "uses instance stub fixture",
108
+ proc do |stubbed_name:|
109
+ expect(stubbed_name).to eq(:stubbed_name)
110
+ expect(SimpleStubSelfTestSubject.new("Alice").name).to eq("fixture Alice")
111
+ end
112
+ )
113
+ )
114
+ suite.tests.add(
115
+ SimpleStubSelfTest.test_case(
116
+ "sees reset instance method",
117
+ proc { expect(SimpleStubSelfTestSubject.new("Alice").name).to eq("original Alice") }
118
+ )
119
+ )
120
+
121
+ status, = SimpleStubSelfTest.run_suite(suite)
122
+
123
+ expect(status).to eq(0)
124
+ end
125
+
126
+ test("simple_stub applies and resets singleton methods from fixture cleanup") do
127
+ fixture_class = Class.new(Smartest::Fixture) do
128
+ fixture :stubbed_time do
129
+ simple_stub(SimpleStubSelfTestClock, :now) { :stubbed_now }
130
+ :stubbed_time
131
+ end
132
+ end
133
+
134
+ suite = Smartest::Suite.new
135
+ suite.fixture_classes.add(fixture_class)
136
+ suite.tests.add(
137
+ SimpleStubSelfTest.test_case(
138
+ "uses singleton stub fixture",
139
+ proc do |stubbed_time:|
140
+ expect(stubbed_time).to eq(:stubbed_time)
141
+ expect(SimpleStubSelfTestClock.now).to eq(:stubbed_now)
142
+ end
143
+ )
144
+ )
145
+ suite.tests.add(
146
+ SimpleStubSelfTest.test_case(
147
+ "sees reset singleton method",
148
+ proc { expect(SimpleStubSelfTestClock.now).to eq(:original_now) }
149
+ )
150
+ )
151
+
152
+ status, = SimpleStubSelfTest.run_suite(suite)
153
+
154
+ expect(status).to eq(0)
155
+ end
156
+
157
+ test("simple stub preserves receiver self and method blocks") do
158
+ subject = SimpleStubSelfTestSubject.new("Alice")
159
+ stub = Smartest::SimpleStub.new(SimpleStubSelfTestSubject, :yielding_greeting) do |prefix, &block|
160
+ block.call("#{prefix}, stubbed #{@name}")
161
+ end
162
+
163
+ stub.apply!
164
+
165
+ begin
166
+ result = subject.yielding_greeting("Hi") { |message| message.upcase }
167
+ expect(result).to eq("HI, STUBBED ALICE")
168
+ ensure
169
+ stub.reset
170
+ end
171
+
172
+ expect(subject.yielding_greeting("Hi") { |message| message }).to eq("Hi, Alice")
173
+ end
174
+
175
+ test("simple stub is scoped to the current fiber") do
176
+ subject = SimpleStubSelfTestSubject.new("Alice")
177
+ stub = Smartest::SimpleStub.new(SimpleStubSelfTestSubject, :name) { "stubbed #{@name}" }
178
+
179
+ stub.apply!
180
+
181
+ begin
182
+ expect(subject.name).to eq("stubbed Alice")
183
+ Fiber.new do
184
+ expect(subject.name).to eq("original Alice")
185
+ end.resume
186
+ ensure
187
+ stub.reset
188
+ end
189
+
190
+ expect(subject.name).to eq("original Alice")
191
+ end
192
+
193
+ test("simple stub can differ per thread") do
194
+ subject = SimpleStubSelfTestSubject.new("Alice")
195
+ queue = Queue.new
196
+ main_stub = Smartest::SimpleStub.new(SimpleStubSelfTestSubject, :name) { "main #{@name}" }
197
+
198
+ main_stub.apply!
199
+
200
+ thread = Thread.new do
201
+ thread_stub = Smartest::SimpleStub.new(SimpleStubSelfTestSubject, :name) { "thread #{@name}" }
202
+ thread_stub.apply!
203
+
204
+ begin
205
+ queue << subject.name
206
+ rescue Exception => error
207
+ queue << error
208
+ ensure
209
+ thread_stub.reset
210
+ end
211
+ end
212
+
213
+ begin
214
+ expect(subject.name).to eq("main Alice")
215
+
216
+ thread_result = queue.pop
217
+ raise thread_result if thread_result.is_a?(Exception)
218
+
219
+ expect(thread_result).to eq("thread Alice")
220
+ thread.join
221
+ expect(subject.name).to eq("main Alice")
222
+ ensure
223
+ main_stub.reset
224
+ thread.kill if thread.alive?
225
+ end
226
+
227
+ expect(subject.name).to eq("original Alice")
228
+ end
229
+
230
+ test("simple stub supports safe and strict apply reset APIs") do
231
+ stub = Smartest::SimpleStub.new(SimpleStubSelfTestSubject, :name) { "stubbed" }
232
+
233
+ stub.apply
234
+ stub.apply
235
+
236
+ begin
237
+ error = SimpleStubSelfTest.capture_error(Smartest::SimpleStub::AlreadyAppliedError) do
238
+ Smartest::SimpleStub.new(SimpleStubSelfTestSubject, :name) { "other" }.apply!
239
+ end
240
+
241
+ expect(error.message).to eq("stub for SimpleStubSelfTestSubject#name is already applied")
242
+ ensure
243
+ stub.reset
244
+ end
245
+
246
+ stub.reset
247
+
248
+ error = SimpleStubSelfTest.capture_error(Smartest::SimpleStub::NotAppliedError) do
249
+ stub.reset!
250
+ end
251
+
252
+ expect(error.message).to eq("stub for SimpleStubSelfTestSubject#name is not applied")
253
+ end
254
+
255
+ test("simple stub validates constructor arguments and apply block") do
256
+ error = SimpleStubSelfTest.capture_error(ArgumentError) do
257
+ Smartest::SimpleStub.new(Object.new, :name)
258
+ end
259
+
260
+ expect(error.message).to eq("klass must be a Class. Object specified.")
261
+
262
+ error = SimpleStubSelfTest.capture_error(ArgumentError) do
263
+ Smartest::SimpleStub.new(SimpleStubSelfTestSubject, "name")
264
+ end
265
+
266
+ expect(error.message).to eq("method name must be a Symbol.")
267
+
268
+ error = SimpleStubSelfTest.capture_error(ArgumentError) do
269
+ Smartest::SimpleStub.new(SimpleStubSelfTestSubject, :name).apply!
270
+ end
271
+
272
+ expect(error.message).to eq("block must be given for applying stub")
273
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: smartest
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.3.alpha1
4
+ version: 0.3.3.alpha2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yusuke Iwaki
@@ -62,6 +62,7 @@ files:
62
62
  - lib/smartest/parameter_extractor.rb
63
63
  - lib/smartest/reporter.rb
64
64
  - lib/smartest/runner.rb
65
+ - lib/smartest/simple_stub.rb
65
66
  - lib/smartest/suite.rb
66
67
  - lib/smartest/suite_run.rb
67
68
  - lib/smartest/test_case.rb
@@ -71,6 +72,7 @@ files:
71
72
  - lib/smartest/test_run_state.rb
72
73
  - lib/smartest/version.rb
73
74
  - smartest.gemspec
75
+ - smartest/simple_stub_test.rb
74
76
  - smartest/smartest_test.rb
75
77
  homepage: https://smartest-rb.vercel.app
76
78
  licenses: