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,151 @@
|
|
|
1
|
+
# Mocking out network requests by faking HTTP
|
|
2
|
+
|
|
3
|
+
Mocktail does nothing to make faking HTTP requests easy. And if you care to try,
|
|
4
|
+
know that it also does nothing to ensure mocked requests behave consistently
|
|
5
|
+
across various HTTP libraries and gems (`Net::HTTP`, faraday, httparty, etc.).
|
|
6
|
+
In fact, other gems are designed to do this and only this, most notably
|
|
7
|
+
[webmock](https://github.com/bblimke/webmock).
|
|
8
|
+
|
|
9
|
+
But, if you're reading this, there's a _small chance_ you're thinking about
|
|
10
|
+
faking out the network in the context of something you're conceiving of as a
|
|
11
|
+
unit test, and that's why you're looking to your mocking library for an answer.
|
|
12
|
+
If that's the case, here's a quick note on why you might be trying to apply
|
|
13
|
+
the wrong solution to the problem.
|
|
14
|
+
|
|
15
|
+
Suppose you're writing a unit test and trying to control for the network and
|
|
16
|
+
thinking about using a library like Mocktail as a result. To illustrate: imagine
|
|
17
|
+
you're testing a method that makes a network request and does one thing if the
|
|
18
|
+
response succeeds and another thing to handle failures.
|
|
19
|
+
|
|
20
|
+
You might want to write these two test cases:
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
def test_success
|
|
24
|
+
result = @subject.hack_computer("/school_sprinklers")
|
|
25
|
+
|
|
26
|
+
assert_equal "Mess with the best, die like the rest", result
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def test_failure
|
|
30
|
+
assert_raises(CrashOverrideError) do
|
|
31
|
+
@subject.hack_computer("/the_gibson")
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
But you may not have an easy way to force the first test case to always exercise
|
|
37
|
+
the happy path in which the request succeeds. Similarly, you would need a way to
|
|
38
|
+
ensure the other test case to consistently travels the sad path wherein the
|
|
39
|
+
request fails. Short of spinning up a fake HTTP service, you don't have a lot
|
|
40
|
+
of options, especially if your implementation looks anything like this:
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
class Computer
|
|
44
|
+
SICK_BURN = "Mess with the best, die like the rest"
|
|
45
|
+
|
|
46
|
+
def hack_computer(target)
|
|
47
|
+
uri = URI.parse("https://example.com#{target}")
|
|
48
|
+
req = Net::HTTP::Post.new(uri)
|
|
49
|
+
|
|
50
|
+
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
|
|
51
|
+
http.request(req) do |res|
|
|
52
|
+
if Net::HTTPSuccess === res
|
|
53
|
+
return SICK_BURN
|
|
54
|
+
else
|
|
55
|
+
raise CrashOverrideError
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
This case is so simple that it may strain credulity, but instead of trying to
|
|
64
|
+
introduce a network-layer mock to your application-layer concern, you might
|
|
65
|
+
consider writing a [wrapper](../support/glossary.md#wrapper-object) around the
|
|
66
|
+
`Net::HTTP` API that you don't control and can't change that your application
|
|
67
|
+
can invoke wherever it needs to make a network request. Then, instead of your
|
|
68
|
+
test needing to mock out the entire networking stack, it just needs to _mock out
|
|
69
|
+
the wrapper_ instead.
|
|
70
|
+
|
|
71
|
+
Here's what the refactored [subject](../support/glossary.md#subject-under-test)
|
|
72
|
+
could look like if we extracted its usage of `Net::HTTP` into an `Http` wrapper
|
|
73
|
+
that we own:
|
|
74
|
+
|
|
75
|
+
```ruby
|
|
76
|
+
class Computer
|
|
77
|
+
SICK_BURN = "Mess with the best, die like the rest"
|
|
78
|
+
|
|
79
|
+
def initialize
|
|
80
|
+
@http = Http.new
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def hack_computer(target)
|
|
84
|
+
if @http.post("https://example.com#{target}").success?
|
|
85
|
+
return SICK_BURN
|
|
86
|
+
else
|
|
87
|
+
raise CrashOverrideError
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
class Http
|
|
93
|
+
Result = Data.define(:json, :success?)
|
|
94
|
+
|
|
95
|
+
def post(url)
|
|
96
|
+
uri = URI.parse(url)
|
|
97
|
+
req = Net::HTTP::Post.new(uri)
|
|
98
|
+
|
|
99
|
+
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
|
|
100
|
+
http.request(req) do |res|
|
|
101
|
+
return Result.new(
|
|
102
|
+
data: JSON.parse(res.body),
|
|
103
|
+
success?: Net::HTTPSuccess === res
|
|
104
|
+
)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Now, the test will be much easier to implement, because instead of trying to
|
|
112
|
+
figure out how to mock out all of HTTP, we're simply mocking out an object that
|
|
113
|
+
quacks just like any other object—leaving the fact that it crosses a spooky
|
|
114
|
+
network boundary as a mere implementation detail that `Computer`'s test doesn't
|
|
115
|
+
need to worry about.
|
|
116
|
+
|
|
117
|
+
Now, a complete test could fully cover the subject with minimally-invasive
|
|
118
|
+
mocking:
|
|
119
|
+
|
|
120
|
+
```ruby
|
|
121
|
+
def initialize
|
|
122
|
+
@http = Mocktail.of_next(Http)
|
|
123
|
+
|
|
124
|
+
@subject = Computer.new
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def test_success
|
|
128
|
+
stubs { Http.post("/school_sprinklers") }.with { Computer::Result.new(data: nil, success?: true) }
|
|
129
|
+
|
|
130
|
+
result = @subject.hack_computer("/school_sprinklers")
|
|
131
|
+
|
|
132
|
+
assert_equal "Mess with the best, die like the rest", result
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def test_failure
|
|
136
|
+
stubs { Http.post("/the_gibson") }.with { Computer::Result.new(data: nil, success?: false) }
|
|
137
|
+
|
|
138
|
+
assert_raises(CrashOverrideError) do
|
|
139
|
+
@subject.hack_computer("/the_gibson")
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Anyway, that's why you _probably_ don't want to fake out an HTTP interaction
|
|
145
|
+
directly with Mocktail and should instead look to a purpose-built gem like
|
|
146
|
+
[webmock](https://github.com/bblimke/webmock) or else consider alternative
|
|
147
|
+
designs that make the code in question easier to test.
|
|
148
|
+
|
|
149
|
+
**If you've heard enough, you can go back and consider [non-TDD use cases for Mocktail](../other_uses.md).**
|
|
150
|
+
|
|
151
|
+
**Or if you're finally ready to walk the golden path, you can revisit [Mocktail as a TDD tool](../tdd.md).**
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# Mocking methods on the subject being tested
|
|
2
|
+
|
|
3
|
+
Once in a while, it may seem desirable to fake out a method on the [subject
|
|
4
|
+
under test](../support/glossary.md#subject-under-test).
|
|
5
|
+
|
|
6
|
+
The scenario usually looks something like this:
|
|
7
|
+
|
|
8
|
+
```ruby
|
|
9
|
+
class User < ApplicationRecord
|
|
10
|
+
# The method under test:
|
|
11
|
+
def best_promo
|
|
12
|
+
promos = CouponCode.active.flat_map { |coupon_code|
|
|
13
|
+
eligible_promotions(coupon_code)
|
|
14
|
+
}.max_by { |promo|
|
|
15
|
+
promo.percent_discount
|
|
16
|
+
}
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# … 45 other methods
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def eligible_promotions(coupon_code)
|
|
24
|
+
# 30 lines of gnarly queries and conditions
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Suppose a developer wants to open up `user_spec.rb` and write a spec for
|
|
30
|
+
`best_promo`. While `best_promo` might not be the simplest method in the world,
|
|
31
|
+
one might imagine a straightforward test with two or three test cases based on
|
|
32
|
+
the existence of different `CouponCode` records.
|
|
33
|
+
|
|
34
|
+
But inevitably, during test setup, it becomes clear that the stuff going on in
|
|
35
|
+
`eligible_promotions` is _intense_, and in order to get the database into the
|
|
36
|
+
state it would need to be to exercise the logic in `best_promo`, the test would
|
|
37
|
+
require fifteen lines of painful and confusing setup code.
|
|
38
|
+
|
|
39
|
+
This gives rise to the idea: "I could just mock out `eligible_promotions` and
|
|
40
|
+
this test would be far clearer and easier to write".
|
|
41
|
+
|
|
42
|
+
Of course, if that idea led you to this page, I have bad news: Mocktail
|
|
43
|
+
can't help you here and it's our opinion that getting into the habit of using
|
|
44
|
+
mocking for this purpose is a _really_ bad idea.
|
|
45
|
+
|
|
46
|
+
Why?
|
|
47
|
+
|
|
48
|
+
First of all, to mock the `eligible_promotions` method in this case would be an
|
|
49
|
+
example of a [partial mock](../support/glossary.md#partial-mock) (discussed
|
|
50
|
+
[elsewhere](./partial_mocks.md)). Moreover, that partial mock would be the
|
|
51
|
+
_subject itself_—meaning, a test to validate a subject's public API would fake
|
|
52
|
+
out the subject's private implementation. As a result, no meaningful confidence
|
|
53
|
+
would really be gained from such a test. At best, a test might tell you how the
|
|
54
|
+
`best_promo` method would behave in the hypothetical scenario of however
|
|
55
|
+
`eligible_promotions` was [stubbed](../support/glossary.md#stub), but that
|
|
56
|
+
isn't a particularly interesting thing to know (much less codify in a test suite
|
|
57
|
+
for perpetuity).
|
|
58
|
+
|
|
59
|
+
Nevertheless, "mocking the subject" has been remarkably common in Rails
|
|
60
|
+
applications since the mid-2000s. In 2010, [DHH
|
|
61
|
+
tweeted](https://twitter.com/dhh/status/27444365459?s=20) something that was
|
|
62
|
+
seen as controversial with respect to test runners (as RSpec had gained a
|
|
63
|
+
massive following) but entirely mundane when it came to mocking libraries
|
|
64
|
+
([mocha](https://github.com/freerange/mocha), in this case):
|
|
65
|
+
|
|
66
|
+
> Q: What testing framework do you use at 37signals? A: test/unit with the occasional splash of mocha. (That's all you need for great testing)
|
|
67
|
+
|
|
68
|
+
In the experience of the present author, that "splash of mocha" was almost
|
|
69
|
+
always used in cases like the above: to fake out one method in an Active Record
|
|
70
|
+
model in the service of testing another method in the same model. It made the
|
|
71
|
+
initial writing of a test easier, but at the cost of its comprehensibility and
|
|
72
|
+
long-term maintainability.
|
|
73
|
+
|
|
74
|
+
So, if mocking out a method on the subject isn't the solution, what should you
|
|
75
|
+
do instead?
|
|
76
|
+
|
|
77
|
+
In almost every case, this situation is indicating that the subject is serving
|
|
78
|
+
too many disparate purposes and the only workable answer is that the method
|
|
79
|
+
being tested, the method being mocked, or perhaps _both_ should be extracted
|
|
80
|
+
into classes of their own—complete with proper names and a clear
|
|
81
|
+
[dependency](../support/glossary.md#dependency) relationship. Only _then_ might
|
|
82
|
+
an isolated unit test with mocks be appropriate as a tool to help specify the
|
|
83
|
+
terms of that relationship.
|
|
84
|
+
|
|
85
|
+
To illustrate, we could carry through this refactor by extracting both methods
|
|
86
|
+
into new classes and referencing them from the original `best_promo` entrypoint
|
|
87
|
+
in the model:
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
class User < ApplicationRecord
|
|
91
|
+
def best_promo
|
|
92
|
+
FindsBestPromo.new.find(CouponCode.active)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
class FindsBestPromo
|
|
97
|
+
def initialize
|
|
98
|
+
@determines_eligible_promotions = DeterminesEligiblePromotions.new
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def find(coupon_codes)
|
|
102
|
+
coupon_codes.flat_map { |coupon_code|
|
|
103
|
+
@determines_eligible_promotions.determine(coupon_code)
|
|
104
|
+
}.max_by { |promo|
|
|
105
|
+
promo.percent_discount
|
|
106
|
+
}
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
class DeterminesEligiblePromotions
|
|
111
|
+
def determine(coupon_code)
|
|
112
|
+
# 30 lines, still gnarly
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Finally, we could write a test of `FindsBestPromo` that used Mocktail:
|
|
118
|
+
|
|
119
|
+
```ruby
|
|
120
|
+
class FindsBestPromoTest < Minitest::Test
|
|
121
|
+
def setup
|
|
122
|
+
@determines_eligible_promotions = Mocktail.of_next(DeterminesEligiblePromotions)
|
|
123
|
+
@subject = FindsBestPromo.new
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def test_max_discount
|
|
127
|
+
stubs { @determines_eligible_promotions.determine("NEAT25") }.with {
|
|
128
|
+
[
|
|
129
|
+
Promo.new(name: "A", percent_discount: 20),
|
|
130
|
+
Promo.new(name: "B", percent_discount: 25)
|
|
131
|
+
]
|
|
132
|
+
}
|
|
133
|
+
stubs { @determines_eligible_promotions.determine("COOL15") }.with {
|
|
134
|
+
[Promo.new(name: "C", percent_discount: 15)]
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
result = @subject.find(["NEAT25", "COOL15"])
|
|
138
|
+
|
|
139
|
+
assert_equal "B", result.name
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Given the starting point, however, it's important to admit that nothing about
|
|
145
|
+
this is easy. Extracting behavior from a years-old class that's juggling dozens
|
|
146
|
+
of responsibilities where inter-dependencies abound is never as simple as
|
|
147
|
+
cut-and-paste. And creating new, single-use classes in a codebase dominated by a
|
|
148
|
+
handful of massive files will surely feel out of place and inconsistent.
|
|
149
|
+
Finally, deviating from the perceived "Rails Way" requires thoughtful planning,
|
|
150
|
+
and is likely to engender conflict without winning the buy-in of all interested
|
|
151
|
+
parties. That said, there's no time like the present, because this work willl
|
|
152
|
+
only become more difficult with time.
|
|
153
|
+
|
|
154
|
+
If you've read this far and feel a little hopeless about how to make forward
|
|
155
|
+
progress wrangling complexity of this scale, it may make sense to discuss
|
|
156
|
+
bringing in outside help, like [the kind we offer at Test
|
|
157
|
+
Double](https://testdouble.com/contact).
|
|
158
|
+
|
|
159
|
+
**If you've heard enough, you can go back and consider [non-TDD use cases for Mocktail](../other_uses.md).**
|
|
160
|
+
|
|
161
|
+
**Or if you're finally ready to walk the golden path, you can revisit [Mocktail as a TDD tool](../tdd.md).**
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Mocking out time
|
|
2
|
+
|
|
3
|
+
If you want to fake out time in a Ruby test, there are three things to think
|
|
4
|
+
about:
|
|
5
|
+
|
|
6
|
+
1. Editing the actual system time will prevent you from running tests in
|
|
7
|
+
parallel, will break HTTPS/SSL certificate authentication, and will generally
|
|
8
|
+
wreak havoc on most modern computers
|
|
9
|
+
|
|
10
|
+
2. Editing the value of when "now" is for `Time` and `Date` in Ruby means that
|
|
11
|
+
your Ruby code will have a very different time than your database, any network
|
|
12
|
+
dependencies, and any binaries you shell out to
|
|
13
|
+
|
|
14
|
+
3. _Freezing_ time to a specific, fixed value can open the door to bugs due to
|
|
15
|
+
code that comes to depend on that fixedness. For example, two measurements of
|
|
16
|
+
"now" equalling one another when that can never be guaranteed to happen in
|
|
17
|
+
production
|
|
18
|
+
|
|
19
|
+
Where does that leave you? I don't know, but Mocktail doesn't do anything to
|
|
20
|
+
help you here.
|
|
21
|
+
|
|
22
|
+
The best tool for this job is definitely the [timecop
|
|
23
|
+
gem](https://github.com/travisjeffery/timecop). Just use its `travel` methods to
|
|
24
|
+
shift the Ruby-time to where you want, and be mindful that the time will
|
|
25
|
+
disagree with any network or system dependencies.
|
|
26
|
+
|
|
27
|
+
Oh, and if you're looking at the [ActiveSupport time
|
|
28
|
+
helpers](https://api.rubyonrails.org/v5.2.4/classes/ActiveSupport/Testing/TimeHelpers.html),
|
|
29
|
+
just know that even when they _say_ `travel`, they're actually _freezing_ time,
|
|
30
|
+
which creates the category of problems described in issue #3 above.
|
|
31
|
+
|
|
32
|
+
**If you've heard enough, you can go back and consider [non-TDD use cases for Mocktail](../other_uses.md).**
|
|
33
|
+
|
|
34
|
+
**Or if you're finally ready to walk the golden path, you can revisit [Mocktail as a TDD tool](../tdd.md).**
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Creating partial mocks that fake out a subset of methods
|
|
2
|
+
|
|
3
|
+
If you've looked at Mocktail's APIs for [creating mock
|
|
4
|
+
instances](../support/api.md#mocktailof) and [replacing class or module
|
|
5
|
+
methods](../support/api.md#mocktailreplace), you'll find that it's an a bit of
|
|
6
|
+
an _all or nothing_ affair. You can get create an instance of a class with
|
|
7
|
+
_all_ its methods replaced with `nil`-returning fakes and you can replace _all_
|
|
8
|
+
a module's module methods, but **you can't tell Mocktail to only replace one or a
|
|
9
|
+
handful of methods while continuing to call through to some other real methods**.
|
|
10
|
+
|
|
11
|
+
Why?! Why would we do something so unfair as to withhold such obviously useful
|
|
12
|
+
functionality?
|
|
13
|
+
|
|
14
|
+
Well, the answer—like everything having to do with [test
|
|
15
|
+
doubles](../support/glossary.md#test-double)—requires some nuance. In testing
|
|
16
|
+
parlance, to replace some of the methods on a
|
|
17
|
+
[dependency](../support/glossary.md#dependency) but not all of them is to create
|
|
18
|
+
what is called a [partial mock](../support/glossary.md#partial-mock) (click
|
|
19
|
+
through its glossary definition for some of the reasons partial mocks are
|
|
20
|
+
considered to be an antipattern).
|
|
21
|
+
|
|
22
|
+
Mocktail was written to promote test-driven development that specifies
|
|
23
|
+
thoughtfully-designed classes that interact with other classes, but partial
|
|
24
|
+
mocks actually detract from that purpose in practice. As a result Mocktail,
|
|
25
|
+
doesn't offer a way to create partial mocks.
|
|
26
|
+
|
|
27
|
+
If you find yourself wanting to reach for a partial mock, we'd encourage you to
|
|
28
|
+
first take it as potential design feedback that a dependency is perhaps too big
|
|
29
|
+
and its contract with [subject](../support/glossary.md#subject-under-test) too
|
|
30
|
+
porous.
|
|
31
|
+
|
|
32
|
+
**If you've heard enough, you can go back and consider [non-TDD use cases for Mocktail](../other_uses.md).**
|
|
33
|
+
|
|
34
|
+
**Or if you're finally ready to walk the golden path, you can revisit [Mocktail as a TDD tool](../tdd.md).**
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Recording interactions on real methods
|
|
2
|
+
|
|
3
|
+
Okay, so imagine this situation: you have a unit test that, among other things,
|
|
4
|
+
calls some API that doesn't return a meaningful
|
|
5
|
+
[value](../support/glossary.md#value) and you want to make sure that you've
|
|
6
|
+
covered it. Since your test can't observe the result of the call, you want to
|
|
7
|
+
make sure it happened
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
def roll_dice(dice)
|
|
11
|
+
@mouth.blow!(dice)
|
|
12
|
+
dice.roll
|
|
13
|
+
end
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
If a developer were simultaneously very eager to achieve an extreme amount of
|
|
17
|
+
code coverage and also superstitious that _actually_ blowing on the dice is very
|
|
18
|
+
important, they could look at that `@mouth.blow!` invocation and ask "how can I
|
|
19
|
+
verify that call happened while still calling through to its real
|
|
20
|
+
implementation?"
|
|
21
|
+
|
|
22
|
+
Well, the answer if you're using Mocktail is, "sorry, you can't."
|
|
23
|
+
|
|
24
|
+
The primary reason this isn't supported is because Mocktail exists to facilitate
|
|
25
|
+
test-driven development of code that's fully isolated from its
|
|
26
|
+
[dependencies](../support/glossary.md#dependency) in order to design
|
|
27
|
+
well-considered interactions, and calling through to a dependency's actual
|
|
28
|
+
implementation naturally violates that isolation. Additionally, for any reason a
|
|
29
|
+
tester might wish to call through to the actual implementation _other than
|
|
30
|
+
superstition_, it stands to reason that at most one of the following is
|
|
31
|
+
possible:
|
|
32
|
+
|
|
33
|
+
1. An integrated test that doesn't use mocks could indirectly observe a
|
|
34
|
+
dependency's side effect as evidence that a call occurred and therefore a
|
|
35
|
+
mocking library isn't required to [proxy it](../support/glossary.md#proxy), _or_
|
|
36
|
+
2. It's not possible for the side effect of the call to be observed by either
|
|
37
|
+
the test or its [subject](../support/glossary.md#subject-under-test) and
|
|
38
|
+
therefore it's safe to achieve proper isolation by replacing the real
|
|
39
|
+
implementation with a fake one
|
|
40
|
+
|
|
41
|
+
But both conditions can't be true. As a result, the urge to add a test
|
|
42
|
+
assertion that a call occurred in a particular way may represent an [unnecessary
|
|
43
|
+
over-specification](https://blog.testdouble.com/posts/2020-02-25-necessary-and-sufficient/).
|
|
44
|
+
|
|
45
|
+
That said, if you're unswayed and still set on having this functionality, you
|
|
46
|
+
can find it in the [rr
|
|
47
|
+
gem](https://github.com/rr/rr/blob/master/doc/03_api_overview.md#mockproxy)'s
|
|
48
|
+
`mock.proxy` API.
|
|
49
|
+
|
|
50
|
+
**If you've heard enough, you can go back and consider [non-TDD use cases for Mocktail](../other_uses.md).**
|
|
51
|
+
|
|
52
|
+
**Or if you're finally ready to walk the golden path, you can revisit [Mocktail as a TDD tool](../tdd.md).**
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
data/docs/img/spacer.png
ADDED
|
Binary file
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Installing Mocktail with Sorbet
|
|
2
|
+
|
|
3
|
+
Alright, when you add a type checker to isolated unit tests, you're really
|
|
4
|
+
cooking with gas. That's because tests that mock out
|
|
5
|
+
[dependencies](support/glossary.md#dependency) to design the internal APIs
|
|
6
|
+
a [subject](support/glossary.md#subject-under-test) will invoke to divide
|
|
7
|
+
its job into smaller, focused sub-tasks is really about defining the _contracts_
|
|
8
|
+
between units, and those contracts are most often defined by method signatures:
|
|
9
|
+
parameters and return values.
|
|
10
|
+
|
|
11
|
+
And what are type checkers most concerned with? Oh yeah, parameters and return
|
|
12
|
+
values! Instead of potentially brittle tests that may keep passing even when the
|
|
13
|
+
contract changes, tests that are strictly type-checked won't lie to you.
|
|
14
|
+
Additionally, editors that support [red squiggles and
|
|
15
|
+
autocorrections](https://sorbet.org/docs/vscode) for Sorbet can enable
|
|
16
|
+
test-driven workflows you won't see elsewhere in Ruby: invoke a method as you
|
|
17
|
+
wish it existed in your test, and a quickfix to update the method's definition
|
|
18
|
+
is often just a keyboard shortcut away. The power you'll feel is intoxicating!
|
|
19
|
+
|
|
20
|
+
All that is why topping your Mocktail with Sorbet is a match made in zero-proof
|
|
21
|
+
testing heaven. All you need to do to install it is add this to your Gemfile:
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
gem "mocktail", group: :test, require: "mocktail/sorbet"
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Note the goofy `require: "mocktail/sorbet"` option above. We don't see that a
|
|
28
|
+
lot in Ruby, because most gems have a single primary entrypoint to require, and
|
|
29
|
+
it's almost always the same name as the gem. However, so that Mocktail can be
|
|
30
|
+
used with and without [Sorbet's runtime type
|
|
31
|
+
safety](https://sorbet.org/docs/runtime) enabled, it actually ships with two
|
|
32
|
+
distributions:
|
|
33
|
+
|
|
34
|
+
* `require "mocktail"` will load a version of the library that has [effectively
|
|
35
|
+
erased](https://github.com/kddnewton/sorbet-eraser) all of its Sorbet types, so
|
|
36
|
+
you'll never see a runtime type check
|
|
37
|
+
* `require "mocktail/sorbet"` will load the library with all of its Sorbet
|
|
38
|
+
signatures intact, so that Sorbet users can benefit from both static and runtime
|
|
39
|
+
checks
|
|
40
|
+
|
|
41
|
+
Be warned: it's all Sorbet or no Sorbet. **Do not cross the streams and require
|
|
42
|
+
both of them** unless you like constant redefinition warnings. Mocktail may
|
|
43
|
+
throw an error to try to fail fast if it detects both are required, since the
|
|
44
|
+
resulting behavior is undefined.
|
|
45
|
+
|
|
46
|
+
As for Sorbet, that's really all you need to know! Everything else is just the
|
|
47
|
+
usual stack of tools like the [srb CLI](https://sorbet.org/docs/cli),
|
|
48
|
+
[tapioca](https://github.com/Shopify/tapioca), and
|
|
49
|
+
[spoom](https://github.com/Shopify/spoom), plus whatever editor integration you
|
|
50
|
+
prefer.
|
|
51
|
+
|
|
52
|
+
## Type checking your tests
|
|
53
|
+
|
|
54
|
+
If you want to benefit from type checking in your tests, it means you'll have to
|
|
55
|
+
enable type checking in your tests. Code examples you find in the rest of
|
|
56
|
+
Mocktail's documentation won't show this, but a test listing might look like
|
|
57
|
+
this to get all the benefits that Sorbet-flavored mocktail has to offer
|
|
58
|
+
(Minitest below):
|
|
59
|
+
|
|
60
|
+
```ruby
|
|
61
|
+
# typed: strict
|
|
62
|
+
|
|
63
|
+
require "test_helper"
|
|
64
|
+
|
|
65
|
+
class StuffDoerTest < Minitest::Test
|
|
66
|
+
extend T::Sig
|
|
67
|
+
|
|
68
|
+
sig { params(name: String).void }
|
|
69
|
+
def initialize(name)
|
|
70
|
+
super
|
|
71
|
+
@loads_stuff = T.let(Mocktail.of_next(LoadsStuff), LoadsStuff)
|
|
72
|
+
|
|
73
|
+
@subject = T.let(StuffDoer.new, StuffDoer)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
sig { void }
|
|
77
|
+
def test_doing_stuff
|
|
78
|
+
# tests go here
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
It probably won't be a surprise to have to add `sig` to each test case when
|
|
84
|
+
using `typed: strict`, but the `T.let` in the initializer is a definite bummer.
|
|
85
|
+
This could probably be worked around with memoized helpers, but that's outside
|
|
86
|
+
the scope of Mocktail for now, especially considering [Sorbet may obviate the
|
|
87
|
+
need for explicit declaration of certain instance variables in the
|
|
88
|
+
future](https://github.com/sorbet/sorbet/issues/3513).
|
|
89
|
+
|
|
90
|
+
## Configuring your test runner
|
|
91
|
+
|
|
92
|
+
But before we dive in and start writing tests, we need to configure Mocktail
|
|
93
|
+
with your preferred test runner.
|
|
94
|
+
|
|
95
|
+
**If you were glad to see the code example above was in Minitest, [keep the party going](configuring_minitest.md).**
|
|
96
|
+
|
|
97
|
+
**If you're annoyed it wasn't in RSpec, [switch trains to go to RSpec-land](configuring_rspec.md).**
|
|
98
|
+
|
|
99
|
+
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Installing Mocktail
|
|
2
|
+
|
|
3
|
+
So, you want to get started with Mocktail Classic™, do you? Here's how to get it
|
|
4
|
+
installed! (And if you're having second thoughts on passing on [the Sorbet
|
|
5
|
+
edition](installation_sorbet.md), there's still time!)
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
First thing's first, add this to your Gemfile:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
gem "mocktail", group: :test, require: "mocktail"
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
(That redundant `require` option is just a garnish and won't cause any harm;
|
|
16
|
+
it's there to signal to future readers that you're _not_ using
|
|
17
|
+
`mocktail/sorbet`.)
|
|
18
|
+
|
|
19
|
+
Once installed, you can require Mocktail like you might expect:
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
require "mocktail"
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Next step: it's time to configure Mocktail with your test runner!
|
|
26
|
+
|
|
27
|
+
**If you like to keep things classy, maybe [you use Minitest](configuring_minitest.md).**
|
|
28
|
+
|
|
29
|
+
**Or maybe you'd rather express your intent [to use RSpec](configuring_rspec.md).**
|
data/docs/other_uses.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Other uses for Mocktail
|
|
2
|
+
|
|
3
|
+
If you're looking to accomplish something with Mocktail that doesn't involve
|
|
4
|
+
test-driven development, let's get real. There's a very high likelihood that
|
|
5
|
+
what you're looking to do is either:
|
|
6
|
+
|
|
7
|
+
1. Not the best tool for the job
|
|
8
|
+
2. Not a job worth doing
|
|
9
|
+
|
|
10
|
+
But hey, could be wrong. The present author once did a [talk about all the ways
|
|
11
|
+
people abuse mocking
|
|
12
|
+
libraries](https://blog.testdouble.com/talks/2018-03-06-please-dont-mock-me/)
|
|
13
|
+
and undermine the value of their tests, so there is a track record of bias here.
|
|
14
|
+
|
|
15
|
+
By popular demand, here are some ways you might be thinking about using
|
|
16
|
+
Mocktail:
|
|
17
|
+
|
|
18
|
+
**Mocking out the system clock in a vain attempt to [master space and time](faq/mocking_time.md).**
|
|
19
|
+
|
|
20
|
+
**Mocking an HTTP API by faking out [Ruby's built-in networking](faq/mocking_http.md).**
|
|
21
|
+
|
|
22
|
+
**Mocking out _just one_ method on an [otherwise real object](faq/partial_mocks.md).**
|
|
23
|
+
|
|
24
|
+
**Recording method invocations while [calling through to their real implementation](faq/verifying_real_interactions.md).**
|
|
25
|
+
|
|
26
|
+
**Using Mocktail to fix an existing test that's [failing in a gnarly way you don't understand](faq/existing_tests.md).**
|
|
27
|
+
|
|
28
|
+
**Mocking out a method on the subject under test, AKA [the thing you're testing itself](faq/mocking_the_subject.md).**
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
**Once you've seen enough, you can take a second look at [Mocktail as a TDD tool](tdd.md).**
|