mocktail 2.0.0 → 3.0.0
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 +4 -4
- data/.github/workflows/main.yml +2 -2
- data/CHANGELOG.md +5 -0
- data/Gemfile +3 -2
- data/Gemfile.lock +76 -82
- data/README.md +2 -0
- data/docs/code_of_conduct.md +8 -0
- data/docs/configuring_minitest.md +20 -0
- data/docs/configuring_rspec.md +19 -0
- data/docs/example_test.md +594 -0
- data/docs/faq/existing_tests.md +47 -0
- data/docs/faq/mocking_http.md +151 -0
- data/docs/faq/mocking_the_subject.md +161 -0
- data/docs/faq/mocking_time.md +34 -0
- data/docs/faq/partial_mocks.md +34 -0
- data/docs/faq/verifying_real_interactions.md +52 -0
- data/docs/img/delegator_tree.png +0 -0
- data/docs/img/example_test.png +0 -0
- data/docs/img/extract_transform_load.png +0 -0
- data/docs/img/mocktail_sorbet.jpg +0 -0
- data/docs/img/mocktail_untyped.jpg +0 -0
- data/docs/img/spacer.png +0 -0
- data/docs/installation_sorbet.md +99 -0
- data/docs/installation_untyped.md +29 -0
- data/docs/other_uses.md +32 -0
- data/docs/stubbing_and_verifying.md +515 -0
- data/docs/support/api.md +819 -0
- data/docs/support/example_test.rb +94 -0
- data/docs/support/glossary.md +426 -0
- data/docs/tdd/class_methods.md +44 -0
- data/docs/tdd/poro/dependency_inception.md +136 -0
- data/docs/tdd/poro/dependency_injection.md +82 -0
- data/docs/tdd/poro.md +15 -0
- data/docs/tdd/third_party.md +220 -0
- data/docs/tdd.md +15 -0
- data/lib/mocktail/debug.rb +4 -3
- data/lib/mocktail/share/stringifies_call.rb +0 -1
- data/lib/mocktail/simulates_argument_error/transforms_params.rb +2 -2
- data/lib/mocktail/sorbet/mocktail/debug.rb +4 -3
- data/lib/mocktail/sorbet/mocktail/share/stringifies_call.rb +0 -1
- data/lib/mocktail/sorbet/mocktail/simulates_argument_error/transforms_params.rb +2 -2
- data/lib/mocktail/sorbet/mocktail/value/call.rb +1 -1
- data/lib/mocktail/sorbet/mocktail/version.rb +1 -1
- data/lib/mocktail/value/call.rb +1 -1
- data/lib/mocktail/version.rb +1 -1
- data/mocktail.gemspec +2 -2
- data/rbi/mocktail-pregenerated.rbi +58 -0
- data/script/build +47 -0
- data/script/setup +17 -0
- data/script/spoom_me +7 -0
- data/script/strip_sigils +17 -0
- data/script/test +33 -0
- data/script/test_double_require_warnings +46 -0
- data/script/update +21 -0
- data/sorbet/config +12 -0
- data/sorbet/rbi/annotations/rainbow.rbi +269 -0
- data/sorbet/rbi/gems/ast@2.4.2.rbi +584 -0
- data/sorbet/rbi/gems/diff-lcs@1.5.0.rbi +1083 -0
- data/sorbet/rbi/gems/docile@1.4.0.rbi +376 -0
- data/sorbet/rbi/gems/json@2.6.3.rbi +1533 -0
- data/sorbet/rbi/gems/language_server-protocol@3.17.0.3.rbi +14237 -0
- data/sorbet/rbi/gems/lint_roller@1.0.0.rbi +230 -0
- data/sorbet/rbi/gems/m@1.6.1.rbi +257 -0
- data/sorbet/rbi/gems/method_source@1.0.0.rbi +272 -0
- data/sorbet/rbi/gems/minitest@5.18.0.rbi +1491 -0
- data/sorbet/rbi/gems/netrc@0.11.0.rbi +158 -0
- data/sorbet/rbi/gems/parallel@1.23.0.rbi +273 -0
- data/sorbet/rbi/gems/parser@3.2.2.1.rbi +7253 -0
- data/sorbet/rbi/gems/rainbow@3.1.1.rbi +402 -0
- data/sorbet/rbi/gems/rake@13.0.6.rbi +3024 -0
- data/sorbet/rbi/gems/rbi@0.0.16.rbi +3049 -0
- data/sorbet/rbi/gems/regexp_parser@2.8.0.rbi +3748 -0
- data/sorbet/rbi/gems/rexml@3.2.5.rbi +4717 -0
- data/sorbet/rbi/gems/rubocop-ast@1.29.0.rbi +7003 -0
- data/sorbet/rbi/gems/rubocop-performance@1.18.0.rbi +8 -0
- data/sorbet/rbi/gems/rubocop-sorbet@0.7.0.rbi +1043 -0
- data/sorbet/rbi/gems/rubocop@1.52.0.rbi +55500 -0
- data/sorbet/rbi/gems/ruby-progressbar@1.13.0.rbi +1317 -0
- data/sorbet/rbi/gems/simplecov-html@0.12.3.rbi +216 -0
- data/sorbet/rbi/gems/simplecov@0.22.0.rbi +2148 -0
- data/sorbet/rbi/gems/simplecov_json_formatter@0.1.4.rbi +8 -0
- data/sorbet/rbi/gems/spoom@1.2.1.rbi +2536 -0
- data/sorbet/rbi/gems/standard-custom@1.0.1.rbi +63 -0
- data/sorbet/rbi/gems/standard-performance@1.1.0.rbi +8 -0
- data/sorbet/rbi/gems/standard@1.29.0.rbi +850 -0
- data/sorbet/rbi/gems/tapioca@0.11.6.rbi +3301 -0
- data/sorbet/rbi/gems/thor@1.2.2.rbi +3965 -0
- data/sorbet/rbi/gems/unicode-display_width@2.4.2.rbi +65 -0
- data/sorbet/rbi/gems/unparser@0.6.7.rbi +4524 -0
- data/sorbet/rbi/gems/yard-sorbet@0.8.1.rbi +428 -0
- data/sorbet/rbi/gems/yard@0.9.34.rbi +18219 -0
- data/sorbet/tapioca/config.yml +13 -0
- data/sorbet/tapioca/require.rb +4 -0
- data/spoom_data/05f0c49.json +1 -0
- data/spoom_data/1657012.json +1 -0
- data/spoom_data/167434b.json +1 -0
- data/spoom_data/1d0ba5b.json +1 -0
- data/spoom_data/1ffa724.json +1 -0
- data/spoom_data/2129b3d.json +1 -0
- data/spoom_data/24b1c92.json +1 -0
- data/spoom_data/2c46aee.json +1 -0
- data/spoom_data/305ec0b.json +1 -0
- data/spoom_data/30e9528.json +1 -0
- data/spoom_data/4638cd5.json +1 -0
- data/spoom_data/47c7dad.json +1 -0
- data/spoom_data/4b1edef.json +1 -0
- data/spoom_data/4de157f.json +1 -0
- data/spoom_data/526e7db.json +1 -0
- data/spoom_data/5d093b9.json +1 -0
- data/spoom_data/5db3b43.json +1 -0
- data/spoom_data/5fe2a65.json +1 -0
- data/spoom_data/6891312.json +1 -0
- data/spoom_data/6b0fef4.json +1 -0
- data/spoom_data/6b83d12.json +1 -0
- data/spoom_data/74c83c2.json +1 -0
- data/spoom_data/7644ff4.json +1 -0
- data/spoom_data/79054db.json +1 -0
- data/spoom_data/814e515.json +1 -0
- data/spoom_data/88c3b60.json +1 -0
- data/spoom_data/8bd4b6e.json +1 -0
- data/spoom_data/93f8153.json +1 -0
- data/spoom_data/95242fe.json +1 -0
- data/spoom_data/97f4c09.json +1 -0
- data/spoom_data/a13d150.json +1 -0
- data/spoom_data/a17f215.json +1 -0
- data/spoom_data/b705a9d.json +1 -0
- data/spoom_data/ba19195.json +1 -0
- data/spoom_data/bef51ca.json +1 -0
- data/spoom_data/dbb595a.json +1 -0
- data/spoom_data/e8fab92.json +1 -0
- data/spoom_data/f166c87.json +1 -0
- data/spoom_data/f57992d.json +1 -0
- data/spoom_data/f5a1e40.json +1 -0
- data/spoom_data/f6d6431.json +1 -0
- data/spoom_data/f72b67c.json +1 -0
- data/spoom_data/fc2f231.json +1 -0
- data/src/mocktail/collects_calls.rb +18 -0
- data/src/mocktail/debug.rb +55 -0
- data/src/mocktail/dsl.rb +46 -0
- data/src/mocktail/errors.rb +19 -0
- data/src/mocktail/explains_nils.rb +41 -0
- data/src/mocktail/explains_thing.rb +137 -0
- data/src/mocktail/grabs_original_method_parameters.rb +33 -0
- data/src/mocktail/handles_dry_call/fulfills_stubbing/describes_unsatisfied_stubbing.rb +27 -0
- data/src/mocktail/handles_dry_call/fulfills_stubbing/finds_satisfaction.rb +24 -0
- data/src/mocktail/handles_dry_call/fulfills_stubbing.rb +45 -0
- data/src/mocktail/handles_dry_call/logs_call.rb +12 -0
- data/src/mocktail/handles_dry_call/validates_arguments.rb +45 -0
- data/src/mocktail/handles_dry_call.rb +25 -0
- data/src/mocktail/handles_dry_new_call.rb +42 -0
- data/src/mocktail/imitates_type/ensures_imitation_support.rb +16 -0
- data/src/mocktail/imitates_type/makes_double/declares_dry_class/reconstructs_call.rb +73 -0
- data/src/mocktail/imitates_type/makes_double/declares_dry_class.rb +136 -0
- data/src/mocktail/imitates_type/makes_double/gathers_fakeable_instance_methods.rb +28 -0
- data/src/mocktail/imitates_type/makes_double.rb +29 -0
- data/src/mocktail/imitates_type.rb +29 -0
- data/src/mocktail/initialize_based_on_type_system_mode_switching.rb +11 -0
- data/src/mocktail/initializes_mocktail.rb +25 -0
- data/src/mocktail/matcher_presentation.rb +21 -0
- data/src/mocktail/matchers/any.rb +27 -0
- data/src/mocktail/matchers/base.rb +39 -0
- data/src/mocktail/matchers/captor.rb +76 -0
- data/src/mocktail/matchers/includes.rb +32 -0
- data/src/mocktail/matchers/includes_hash.rb +12 -0
- data/src/mocktail/matchers/includes_key.rb +12 -0
- data/src/mocktail/matchers/includes_string.rb +12 -0
- data/src/mocktail/matchers/is_a.rb +17 -0
- data/src/mocktail/matchers/matches.rb +19 -0
- data/src/mocktail/matchers/not.rb +17 -0
- data/src/mocktail/matchers/numeric.rb +27 -0
- data/src/mocktail/matchers/that.rb +32 -0
- data/src/mocktail/matchers.rb +19 -0
- data/src/mocktail/raises_neato_no_method_error.rb +93 -0
- data/src/mocktail/records_demonstration.rb +43 -0
- data/src/mocktail/registers_matcher.rb +65 -0
- data/src/mocktail/registers_stubbing.rb +31 -0
- data/src/mocktail/replaces_next.rb +55 -0
- data/src/mocktail/replaces_type/redefines_new.rb +32 -0
- data/src/mocktail/replaces_type/redefines_singleton_methods.rb +80 -0
- data/src/mocktail/replaces_type/runs_sorbet_sig_blocks_before_replacement.rb +39 -0
- data/src/mocktail/replaces_type.rb +36 -0
- data/src/mocktail/resets_state.rb +14 -0
- data/src/mocktail/share/bind.rb +18 -0
- data/src/mocktail/share/cleans_backtrace.rb +22 -0
- data/src/mocktail/share/creates_identifier.rb +39 -0
- data/src/mocktail/share/determines_matching_calls.rb +72 -0
- data/src/mocktail/share/stringifies_call.rb +84 -0
- data/src/mocktail/share/stringifies_method_name.rb +16 -0
- data/src/mocktail/simulates_argument_error/reconciles_args_with_params.rb +27 -0
- data/src/mocktail/simulates_argument_error/recreates_message.rb +34 -0
- data/src/mocktail/simulates_argument_error/transforms_params.rb +58 -0
- data/src/mocktail/simulates_argument_error.rb +36 -0
- data/src/mocktail/sorbet.rb +3 -0
- data/src/mocktail/stringifies_method_signature.rb +53 -0
- data/src/mocktail/typed.rb +5 -0
- data/src/mocktail/value/cabinet.rb +91 -0
- data/src/mocktail/value/call.rb +51 -0
- data/src/mocktail/value/demo_config.rb +10 -0
- data/src/mocktail/value/double.rb +10 -0
- data/src/mocktail/value/double_data.rb +15 -0
- data/src/mocktail/value/explanation.rb +68 -0
- data/src/mocktail/value/explanation_data.rb +19 -0
- data/src/mocktail/value/fake_method_data.rb +11 -0
- data/src/mocktail/value/matcher_registry.rb +27 -0
- data/src/mocktail/value/no_explanation_data.rb +20 -0
- data/src/mocktail/value/signature.rb +35 -0
- data/src/mocktail/value/stubbing.rb +26 -0
- data/src/mocktail/value/top_shelf.rb +79 -0
- data/src/mocktail/value/type_replacement.rb +11 -0
- data/src/mocktail/value/type_replacement_data.rb +19 -0
- data/src/mocktail/value/unsatisfying_call.rb +9 -0
- data/src/mocktail/value/unsatisfying_call_explanation.rb +24 -0
- data/src/mocktail/value.rb +19 -0
- data/src/mocktail/verifies_call/finds_verifiable_calls.rb +21 -0
- data/src/mocktail/verifies_call/raises_verification_error/gathers_calls_of_method.rb +15 -0
- data/src/mocktail/verifies_call/raises_verification_error.rb +74 -0
- data/src/mocktail/verifies_call.rb +37 -0
- data/src/mocktail/version.rb +12 -0
- data/src/mocktail.rb +154 -0
- data/sub_projects/rbi_generator/Gemfile +4 -0
- data/sub_projects/rbi_generator/Gemfile.lock +63 -0
- data/sub_projects/rbi_generator/sorbet/rbi/gems/.gitattributes +1 -0
- data/sub_projects/sorbet_user/Gemfile +13 -0
- data/sub_projects/sorbet_user/Gemfile.lock +126 -0
- data/sub_projects/sorbet_user/Rakefile +10 -0
- data/sub_projects/sorbet_user/bin/tapioca +27 -0
- data/sub_projects/sorbet_user/rbi/mocktail.rbi +9 -0
- data/sub_projects/sorbet_user/script/test +13 -0
- data/sub_projects/sorbet_user/sorbet/config +4 -0
- data/sub_projects/sorbet_user/sorbet/rbi/gems/.gitattributes +1 -0
- data/sub_projects/sorbet_user/sorbet/rbi/gems/ast@2.4.3.rbi +585 -0
- data/sub_projects/sorbet_user/sorbet/rbi/gems/benchmark@0.4.0.rbi +618 -0
- data/sub_projects/sorbet_user/sorbet/rbi/gems/erubi@1.13.1.rbi +155 -0
- data/sub_projects/sorbet_user/sorbet/rbi/gems/json@2.21.2.rbi +2140 -0
- data/sub_projects/sorbet_user/sorbet/rbi/gems/language_server-protocol@3.17.0.4.rbi +14244 -0
- data/sub_projects/sorbet_user/sorbet/rbi/gems/lint_roller@1.1.0.rbi +239 -0
- data/sub_projects/sorbet_user/sorbet/rbi/gems/logger@1.7.0.rbi +963 -0
- data/sub_projects/sorbet_user/sorbet/rbi/gems/m@1.6.2.rbi +258 -0
- data/sub_projects/sorbet_user/sorbet/rbi/gems/method_source@1.1.0.rbi +9 -0
- data/sub_projects/sorbet_user/sorbet/rbi/gems/minitest@5.27.0.rbi +1549 -0
- data/sub_projects/sorbet_user/sorbet/rbi/gems/mocktail@3.0.0.rbi +1923 -0
- data/sub_projects/sorbet_user/sorbet/rbi/gems/netrc@0.11.0.rbi +158 -0
- data/sub_projects/sorbet_user/sorbet/rbi/gems/parallel@1.26.3.rbi +291 -0
- data/sub_projects/sorbet_user/sorbet/rbi/gems/parser@3.3.7.4.rbi +5451 -0
- data/sub_projects/sorbet_user/sorbet/rbi/gems/prism@1.4.0.rbi +41732 -0
- data/sub_projects/sorbet_user/sorbet/rbi/gems/racc@1.8.1.rbi +164 -0
- data/sub_projects/sorbet_user/sorbet/rbi/gems/rainbow@3.1.1.rbi +402 -0
- data/sub_projects/sorbet_user/sorbet/rbi/gems/rake@13.2.1.rbi +3028 -0
- data/sub_projects/sorbet_user/sorbet/rbi/gems/rbi@0.3.1.rbi +6599 -0
- data/sub_projects/sorbet_user/sorbet/rbi/gems/rbs@3.9.2.rbi +6976 -0
- data/sub_projects/sorbet_user/sorbet/rbi/gems/regexp_parser@2.10.0.rbi +3795 -0
- data/sub_projects/sorbet_user/sorbet/rbi/gems/rubocop-ast@1.43.0.rbi +7717 -0
- data/sub_projects/sorbet_user/sorbet/rbi/gems/rubocop-performance@1.24.0.rbi +9 -0
- data/sub_projects/sorbet_user/sorbet/rbi/gems/rubocop-sorbet@0.9.0.rbi +9 -0
- data/sub_projects/sorbet_user/sorbet/rbi/gems/rubocop@1.73.2.rbi +61306 -0
- data/sub_projects/sorbet_user/sorbet/rbi/gems/ruby-progressbar@1.13.0.rbi +1317 -0
- data/sub_projects/sorbet_user/sorbet/rbi/gems/sorbet-eraser@0.3.1.rbi +8 -0
- data/sub_projects/sorbet_user/sorbet/rbi/gems/spoom@1.6.1.rbi +7274 -0
- data/sub_projects/sorbet_user/sorbet/rbi/gems/standard-custom@1.0.2.rbi +8 -0
- data/sub_projects/sorbet_user/sorbet/rbi/gems/standard-performance@1.7.0.rbi +9 -0
- data/sub_projects/sorbet_user/sorbet/rbi/gems/standard-sorbet@0.0.3.rbi +53 -0
- data/sub_projects/sorbet_user/sorbet/rbi/gems/standard@1.47.0.rbi +935 -0
- data/sub_projects/sorbet_user/sorbet/rbi/gems/tapioca@0.16.11.rbi +3628 -0
- data/sub_projects/sorbet_user/sorbet/rbi/gems/thor@1.3.2.rbi +4378 -0
- data/sub_projects/sorbet_user/sorbet/rbi/gems/unicode-display_width@3.1.4.rbi +132 -0
- data/sub_projects/sorbet_user/sorbet/rbi/gems/unicode-emoji@4.2.0.rbi +254 -0
- data/sub_projects/sorbet_user/sorbet/rbi/gems/yard-sorbet@0.9.0.rbi +435 -0
- data/sub_projects/sorbet_user/sorbet/rbi/gems/yard@0.9.45.rbi +18916 -0
- data/sub_projects/sorbet_user/sorbet/tapioca/config.yml +13 -0
- data/sub_projects/sorbet_user/sorbet/tapioca/require.rb +4 -0
- data/sub_projects/sorbet_user/test/ensure_type_safety_test.rb +21 -0
- data/sub_projects/sorbet_user/test/paint_by_number_test.rb +113 -0
- data/sub_projects/sorbet_user/test/sorbet_test.rb +289 -0
- data/sub_projects/sorbet_user/test/test_helper.rb +35 -0
- data/sub_projects/untyped_user/Gemfile +4 -0
- data/sub_projects/untyped_user/Gemfile.lock +25 -0
- data/sub_projects/untyped_user/antitype_test.rb +54 -0
- data/sub_projects/untyped_user/script/test +5 -0
- metadata +264 -8
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# Dependency _inception_ by magically instantiating mocks with `Mocktail.of_next(SomeClass)`
|
|
2
|
+
|
|
3
|
+
Perhaps your typical [subject](/docs/support/glossary.md#subject-under-test)
|
|
4
|
+
instantiates its own [dependencies](/docs/support/glossary.md#dependency):
|
|
5
|
+
|
|
6
|
+
```ruby
|
|
7
|
+
class ThingHaver
|
|
8
|
+
def initialize
|
|
9
|
+
@thing_1 = Thing1.new
|
|
10
|
+
@thing_2 = Thing2.new
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Or, for the sake of testability, follows a
|
|
16
|
+
dependency injection pattern like the following:
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
class ThingHaver
|
|
20
|
+
def initialize(thing_1 = Thing1.new, thing_2 = Thing2.new)
|
|
21
|
+
@thing_1 = thing_1
|
|
22
|
+
@thing_2 = thing_2
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Where, in the universe imagined above, production code always instantiates
|
|
28
|
+
`ThingHaver` with no arguments passed to `new` (meaning that `ThingHaver` is
|
|
29
|
+
invoking `Thing1.new` and `Thing2.new` itself via default argument assignment),
|
|
30
|
+
but unit test code creates mocks of `Thing1` and `Thing2` and passes them in.
|
|
31
|
+
This is a clever pattern, but if our code coverage tools were stricter, we'd see
|
|
32
|
+
this actually results in unit tests that fail to cover the execution of each
|
|
33
|
+
default assignment, which can lead to surprising bugs if you ever stray from
|
|
34
|
+
plain, no-arg constructors.
|
|
35
|
+
|
|
36
|
+
If either of these cases look familiar, then you may find a lot to like with the
|
|
37
|
+
[Mocktail.of_next](/docs/support/api.md#mocktailof_next) convenience function.
|
|
38
|
+
|
|
39
|
+
Here's the first example and a test listing:
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
class ThingHaver
|
|
43
|
+
def initialize
|
|
44
|
+
@thing_1 = Thing1.new
|
|
45
|
+
@thing_2 = Thing2.new
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def have_things
|
|
49
|
+
[@thing_1, @thing_2].map(&:name)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Elsewhere, in a test:
|
|
54
|
+
thing_1 = Mocktail.of_next(Thing1)
|
|
55
|
+
thing_2 = Mocktail.of_next(Thing2)
|
|
56
|
+
subject = ThingHaver.new
|
|
57
|
+
stubs { thing_1.name }.with { :alpha }
|
|
58
|
+
stubs { thing_2.name }.with { :omega }
|
|
59
|
+
|
|
60
|
+
result = subject.have_things
|
|
61
|
+
|
|
62
|
+
assert_equal [:alpha, :omega], result
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
In the above setup, the value of the `subject`'s `@thing_1` instance variable
|
|
66
|
+
will reference the same mock instance as the test's `thing_1` local variable.
|
|
67
|
+
The same goes for the subject's `@thing_2` and test's `thing_2`.
|
|
68
|
+
|
|
69
|
+
Perfectly testable. No goofy dependency injection mechanics necessary.
|
|
70
|
+
|
|
71
|
+
Here's what is going on under the hood:
|
|
72
|
+
|
|
73
|
+
1. When passed `Thing1`, `Mocktail.of_next` does two things:
|
|
74
|
+
a. Makes a fake `Thing1` instance and returns it
|
|
75
|
+
b. Overwrites the `Thing1.new` method with an alternate implementation that
|
|
76
|
+
returns the same fake `Thing1` instance
|
|
77
|
+
2. The next time `Thing1.new` is called and the fake `Thing1` instance is returned,
|
|
78
|
+
Mocktail removes its fake `Thing1.new` method and restores the original, meaning
|
|
79
|
+
subsequent calls to `Thing1.new` will once again return real `Thing1` instances
|
|
80
|
+
|
|
81
|
+
Sneaky!
|
|
82
|
+
|
|
83
|
+
## Handling dependencies instantiated more than once
|
|
84
|
+
|
|
85
|
+
Wait, there's more! In the rare event your subject needs multiple instances of
|
|
86
|
+
the same dependency at a time (suppose one for each element in an array), you
|
|
87
|
+
can generate more than one with [Mocktail.of_next_with_count(type, count)](/docs/support/api.md#mocktailof_next_with_count).
|
|
88
|
+
|
|
89
|
+
Suppose we rewrite the above example with a single `Thing` class instead of
|
|
90
|
+
`Thing1` and `Thing2`. We could use `of_next_with_count` to create both the
|
|
91
|
+
fakes in one go, without losing track of the references:
|
|
92
|
+
|
|
93
|
+
```ruby
|
|
94
|
+
class ThingHaver
|
|
95
|
+
def initialize
|
|
96
|
+
@thing_1 = Thing.new
|
|
97
|
+
@thing_2 = Thing.new
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def have_things
|
|
101
|
+
[@thing_1, @thing_2].map(&:name)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Elsewhere, in a test:
|
|
106
|
+
thing_1, thing_2 = Mocktail.of_next_with_count(Thing, 2)
|
|
107
|
+
subject = ThingHaver.new
|
|
108
|
+
stubs { thing_1.name }.with { :alpha }
|
|
109
|
+
stubs { thing_2.name }.with { :omega }
|
|
110
|
+
|
|
111
|
+
result = subject.have_things
|
|
112
|
+
|
|
113
|
+
assert_equal [:alpha, :omega], result
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## This doesn't work for modules
|
|
117
|
+
|
|
118
|
+
If you want Mocktail to create a mock instance from a reference to a module,
|
|
119
|
+
however, `Mocktail.of_next` won't work—there's no `new` method for it to
|
|
120
|
+
override or for the subject to reference! The best way to do it is either to
|
|
121
|
+
create a test-scoped class that includes the module yourself first (and passing
|
|
122
|
+
that class to the subject somehow so it can call `new` on it) or to give up on
|
|
123
|
+
trying to use `of_next` in favor of
|
|
124
|
+
[Mocktail.of](/docs/support/api.md#mocktailof), which can accept a module and
|
|
125
|
+
return a mock instance.
|
|
126
|
+
|
|
127
|
+
## Behold your awesome power!
|
|
128
|
+
|
|
129
|
+
If your coding style supports it, maximizing `Mocktail.of_next` usage in your
|
|
130
|
+
test setup is a great way to mop up redundant test setup boilerplate while
|
|
131
|
+
potentially eliminating uncovered dependency instantiations from your production
|
|
132
|
+
code.
|
|
133
|
+
|
|
134
|
+
**Keep the magic flowing by [stubbing and verifying some interactions](../../stubbing_and_verifying.md).**
|
|
135
|
+
|
|
136
|
+
**Go back and learn of less magical ways to [create mocks](../../tdd.md).**
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Dependency injection by manually instantiating mocks with `Mocktail.of(SomeClassOrModule)`
|
|
2
|
+
|
|
3
|
+
The method we use to instantiate instances of mocks for a given class or module
|
|
4
|
+
is [Mocktail.of](/docs/support/api.md#mocktailof).
|
|
5
|
+
|
|
6
|
+
Assuming you're passing a [dependency](/docs/support/glossary.md#dependency) to
|
|
7
|
+
your [subject's](/docs/support/glossary.md#subject-under-test) initializer,
|
|
8
|
+
here's an example of how you might do that with `Mocktail.of()`.
|
|
9
|
+
|
|
10
|
+
Given the following subject:
|
|
11
|
+
|
|
12
|
+
```ruby
|
|
13
|
+
class CashRegister
|
|
14
|
+
def initialize(payment_processor)
|
|
15
|
+
@payment_processor = payment_processor
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def boot!
|
|
19
|
+
@payment_processor.establish_connection
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
And this dependency:
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
class PaymentProcessor
|
|
28
|
+
def establish_connection
|
|
29
|
+
# ⚡️ NETWORKING ⚡️
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
You could write a little test with Mocktail like this, using
|
|
35
|
+
[Mocktail.verify](/docs/support/api.md#mocktailverify) for the assertion:
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
payment_processor = Mocktail.of(PaymentProcessor)
|
|
39
|
+
subject = CashRegister.new(payment_processor)
|
|
40
|
+
|
|
41
|
+
subject.boot!
|
|
42
|
+
|
|
43
|
+
verify { payment_processor.establish_connection }
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
In the above example, `Mocktail.of(PaymentProcessor)` returns a fake instance of
|
|
47
|
+
a `PaymentProcessor`, replete with fake instance methods in place of all its
|
|
48
|
+
real ones (and also retaining their parameter signatures). Those fake methods
|
|
49
|
+
will return `nil` by default (unless stubbed with
|
|
50
|
+
[Mocktail.stubs](/docs/support/api.md#mocktailstubs)). As shown above, we can
|
|
51
|
+
assert that the fake `payment_processor`'s `establish_connection` method using
|
|
52
|
+
[Mocktail.verify](/docs/support/api.md#mocktailverify).
|
|
53
|
+
|
|
54
|
+
## Creating mocks of a given module
|
|
55
|
+
|
|
56
|
+
It's worth noting that `Mocktail.of` will gladly receive a module as an argument
|
|
57
|
+
and then create a one-off class only for the purpose of faking it:
|
|
58
|
+
|
|
59
|
+
``` ruby
|
|
60
|
+
module Currency
|
|
61
|
+
def convert(from)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
currency = Mocktail.of(Currency)
|
|
66
|
+
=> #<Mocktail of Currency:0x0000000104a36510>
|
|
67
|
+
|
|
68
|
+
currency.class
|
|
69
|
+
=> #<Class including module for mocktail of Currency:0x00000001077d4620>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The above `currency` mock object will have a fake `convert` method on it, just
|
|
73
|
+
as an instance of a class including the `Currency` module would.
|
|
74
|
+
|
|
75
|
+
## You did it!
|
|
76
|
+
|
|
77
|
+
So there you go, you've got what you need to create mock instances and pass them
|
|
78
|
+
to your subjects.
|
|
79
|
+
|
|
80
|
+
**Delve deeper and explore more about [stubbing and verifying interactions](../../stubbing_and_verifying.md).**
|
|
81
|
+
|
|
82
|
+
**Go back and consider other ways to [create mocks](../../tdd.md).**
|
data/docs/tdd/poro.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Creating mocked instances of classes you own
|
|
2
|
+
|
|
3
|
+
Good news! You find yourself on the golden path of Mocktail usage. Creating
|
|
4
|
+
mocks of instances of classes that you or your team have authored and can
|
|
5
|
+
readily change is right in the crosshairs of what the library was made to do!
|
|
6
|
+
(If you're just starting out you should probably target >90% of your Mocktail
|
|
7
|
+
usage to be of this variety.)
|
|
8
|
+
|
|
9
|
+
Exactly _how_ you create these mocked instances depends on how you prefer to
|
|
10
|
+
get [dependencies](/docs/support/glossary.md#dependency) into the hands of your
|
|
11
|
+
[subject under test](/docs/support/glossary.md#subject-under-test).
|
|
12
|
+
|
|
13
|
+
**Manually pass instances of dependencies to your test subject, AKA [dependency injection](poro/dependency_injection.md).**
|
|
14
|
+
|
|
15
|
+
**Allow your subject to instantiate its dependencies by wielding ✨mocking magic✨, AKA [dependency inception](poro/dependency_inception.md).**
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# Mocking third-party code and gems
|
|
2
|
+
|
|
3
|
+
A common way people use mocking libraries is to isolate the
|
|
4
|
+
[subject](../support/glossary.md#subject-under-test) from third-party code in a
|
|
5
|
+
gem. The reason for this is straightforward enough: gems and standard library
|
|
6
|
+
classes are often used to broker communication between an application's domain
|
|
7
|
+
logic and the outside world via I/O, and one meaningful way to delineate "unit"
|
|
8
|
+
and "integration" tests is to establish boundaries like "unit tests don't
|
|
9
|
+
interact with the file system, or standard I/O, or the network", then use a
|
|
10
|
+
mocking library to enforce that boundary.
|
|
11
|
+
|
|
12
|
+
Take this example code that writes recipes to a CSV file:
|
|
13
|
+
|
|
14
|
+
```ruby
|
|
15
|
+
Recipe = Struct.new(:name, :ingredients, :instructions, keyword_init: true)
|
|
16
|
+
|
|
17
|
+
require "csv"
|
|
18
|
+
|
|
19
|
+
class RecipeWriter
|
|
20
|
+
def write_csv(filename, recipes)
|
|
21
|
+
CSV.open(filename, "w") do |csv|
|
|
22
|
+
csv << ["Name", "Ingredients", "Instructions"]
|
|
23
|
+
|
|
24
|
+
recipes.each do |recipe|
|
|
25
|
+
csv << [recipe.name, recipe.ingredients, recipe.instructions]
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Suppose you wanted to take this method and write a unit test afterward that
|
|
33
|
+
didn't interact with the file system. You could use a mocking library like
|
|
34
|
+
Mocktail to accomplish this.
|
|
35
|
+
|
|
36
|
+
Because `CSV.open` is a class method, we can pass `CSV` to
|
|
37
|
+
[Mocktail.replace](../support/api.md#mocktailreplace) to replace it with a fake:
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
Mocktail.replace(CSV)
|
|
41
|
+
subject = RecipeWriter.new
|
|
42
|
+
csv = Mocktail.of(CSV)
|
|
43
|
+
stubs { CSV.open("some_file.csv", "w") { |blk| blk.call(csv) } }.with { nil }
|
|
44
|
+
|
|
45
|
+
subject.write_csv("some_file.csv", [
|
|
46
|
+
Recipe.new(
|
|
47
|
+
name: "Mojito",
|
|
48
|
+
ingredients: "mint, lime, rum",
|
|
49
|
+
instructions: "muddle the mint then go nuts"
|
|
50
|
+
),
|
|
51
|
+
Recipe.new(
|
|
52
|
+
name: "Negroni",
|
|
53
|
+
ingredients: "gin, campari, sweet vermouth",
|
|
54
|
+
instructions: "pour in a glass"
|
|
55
|
+
)
|
|
56
|
+
])
|
|
57
|
+
|
|
58
|
+
verify { csv << ["Name", "Ingredients", "Instructions"] }
|
|
59
|
+
verify { csv << ["Mojito", "mint, lime, rum", "muddle the mint then go nuts"] }
|
|
60
|
+
verify { csv << ["Negroni", "gin, campari, sweet vermouth", "pour in a glass"] }
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The above is, indeed, an isolated test of the `write_csv` method, as written.
|
|
64
|
+
But, somehow, writing it felt kind of painful!
|
|
65
|
+
|
|
66
|
+
Let us count the pains:
|
|
67
|
+
|
|
68
|
+
1. The test is forced to replace a [global class method](class_methods.md)
|
|
69
|
+
(`CSV.open`) when an instance would have been simpler and less far-reaching
|
|
70
|
+
2. Regardless, we had to create a mock instance of `CSV` anyway, because that's
|
|
71
|
+
the type passed to `CSV.open`'s block param. This could confuse readers skimming
|
|
72
|
+
the test, since now we've faked `CSV`'s class methods as well as creating a fake
|
|
73
|
+
`CSV` instance
|
|
74
|
+
3. That fake `CSV` instance gets worse, because the way the subject receives the
|
|
75
|
+
value is through a block param, which requires us to invoke the `blk.call(csv)` to pass it in during our stubbing [demonstration](../support/glossary.md#demonstration). This won't
|
|
76
|
+
be clear to anyone who isn't familiar with how Mocktail is being used
|
|
77
|
+
4. The best assertion we can manage is to verify that the expected calls to
|
|
78
|
+
`CSV#<<` occurred, but it means the dependency's contract is limited to a side
|
|
79
|
+
effect instead of a return value—which would be easier to debug and compose.
|
|
80
|
+
5. Finally, those `verify` calls do nothing to ensure they were called in the
|
|
81
|
+
correct order or even inside the `CSV.open` block—both of which are necessary
|
|
82
|
+
for the file to be written correctly—indicating a logical gap in the test's
|
|
83
|
+
coverage (a custom assertion could be written to validate call-order using
|
|
84
|
+
[Mocktail.calls](../support/api.md#mocktailcalls), but it wouldn't be pretty)
|
|
85
|
+
|
|
86
|
+
That's five pain points we encountered in the writing of a single test of a
|
|
87
|
+
pretty simple method!
|
|
88
|
+
|
|
89
|
+
What could we have done to avoid that pain? Well, because we're mocking a
|
|
90
|
+
third-party API (Ruby's standard library [csv](https://github.com/ruby/csv)
|
|
91
|
+
gem), all that pain was unavoidable! If it turns out to be hard to mock out
|
|
92
|
+
interactions with third-party code, it's not like we can easily change it to be
|
|
93
|
+
easier to work with.
|
|
94
|
+
|
|
95
|
+
Zooming out, the primary intended benefit of practicing [isolation
|
|
96
|
+
testing](../support/glossary.md#isolated-unit-testing) is to improve our code's
|
|
97
|
+
design. If we listen to testing pain as we design the interaction between the
|
|
98
|
+
subject and its [dependencies](../support/glossary.md#dependency) and respond to
|
|
99
|
+
the pain we experience in our tests by changing the API of the _production_
|
|
100
|
+
code, it improves that code's usability for everyone, not just a test.
|
|
101
|
+
Easier-to-fake code is inherently simpler and therefore easier-to-use code, so
|
|
102
|
+
isolated TDD really serves as a useful proxy to put a healthy pressure on
|
|
103
|
+
developers to arrive at simple designs.
|
|
104
|
+
|
|
105
|
+
So, if a core tenet of isolated testing with mocks is to listen to testing pain
|
|
106
|
+
as a prompt to improve the design of our subjects' dependencies and we can't
|
|
107
|
+
change the design of third-party code when it proves painful, then it stands to
|
|
108
|
+
reason we're not getting the most out of the practice of isolated test-driven
|
|
109
|
+
development when we mock code we don't own. All we're doing in this case is
|
|
110
|
+
subjecting ourselves to unnecessary, useless pain.
|
|
111
|
+
|
|
112
|
+
So, what can we do instead? One strategy is to introduce a [wrapper
|
|
113
|
+
object](../support/glossary.md#wrapper-object) that we _do own_ and use it to
|
|
114
|
+
house our dependence on the `csv` gem. Then we can update our code to depend on
|
|
115
|
+
the wrapper and once again use [test
|
|
116
|
+
doubles](../support/glossary.md#test-double) for their intended purpose: to
|
|
117
|
+
improve the design of the wrapper's API.
|
|
118
|
+
|
|
119
|
+
In this example, that extract refactor might look like this:
|
|
120
|
+
|
|
121
|
+
```ruby
|
|
122
|
+
require "csv"
|
|
123
|
+
module Wrap
|
|
124
|
+
class Csv
|
|
125
|
+
def write(filename, header, rows)
|
|
126
|
+
CSV.open(filename, "w") do |csv|
|
|
127
|
+
csv << header
|
|
128
|
+
rows.each do |row|
|
|
129
|
+
csv << row
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
class RecipeWriter
|
|
137
|
+
def initialize
|
|
138
|
+
@csv = Wrap::Csv.new
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def write_csv(filename, recipes)
|
|
142
|
+
@csv.write(
|
|
143
|
+
filename,
|
|
144
|
+
["Name", "Ingredients", "Instructions"],
|
|
145
|
+
recipes.map { |recipe|
|
|
146
|
+
[recipe.name, recipe.ingredients, recipe.instructions]
|
|
147
|
+
}
|
|
148
|
+
)
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
This refactor would result in a much simpler test of `RecipeWriter#write_csv`
|
|
154
|
+
if we took a second stab at it:
|
|
155
|
+
|
|
156
|
+
```ruby
|
|
157
|
+
csv = Mocktail.of_next(Wrap::Csv)
|
|
158
|
+
subject = RecipeWriter.new
|
|
159
|
+
|
|
160
|
+
subject.write_csv("some_file.csv", [
|
|
161
|
+
Recipe.new(
|
|
162
|
+
name: "Mojito",
|
|
163
|
+
ingredients: "mint, lime, rum",
|
|
164
|
+
instructions: "muddle the mint then go nuts"
|
|
165
|
+
),
|
|
166
|
+
Recipe.new(
|
|
167
|
+
name: "Negroni",
|
|
168
|
+
ingredients: "gin, campari, sweet vermouth",
|
|
169
|
+
instructions: "pour in a glass"
|
|
170
|
+
)
|
|
171
|
+
])
|
|
172
|
+
|
|
173
|
+
verify {
|
|
174
|
+
csv.write(
|
|
175
|
+
"some_file.csv",
|
|
176
|
+
["Name", "Ingredients", "Instructions"],
|
|
177
|
+
[
|
|
178
|
+
["Mojito", "mint, lime, rum", "muddle the mint then go nuts"],
|
|
179
|
+
["Negroni", "gin, campari, sweet vermouth", "pour in a glass"]
|
|
180
|
+
]
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Much more straightforward. It also resolves #1, #2, #3, and #5 on our hit list
|
|
186
|
+
of pain points above. The only issue the new factoring doesn't address is the
|
|
187
|
+
fact that the `Wrap::Csv#write` has a side effect instead of a return value, but
|
|
188
|
+
because our ultimate dependency (`CSV.open`) is effectively a fire-and-forget
|
|
189
|
+
method, it's not clear what return value we might want to introduce here without
|
|
190
|
+
knowing more about the needs of the caller.
|
|
191
|
+
|
|
192
|
+
Still, not bad at all. The new class is much more straightforward and its style
|
|
193
|
+
more consistent with the rest of our application code that was guided by tests.
|
|
194
|
+
|
|
195
|
+
## Testing wrapper objects
|
|
196
|
+
|
|
197
|
+
You might be asking, "but who's testing the wrapper objects", and that's a
|
|
198
|
+
question worth asking! In general, if a wrapper is sufficiently simple—meaning,
|
|
199
|
+
without logical branching—then it's usually sufficient to rely on your
|
|
200
|
+
end-to-end testing to test your wrappers, as they would surely fail if things
|
|
201
|
+
didn't work. Testing them on their own in earnest often veers towards [testing
|
|
202
|
+
the
|
|
203
|
+
framework](https://web.archive.org/web/20210124184231/https://bignerdranch.com/blog/what-does-dont-test-the-framework-mean/).
|
|
204
|
+
|
|
205
|
+
## Also, mocking gems doesn't always work
|
|
206
|
+
|
|
207
|
+
Additionally, Mocktail can't warrant that its methods will work on every class
|
|
208
|
+
in every gem out there. If you try to mock a third-party API directly and
|
|
209
|
+
something goes wrong, we can't offer support if you open an issue. Instead, we'd
|
|
210
|
+
encourage you to try introducing a wrapper as shown above and mock that instead.
|
|
211
|
+
|
|
212
|
+
## Restarting the first party
|
|
213
|
+
|
|
214
|
+
Okay, now that we've covered some details on how to mock third-party code, let's
|
|
215
|
+
keep up the tempo.
|
|
216
|
+
|
|
217
|
+
**Head back onto the golden path and use Mocktail to create [fake instances of Ruby classes](./poro.md).**
|
|
218
|
+
|
|
219
|
+
**Wield your newfound gem-faking wizardry to [stub and verify their methods](../stubbing_and_verifying.md).**
|
|
220
|
+
|
data/docs/tdd.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Using Mocktail for test-driven development
|
|
2
|
+
|
|
3
|
+
If you plan to use this mocking library as a tool in your test-driven
|
|
4
|
+
development workflow, you've come to the right place—it's what Mocktail was
|
|
5
|
+
designed for!
|
|
6
|
+
|
|
7
|
+
There are several flavors of test-driven development, but the relevant
|
|
8
|
+
distinction for figuring out which direction you want to take Mocktail lies
|
|
9
|
+
before you:
|
|
10
|
+
|
|
11
|
+
**Use Mocktail to create [fake instances of Ruby classes](tdd/poro.md) you own.**
|
|
12
|
+
|
|
13
|
+
**Use Mocktail to [fake out class and module methods](tdd/class_methods.md) of types you own.**
|
|
14
|
+
|
|
15
|
+
**Use Mocktail to [fake out third-party code](tdd/third_party.md) you can't readily change yourself.**
|
data/lib/mocktail/debug.rb
CHANGED
|
@@ -22,11 +22,12 @@ module Mocktail
|
|
|
22
22
|
base_path = Pathname.new(__FILE__).dirname.to_s
|
|
23
23
|
backtrace_minus_this_and_whoever_called_this = e.backtrace&.[](2..)
|
|
24
24
|
internal_call_sites = backtrace_minus_this_and_whoever_called_this&.take_while { |call_site|
|
|
25
|
-
# the "in `block"
|
|
25
|
+
# the "in `block"/"in 'block" (Ruby 3.4+ uses a single quote instead of a
|
|
26
|
+
# backtick) is very confusing but necessary to include lines after
|
|
26
27
|
# a stubs { blah.foo }.with { … } call, since that's when most of the
|
|
27
28
|
# good stuff happens
|
|
28
|
-
call_site.start_with?(base_path) || call_site.
|
|
29
|
-
}&.reject { |call_site| call_site.
|
|
29
|
+
call_site.start_with?(base_path) || call_site.match?(/in [`']block/)
|
|
30
|
+
}&.reject { |call_site| call_site.match?(/in [`']block/) } || []
|
|
30
31
|
|
|
31
32
|
approved_call_sites = [
|
|
32
33
|
/fulfills_stubbing.rb:(16|20)/,
|
|
@@ -24,7 +24,7 @@ module Mocktail
|
|
|
24
24
|
positional_args: dry_call.args,
|
|
25
25
|
|
|
26
26
|
keyword_params: Params.new(
|
|
27
|
-
all: params.select { |type, _|
|
|
27
|
+
all: params.select { |type, _| # standard:disable Style/HashSlice
|
|
28
28
|
[:keyreq, :key, :keyrest].include?(type)
|
|
29
29
|
}.map { |pair| pair.fetch(1) },
|
|
30
30
|
required: params.select { |t, _| Bind.call(t, :==, :keyreq) }.map { |pair| pair.fetch(1) },
|
|
@@ -43,7 +43,7 @@ module Mocktail
|
|
|
43
43
|
def name_unnamed_params(params)
|
|
44
44
|
params.map.with_index { |param, i|
|
|
45
45
|
if param.size == 1
|
|
46
|
-
param + ["unnamed_arg_#{i + 1}"
|
|
46
|
+
param + [:"unnamed_arg_#{i + 1}"]
|
|
47
47
|
else
|
|
48
48
|
param
|
|
49
49
|
end
|
|
@@ -24,11 +24,12 @@ module Mocktail
|
|
|
24
24
|
base_path = Pathname.new(__FILE__).dirname.to_s
|
|
25
25
|
backtrace_minus_this_and_whoever_called_this = e.backtrace&.[](2..)
|
|
26
26
|
internal_call_sites = backtrace_minus_this_and_whoever_called_this&.take_while { |call_site|
|
|
27
|
-
# the "in `block"
|
|
27
|
+
# the "in `block"/"in 'block" (Ruby 3.4+ uses a single quote instead of a
|
|
28
|
+
# backtick) is very confusing but necessary to include lines after
|
|
28
29
|
# a stubs { blah.foo }.with { … } call, since that's when most of the
|
|
29
30
|
# good stuff happens
|
|
30
|
-
call_site.start_with?(base_path) || call_site.
|
|
31
|
-
}&.reject { |call_site| call_site.
|
|
31
|
+
call_site.start_with?(base_path) || call_site.match?(/in [`']block/)
|
|
32
|
+
}&.reject { |call_site| call_site.match?(/in [`']block/) } || []
|
|
32
33
|
|
|
33
34
|
approved_call_sites = [
|
|
34
35
|
/fulfills_stubbing.rb:(16|20)/,
|
|
@@ -12,7 +12,6 @@ module Mocktail
|
|
|
12
12
|
sig { params(calls: T::Array[Call], nonzero_message: String, zero_message: String, anonymous_blocks: T::Boolean, always_parens: T::Boolean).returns(String) }
|
|
13
13
|
def stringify_multiple(calls, nonzero_message:, zero_message:,
|
|
14
14
|
anonymous_blocks: false, always_parens: false)
|
|
15
|
-
|
|
16
15
|
if calls.empty?
|
|
17
16
|
"#{zero_message}.\n"
|
|
18
17
|
else
|
|
@@ -28,7 +28,7 @@ module Mocktail
|
|
|
28
28
|
positional_args: dry_call.args,
|
|
29
29
|
|
|
30
30
|
keyword_params: Params.new(
|
|
31
|
-
all: params.select { |type, _|
|
|
31
|
+
all: params.select { |type, _| # standard:disable Style/HashSlice
|
|
32
32
|
[:keyreq, :key, :keyrest].include?(type)
|
|
33
33
|
}.map { |pair| pair.fetch(1) },
|
|
34
34
|
required: params.select { |t, _| Bind.call(t, :==, :keyreq) }.map { |pair| pair.fetch(1) },
|
|
@@ -48,7 +48,7 @@ module Mocktail
|
|
|
48
48
|
def name_unnamed_params(params)
|
|
49
49
|
params.map.with_index { |param, i|
|
|
50
50
|
if param.size == 1
|
|
51
|
-
param + ["unnamed_arg_#{i + 1}"
|
|
51
|
+
param + [:"unnamed_arg_#{i + 1}"]
|
|
52
52
|
else
|
|
53
53
|
param
|
|
54
54
|
end
|
|
@@ -36,7 +36,7 @@ module Mocktail
|
|
|
36
36
|
:singleton, :double, :original_type, :dry_type,
|
|
37
37
|
:method, :original_method, :args, :kwargs, :block
|
|
38
38
|
].all? { |attr|
|
|
39
|
-
instance_variable_get("@#{attr}") == other.send(attr)
|
|
39
|
+
instance_variable_get(:"@#{attr}") == other.send(attr)
|
|
40
40
|
}
|
|
41
41
|
else
|
|
42
42
|
false
|
data/lib/mocktail/value/call.rb
CHANGED
|
@@ -32,7 +32,7 @@ module Mocktail
|
|
|
32
32
|
:singleton, :double, :original_type, :dry_type,
|
|
33
33
|
:method, :original_method, :args, :kwargs, :block
|
|
34
34
|
].all? { |attr|
|
|
35
|
-
instance_variable_get("@#{attr}") == other.send(attr)
|
|
35
|
+
instance_variable_get(:"@#{attr}") == other.send(attr)
|
|
36
36
|
}
|
|
37
37
|
else
|
|
38
38
|
false
|
data/lib/mocktail/version.rb
CHANGED
data/mocktail.gemspec
CHANGED
|
@@ -12,7 +12,7 @@ Gem::Specification.new do |spec|
|
|
|
12
12
|
|
|
13
13
|
spec.summary = "Take your objects, and make them a double"
|
|
14
14
|
spec.homepage = "https://github.com/testdouble/mocktail"
|
|
15
|
-
spec.required_ruby_version = Gem::Requirement.new(">= 3.
|
|
15
|
+
spec.required_ruby_version = Gem::Requirement.new(">= 3.1.0")
|
|
16
16
|
|
|
17
17
|
spec.metadata["homepage_uri"] = spec.homepage
|
|
18
18
|
spec.metadata["source_code_uri"] = spec.homepage
|
|
@@ -22,7 +22,7 @@ Gem::Specification.new do |spec|
|
|
|
22
22
|
# Specify which files should be added to the gem when it is released.
|
|
23
23
|
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
|
24
24
|
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
|
25
|
-
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features
|
|
25
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
|
|
26
26
|
end
|
|
27
27
|
spec.bindir = "exe"
|
|
28
28
|
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|