mocktail 1.2.3 → 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 +6 -5
- data/.gitignore +3 -0
- data/.standard.yml +8 -0
- data/CHANGELOG.md +19 -0
- data/Gemfile +8 -2
- data/Gemfile.lock +105 -38
- data/README.md +20 -922
- data/Rakefile +0 -1
- data/bin/console +1 -2
- data/bin/tapioca +29 -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/collects_calls.rb +2 -0
- data/lib/mocktail/debug.rb +16 -12
- data/lib/mocktail/dsl.rb +2 -0
- data/lib/mocktail/errors.rb +2 -0
- data/lib/mocktail/explains_nils.rb +2 -0
- data/lib/mocktail/explains_thing.rb +7 -4
- data/lib/mocktail/grabs_original_method_parameters.rb +30 -0
- data/lib/mocktail/handles_dry_call/fulfills_stubbing/describes_unsatisfied_stubbing.rb +3 -1
- data/lib/mocktail/handles_dry_call/fulfills_stubbing/finds_satisfaction.rb +5 -1
- data/lib/mocktail/handles_dry_call/fulfills_stubbing.rb +2 -0
- data/lib/mocktail/handles_dry_call/logs_call.rb +2 -0
- data/lib/mocktail/handles_dry_call/validates_arguments.rb +6 -4
- data/lib/mocktail/handles_dry_call.rb +2 -0
- data/lib/mocktail/handles_dry_new_call.rb +2 -0
- data/lib/mocktail/imitates_type/ensures_imitation_support.rb +2 -0
- data/lib/mocktail/imitates_type/makes_double/declares_dry_class/reconstructs_call.rb +4 -1
- data/lib/mocktail/imitates_type/makes_double/declares_dry_class.rb +32 -20
- data/lib/mocktail/imitates_type/makes_double/gathers_fakeable_instance_methods.rb +2 -0
- data/lib/mocktail/imitates_type/makes_double.rb +3 -0
- data/lib/mocktail/imitates_type.rb +3 -1
- data/lib/mocktail/initialize_based_on_type_system_mode_switching.rb +9 -0
- data/lib/mocktail/initializes_mocktail.rb +5 -0
- data/lib/mocktail/matcher_presentation.rb +4 -2
- data/lib/mocktail/matchers/any.rb +4 -3
- data/lib/mocktail/matchers/base.rb +10 -2
- data/lib/mocktail/matchers/captor.rb +9 -0
- data/lib/mocktail/matchers/includes.rb +2 -0
- data/lib/mocktail/matchers/includes_hash.rb +9 -0
- data/lib/mocktail/matchers/includes_key.rb +9 -0
- data/lib/mocktail/matchers/includes_string.rb +9 -0
- data/lib/mocktail/matchers/is_a.rb +2 -0
- data/lib/mocktail/matchers/matches.rb +2 -0
- data/lib/mocktail/matchers/not.rb +2 -0
- data/lib/mocktail/matchers/numeric.rb +5 -4
- data/lib/mocktail/matchers/that.rb +2 -0
- data/lib/mocktail/matchers.rb +3 -0
- data/lib/mocktail/raises_neato_no_method_error.rb +2 -0
- data/lib/mocktail/records_demonstration.rb +2 -0
- data/lib/mocktail/registers_matcher.rb +8 -3
- data/lib/mocktail/registers_stubbing.rb +2 -0
- data/lib/mocktail/replaces_next.rb +7 -1
- data/lib/mocktail/replaces_type/redefines_new.rb +3 -1
- data/lib/mocktail/replaces_type/redefines_singleton_methods.rb +14 -2
- data/lib/mocktail/replaces_type/runs_sorbet_sig_blocks_before_replacement.rb +37 -0
- data/lib/mocktail/replaces_type.rb +6 -0
- data/lib/mocktail/resets_state.rb +2 -0
- data/lib/mocktail/share/bind.rb +7 -5
- data/lib/mocktail/share/cleans_backtrace.rb +3 -5
- data/lib/mocktail/share/creates_identifier.rb +16 -9
- data/lib/mocktail/share/determines_matching_calls.rb +4 -2
- data/lib/mocktail/share/stringifies_call.rb +6 -3
- data/lib/mocktail/share/stringifies_method_name.rb +3 -1
- data/lib/mocktail/simulates_argument_error/reconciles_args_with_params.rb +2 -0
- data/lib/mocktail/simulates_argument_error/recreates_message.rb +2 -0
- data/lib/mocktail/simulates_argument_error/transforms_params.rb +16 -9
- data/lib/mocktail/simulates_argument_error.rb +2 -0
- data/lib/mocktail/sorbet/mocktail/collects_calls.rb +18 -0
- data/lib/mocktail/sorbet/mocktail/debug.rb +55 -0
- data/lib/mocktail/sorbet/mocktail/dsl.rb +46 -0
- data/lib/mocktail/sorbet/mocktail/errors.rb +19 -0
- data/lib/mocktail/sorbet/mocktail/explains_nils.rb +41 -0
- data/lib/mocktail/sorbet/mocktail/explains_thing.rb +137 -0
- data/lib/mocktail/sorbet/mocktail/grabs_original_method_parameters.rb +33 -0
- data/lib/mocktail/sorbet/mocktail/handles_dry_call/fulfills_stubbing/describes_unsatisfied_stubbing.rb +27 -0
- data/lib/mocktail/sorbet/mocktail/handles_dry_call/fulfills_stubbing/finds_satisfaction.rb +24 -0
- data/lib/mocktail/sorbet/mocktail/handles_dry_call/fulfills_stubbing.rb +45 -0
- data/lib/mocktail/sorbet/mocktail/handles_dry_call/logs_call.rb +12 -0
- data/lib/mocktail/sorbet/mocktail/handles_dry_call/validates_arguments.rb +45 -0
- data/lib/mocktail/sorbet/mocktail/handles_dry_call.rb +25 -0
- data/lib/mocktail/sorbet/mocktail/handles_dry_new_call.rb +42 -0
- data/lib/mocktail/sorbet/mocktail/imitates_type/ensures_imitation_support.rb +16 -0
- data/lib/mocktail/sorbet/mocktail/imitates_type/makes_double/declares_dry_class/reconstructs_call.rb +73 -0
- data/lib/mocktail/sorbet/mocktail/imitates_type/makes_double/declares_dry_class.rb +136 -0
- data/lib/mocktail/sorbet/mocktail/imitates_type/makes_double/gathers_fakeable_instance_methods.rb +28 -0
- data/lib/mocktail/sorbet/mocktail/imitates_type/makes_double.rb +29 -0
- data/lib/mocktail/sorbet/mocktail/imitates_type.rb +29 -0
- data/lib/mocktail/sorbet/mocktail/initialize_based_on_type_system_mode_switching.rb +11 -0
- data/lib/mocktail/sorbet/mocktail/initializes_mocktail.rb +25 -0
- data/lib/mocktail/sorbet/mocktail/matcher_presentation.rb +21 -0
- data/lib/mocktail/sorbet/mocktail/matchers/any.rb +27 -0
- data/lib/mocktail/sorbet/mocktail/matchers/base.rb +39 -0
- data/lib/mocktail/sorbet/mocktail/matchers/captor.rb +76 -0
- data/lib/mocktail/sorbet/mocktail/matchers/includes.rb +32 -0
- data/lib/mocktail/sorbet/mocktail/matchers/includes_hash.rb +12 -0
- data/lib/mocktail/sorbet/mocktail/matchers/includes_key.rb +12 -0
- data/lib/mocktail/sorbet/mocktail/matchers/includes_string.rb +12 -0
- data/lib/mocktail/sorbet/mocktail/matchers/is_a.rb +17 -0
- data/lib/mocktail/sorbet/mocktail/matchers/matches.rb +19 -0
- data/lib/mocktail/sorbet/mocktail/matchers/not.rb +17 -0
- data/lib/mocktail/sorbet/mocktail/matchers/numeric.rb +27 -0
- data/lib/mocktail/sorbet/mocktail/matchers/that.rb +32 -0
- data/lib/mocktail/sorbet/mocktail/matchers.rb +19 -0
- data/lib/mocktail/sorbet/mocktail/raises_neato_no_method_error.rb +93 -0
- data/lib/mocktail/sorbet/mocktail/records_demonstration.rb +43 -0
- data/lib/mocktail/sorbet/mocktail/registers_matcher.rb +65 -0
- data/lib/mocktail/sorbet/mocktail/registers_stubbing.rb +31 -0
- data/lib/mocktail/sorbet/mocktail/replaces_next.rb +55 -0
- data/lib/mocktail/sorbet/mocktail/replaces_type/redefines_new.rb +32 -0
- data/lib/mocktail/sorbet/mocktail/replaces_type/redefines_singleton_methods.rb +80 -0
- data/lib/mocktail/sorbet/mocktail/replaces_type/runs_sorbet_sig_blocks_before_replacement.rb +39 -0
- data/lib/mocktail/sorbet/mocktail/replaces_type.rb +36 -0
- data/lib/mocktail/sorbet/mocktail/resets_state.rb +14 -0
- data/lib/mocktail/sorbet/mocktail/share/bind.rb +18 -0
- data/lib/mocktail/sorbet/mocktail/share/cleans_backtrace.rb +22 -0
- data/lib/mocktail/sorbet/mocktail/share/creates_identifier.rb +39 -0
- data/lib/mocktail/sorbet/mocktail/share/determines_matching_calls.rb +72 -0
- data/lib/mocktail/sorbet/mocktail/share/stringifies_call.rb +84 -0
- data/lib/mocktail/sorbet/mocktail/share/stringifies_method_name.rb +16 -0
- data/lib/mocktail/sorbet/mocktail/simulates_argument_error/reconciles_args_with_params.rb +27 -0
- data/lib/mocktail/sorbet/mocktail/simulates_argument_error/recreates_message.rb +34 -0
- data/lib/mocktail/sorbet/mocktail/simulates_argument_error/transforms_params.rb +58 -0
- data/lib/mocktail/sorbet/mocktail/simulates_argument_error.rb +36 -0
- data/lib/mocktail/sorbet/mocktail/sorbet.rb +3 -0
- data/lib/mocktail/sorbet/mocktail/stringifies_method_signature.rb +53 -0
- data/lib/mocktail/sorbet/mocktail/typed.rb +5 -0
- data/lib/mocktail/sorbet/mocktail/value/cabinet.rb +91 -0
- data/lib/mocktail/sorbet/mocktail/value/call.rb +51 -0
- data/lib/mocktail/sorbet/mocktail/value/demo_config.rb +10 -0
- data/lib/mocktail/sorbet/mocktail/value/double.rb +10 -0
- data/lib/mocktail/sorbet/mocktail/value/double_data.rb +15 -0
- data/lib/mocktail/sorbet/mocktail/value/explanation.rb +68 -0
- data/lib/mocktail/sorbet/mocktail/value/explanation_data.rb +19 -0
- data/lib/mocktail/sorbet/mocktail/value/fake_method_data.rb +11 -0
- data/lib/mocktail/sorbet/mocktail/value/matcher_registry.rb +27 -0
- data/lib/mocktail/sorbet/mocktail/value/no_explanation_data.rb +20 -0
- data/lib/mocktail/sorbet/mocktail/value/signature.rb +35 -0
- data/lib/mocktail/sorbet/mocktail/value/stubbing.rb +26 -0
- data/lib/mocktail/sorbet/mocktail/value/top_shelf.rb +79 -0
- data/lib/mocktail/sorbet/mocktail/value/type_replacement.rb +11 -0
- data/lib/mocktail/sorbet/mocktail/value/type_replacement_data.rb +19 -0
- data/lib/mocktail/sorbet/mocktail/value/unsatisfying_call.rb +9 -0
- data/lib/mocktail/sorbet/mocktail/value/unsatisfying_call_explanation.rb +24 -0
- data/lib/mocktail/sorbet/mocktail/value.rb +19 -0
- data/lib/mocktail/sorbet/mocktail/verifies_call/finds_verifiable_calls.rb +21 -0
- data/lib/mocktail/sorbet/mocktail/verifies_call/raises_verification_error/gathers_calls_of_method.rb +15 -0
- data/lib/mocktail/sorbet/mocktail/verifies_call/raises_verification_error.rb +74 -0
- data/lib/mocktail/sorbet/mocktail/verifies_call.rb +37 -0
- data/lib/mocktail/sorbet/mocktail/version.rb +12 -0
- data/lib/mocktail/sorbet/mocktail.rb +154 -0
- data/lib/mocktail/sorbet.rb +1 -0
- data/lib/mocktail/stringifies_method_signature.rb +2 -0
- data/lib/mocktail/typed.rb +3 -0
- data/lib/mocktail/value/cabinet.rb +8 -1
- data/lib/mocktail/value/call.rb +44 -12
- data/lib/mocktail/value/demo_config.rb +6 -7
- data/lib/mocktail/value/double.rb +6 -7
- data/lib/mocktail/value/double_data.rb +11 -7
- data/lib/mocktail/value/explanation.rb +28 -3
- data/lib/mocktail/value/explanation_data.rb +14 -0
- data/lib/mocktail/value/fake_method_data.rb +7 -6
- data/lib/mocktail/value/matcher_registry.rb +2 -0
- data/lib/mocktail/value/no_explanation_data.rb +16 -0
- data/lib/mocktail/value/signature.rb +19 -27
- data/lib/mocktail/value/stubbing.rb +11 -12
- data/lib/mocktail/value/top_shelf.rb +5 -0
- data/lib/mocktail/value/type_replacement.rb +7 -8
- data/lib/mocktail/value/type_replacement_data.rb +10 -7
- data/lib/mocktail/value/unsatisfying_call.rb +5 -6
- data/lib/mocktail/value/unsatisfying_call_explanation.rb +18 -0
- data/lib/mocktail/value.rb +5 -2
- data/lib/mocktail/verifies_call/finds_verifiable_calls.rb +2 -0
- data/lib/mocktail/verifies_call/raises_verification_error/gathers_calls_of_method.rb +2 -0
- data/lib/mocktail/verifies_call/raises_verification_error.rb +2 -0
- data/lib/mocktail/verifies_call.rb +3 -0
- data/lib/mocktail/version.rb +8 -1
- data/lib/mocktail.rb +46 -5
- data/mocktail.gemspec +8 -4
- data/rbi/mocktail-pregenerated.rbi +1923 -0
- data/rbi/mocktail.rbi +77 -0
- data/rbi/sorbet-runtime.rbi +29 -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/spoom_report.html +1248 -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 +392 -9
data/Rakefile
CHANGED
data/bin/console
CHANGED
data/bin/tapioca
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
#
|
|
5
|
+
# This file was generated by Bundler.
|
|
6
|
+
#
|
|
7
|
+
# The application 'tapioca' is installed as part of a gem, and
|
|
8
|
+
# this file is here to facilitate running it.
|
|
9
|
+
#
|
|
10
|
+
|
|
11
|
+
require "pathname"
|
|
12
|
+
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
|
|
13
|
+
Pathname.new(__FILE__).realpath)
|
|
14
|
+
|
|
15
|
+
bundle_binstub = File.expand_path("../bundle", __FILE__)
|
|
16
|
+
|
|
17
|
+
if File.file?(bundle_binstub)
|
|
18
|
+
if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
|
|
19
|
+
load(bundle_binstub)
|
|
20
|
+
else
|
|
21
|
+
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
|
|
22
|
+
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
require "rubygems"
|
|
27
|
+
require "bundler/setup"
|
|
28
|
+
|
|
29
|
+
load Gem.bin_path("tapioca", "tapioca")
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
## Code of Conduct
|
|
2
|
+
|
|
3
|
+
This project follows Test Double's [code of
|
|
4
|
+
conduct](https://testdouble.com/code-of-conduct) for all community interactions,
|
|
5
|
+
including (but not limited to) one-on-one communications, public posts/comments,
|
|
6
|
+
code reviews, pull requests, and GitHub issues. If violations occur, Test Double
|
|
7
|
+
will take any action they deem appropriate for the infraction, up to and
|
|
8
|
+
including blocking a user from the organization's repositories.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
## Configuring Minitest
|
|
2
|
+
|
|
3
|
+
If you're using Minitest, you'll want to plunk this into a test helper:
|
|
4
|
+
|
|
5
|
+
```ruby
|
|
6
|
+
class Minitest::Test
|
|
7
|
+
include Mocktail::DSL
|
|
8
|
+
|
|
9
|
+
def teardown
|
|
10
|
+
super
|
|
11
|
+
Mocktail.reset
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Finally, the real work of faking things can begin.
|
|
17
|
+
|
|
18
|
+
**Incorporate Mocktail into your [test-driven development workflow](tdd.md).**
|
|
19
|
+
|
|
20
|
+
**Leverage Mocktail's metaprogramming essence for [other testing utilities](other_uses.md).**
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
## Configuring RSpec
|
|
2
|
+
|
|
3
|
+
If you find yourself in an RSpec establishment, your spec helper just needs
|
|
4
|
+
this:
|
|
5
|
+
|
|
6
|
+
```ruby
|
|
7
|
+
RSpec.configure do |config|
|
|
8
|
+
config.include Mocktail::DSL
|
|
9
|
+
|
|
10
|
+
config.after(:example) { Mocktail.reset }
|
|
11
|
+
end
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
But configuration will only get you so far. Once your prep is complete, it's
|
|
15
|
+
time to roll up your sleeves and get to work.
|
|
16
|
+
|
|
17
|
+
**Incorporate Mocktail into your [test-driven development workflow](tdd.md).**
|
|
18
|
+
|
|
19
|
+
**Leverage Mocktail's metaprogramming essence for [other testing utilities](other_uses.md).**
|
|
@@ -0,0 +1,594 @@
|
|
|
1
|
+
# An example test with Mocktail
|
|
2
|
+
|
|
3
|
+
As discussed elsewhere, Mocktail's intended use is to facilitate [test-driven development](tdd.md) of [isolated unit tests](support/glossary.md#isolated-unit-testing) by making it trivially easy to achieve isolation
|
|
4
|
+
between a [subject under test](support/glossary.md#subject-under-test) and its [dependencies](support/glossary.md#dependency).
|
|
5
|
+
|
|
6
|
+
## A small example problem: slicing fruit
|
|
7
|
+
|
|
8
|
+
In order to walk through an example test, we need to dream up a class and method
|
|
9
|
+
to implement that is simple enough to condense into example code but realistic
|
|
10
|
+
enough to show off the workflow Mocktail was designed to facilitate. And to do
|
|
11
|
+
that, it's useful context to start with the end in mind. In a codebase that
|
|
12
|
+
was primarily developed with outside-in test-driven development, it's typical
|
|
13
|
+
to see the following:
|
|
14
|
+
|
|
15
|
+
* The average [delegator](support/glossary.md#delegator) has 3 dependencies that
|
|
16
|
+
implement logic
|
|
17
|
+
* Delegators are typically the only units of code whose tests use [test
|
|
18
|
+
doubles](support/glossary.md#test-double), and they tend to simplify their usage
|
|
19
|
+
by _only_ delegating as opposed to performing any logic, thereby avoiding [mixed
|
|
20
|
+
levels of abstraction](support/glossary.md#level-of-abstraction)
|
|
21
|
+
* Because most real-world applications don't exist in a vacuum, a given
|
|
22
|
+
entrypoint for a unit of work will typically shake out into a tree of
|
|
23
|
+
dependencies that resembles an [extract, transform,
|
|
24
|
+
load](https://en.wikipedia.org/wiki/Extract,_transform,_load) process
|
|
25
|
+
|
|
26
|
+
With that in mind, let's write a test of a delegator that assists in the prep
|
|
27
|
+
work before a bar opens: getting fruits from the fridge, slicing them, and
|
|
28
|
+
storing them in a prep station.
|
|
29
|
+
|
|
30
|
+
One way to identify the jobs to be done is to translate a narrative summary of a feature into a list of discrete responsibilities or activities. A class that
|
|
31
|
+
manages fruit prep would:
|
|
32
|
+
|
|
33
|
+
1. Fetch fruits from the stock room
|
|
34
|
+
2. Slice each fruit
|
|
35
|
+
3. Store each fruit in a prep station
|
|
36
|
+
|
|
37
|
+
If we were practicing traditional test-driven development, we might start with a
|
|
38
|
+
test that jumps into implementing one of these activities and then continue to
|
|
39
|
+
add test cases until all the responsibilities are covered. If the subject were
|
|
40
|
+
to get too complex as we saddled it with more tasks, we might pause to extract
|
|
41
|
+
those responsibilities into single-responsibility sub-units, which would often
|
|
42
|
+
necessitate moving test code around or otherwise testing those sub-units only
|
|
43
|
+
indirectly through the original subject.
|
|
44
|
+
|
|
45
|
+
Outside-in test driven development flips the order of operations: once we know
|
|
46
|
+
what the subject needs to do, we imagine new subordinate units to delegate the
|
|
47
|
+
work to and use our test to specify the relationship between the subject and its
|
|
48
|
+
dependencies. We would repeat this process until we were left with a number of
|
|
49
|
+
irreducibly simple, single-responsibility units of domain logic that could
|
|
50
|
+
largely be implemented via straightforward tests (without mocks), often as pure
|
|
51
|
+
functions. This way, simplicity is baked into the process instead of an
|
|
52
|
+
afterthought—where classical TDD often succeeds or fails based on the
|
|
53
|
+
practitioner's patience and discipline to remember to refactor.
|
|
54
|
+
|
|
55
|
+
To make this a little clearer, let's illustrate the ordered list above into imagined dependencies that a delegator might need to accomplish the work:
|
|
56
|
+
|
|
57
|
+
<p align="center" width="100%">
|
|
58
|
+
<img src="img/example_test.png" width="480" alt="an example tree of dependencies that do the above work">
|
|
59
|
+
</p>
|
|
60
|
+
|
|
61
|
+
The diagram matches the assumptions of a typical delegator described above: a
|
|
62
|
+
subject with 3 dependencies, that does nothing but delegate its responsibility,
|
|
63
|
+
and a tree that resembles an extract-transform-load process: something to fetch
|
|
64
|
+
data based on a request, something to perform logic, and something to put the
|
|
65
|
+
result somewhere.
|
|
66
|
+
|
|
67
|
+
To visualize what we mean by ETL:
|
|
68
|
+
|
|
69
|
+
<p align="center" width="100%">
|
|
70
|
+
<img src="img/extract_transform_load.png" width="480" alt="illustrating the tree as an ETL process">
|
|
71
|
+
</p>
|
|
72
|
+
|
|
73
|
+
If you're new to isolated TDD, the focus on these heuristics may appear like
|
|
74
|
+
arbitrary strictures but in reality are just observations of patterns that
|
|
75
|
+
emerge over time when your practice and tools pushes you to the extreme end of
|
|
76
|
+
breaking down problems into small, single-purpose functions and methods. The
|
|
77
|
+
reason ETL emerges as a common pattern is because a huge proportion of software
|
|
78
|
+
features entails getting data via I/O, doing something interesting with that
|
|
79
|
+
data, and then putting that data somewhere.
|
|
80
|
+
|
|
81
|
+
## Initial test setup
|
|
82
|
+
|
|
83
|
+
Okay, so with that gameplan, let's start writing a test. We will use [dependency
|
|
84
|
+
inception](tdd/poro/dependency_inception.md) to get our dependencies in the
|
|
85
|
+
hands of our subject using [Mocktail.of_next](support/api.md#mocktailof_next):
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
require "test_helper"
|
|
89
|
+
|
|
90
|
+
class PrepsFruitsTest < Minitest::Test
|
|
91
|
+
def setup
|
|
92
|
+
@fetches_fruits = Mocktail.of_next(FetchesFruits)
|
|
93
|
+
@slices_fruit = Mocktail.of_next(SlicesFruit)
|
|
94
|
+
@stores_fruit = Mocktail.of_next(StoresFruit)
|
|
95
|
+
|
|
96
|
+
@subject = PrepsFruits.new
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def test_prep
|
|
100
|
+
# TODO
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
If we run this test, of course it will fail, because none of the things
|
|
106
|
+
referenced in `setup` exist yet! That said, it's important to get in the habit
|
|
107
|
+
of running tests early and often to make sure that their message and status
|
|
108
|
+
matches our expectations.
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
PrepsFruitsTest#test_prep:
|
|
112
|
+
NameError: uninitialized constant PrepsFruitsTest::FetchesFruits
|
|
113
|
+
example.rb:5:in `setup'
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
As they say, the goal of each action in TDD is to "either make the test pass or
|
|
117
|
+
change the message", so let's rapidly iterate to overcome each of these errors
|
|
118
|
+
until the empty `test_prep` method exits cleanly:
|
|
119
|
+
|
|
120
|
+
```ruby
|
|
121
|
+
class FetchesFruits
|
|
122
|
+
end
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Fails with `uninitialized constant PrepsFruitsTest::SlicesFruit` until we:
|
|
126
|
+
|
|
127
|
+
```ruby
|
|
128
|
+
class SlicesFruit
|
|
129
|
+
end #=> message is now: `uninitialized constant PrepsFruitsTest::StoresFruit`
|
|
130
|
+
|
|
131
|
+
class StoresFruit
|
|
132
|
+
end #=> message is now: `uninitialized constant PrepsFruitsTest::PrepsFruits`
|
|
133
|
+
|
|
134
|
+
class PrepsFruits
|
|
135
|
+
end #=> …success!
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Creating these 4 classes (the subject and its three dependencies) is enough to
|
|
139
|
+
get the test to exit cleanly for now.
|
|
140
|
+
|
|
141
|
+
## Arrange: creating values and configuring stubbings
|
|
142
|
+
|
|
143
|
+
Each test has three phases: [arrange, act, and
|
|
144
|
+
assert](support/glossary.md#arrange-act-assert), and it usually makes the most
|
|
145
|
+
sense to start with test setup. Here we'll need to create the
|
|
146
|
+
[values](support/glossary.md#value) that our subject will be acting on as well
|
|
147
|
+
as the [stubbing](support/glossary.md#stub) configurations for each of its
|
|
148
|
+
mocked dependencies.
|
|
149
|
+
|
|
150
|
+
Like before, we'll rapidly iterate by adding a stubbing, running the test,
|
|
151
|
+
making a change, and changing the message. After you get in the flow, driving
|
|
152
|
+
the design of a system with outside-in test-driven development starts to feel
|
|
153
|
+
like [paint by number](https://en.wikipedia.org/wiki/Paint_by_number).
|
|
154
|
+
|
|
155
|
+
We'll start by stubbing the first interaction to fetch the fruit. This will
|
|
156
|
+
require us to specify some value objects (fruits) we haven't created yet.
|
|
157
|
+
|
|
158
|
+
```ruby
|
|
159
|
+
def test_prep
|
|
160
|
+
fruits = [Lime.new, Mango.new, Pineapple.new]
|
|
161
|
+
stubs { @fetches_fruits.fetch([:lime, :mango, :pineapple]) }.with { fruits }
|
|
162
|
+
# …TODO
|
|
163
|
+
end
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
The first error is `uninitialized constant PrepsFruitsTest::Lime`, so let's
|
|
167
|
+
start fixing:
|
|
168
|
+
|
|
169
|
+
```ruby
|
|
170
|
+
class Lime
|
|
171
|
+
end # message is now: uninitialized constant PrepsFruitsTest::Mango
|
|
172
|
+
|
|
173
|
+
class Mango
|
|
174
|
+
end # message is now: uninitialized constant PrepsFruitsTest::Pineapple
|
|
175
|
+
|
|
176
|
+
class Pineapple
|
|
177
|
+
end
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
After defining `Pineapple`, the next message becomes more interesting!
|
|
181
|
+
|
|
182
|
+
```
|
|
183
|
+
No method `FetchesFruits#fetch' exists for call:
|
|
184
|
+
|
|
185
|
+
fetch([:lime, :mango, :pineapple])
|
|
186
|
+
|
|
187
|
+
Need to define the method? Here's a sample definition:
|
|
188
|
+
|
|
189
|
+
def fetch(lime_mango_pineapple)
|
|
190
|
+
end
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Mocktail can see what we're stubbing and tries to guess the number and name of
|
|
194
|
+
arguments based on what we passed in, generating a little method for us. We can
|
|
195
|
+
paste that in or we can write our own to clear the error and change the message:
|
|
196
|
+
|
|
197
|
+
```ruby
|
|
198
|
+
class FetchesFruits
|
|
199
|
+
def fetch(types)
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
And now we're back to passing! Let's move onto the next stubbing. This one is
|
|
205
|
+
much fancier, so look closely:
|
|
206
|
+
|
|
207
|
+
```ruby
|
|
208
|
+
def test_prep
|
|
209
|
+
fruits = [Lime.new, Mango.new, Pineapple.new]
|
|
210
|
+
stubs { @fetches_fruits.fetch([:lime, :mango, :pineapple]) }.with { fruits }
|
|
211
|
+
stubs { |m| @slices_fruit.slice(m.is_a(Fruit)) }.with { |call|
|
|
212
|
+
SlicedFruit.new(call.args.first)
|
|
213
|
+
}
|
|
214
|
+
# …TODO
|
|
215
|
+
end
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
This stubbing is taking advantage of two advanced features in Mocktail:
|
|
219
|
+
|
|
220
|
+
* **Argument matchers -** the [m.is_a](support/api.md#mis_a) matcher allows the
|
|
221
|
+
stubbing to do triple-duty and results in the stub being satisfied any time
|
|
222
|
+
a `Fruit` instance is passed to `@slices_fruit.slice`
|
|
223
|
+
* **Call introspection -** `with` receives an optional
|
|
224
|
+
[Call](/src/mocktail/value/call.rb) block param that represents an actual call
|
|
225
|
+
of the mocked method by the subject whenever it satisfies the stub configuration
|
|
226
|
+
|
|
227
|
+
If you're not used to reading Mocktail's API yet, these two stubbings can be
|
|
228
|
+
combined to facilitate production code like this:
|
|
229
|
+
|
|
230
|
+
```ruby
|
|
231
|
+
@fetches_fruits.fetch([:lime, :mango, :pineapple]).map { |fruit|
|
|
232
|
+
@slices_fruit.slice(fruit)
|
|
233
|
+
} #=> returns [SlicedFruit(Lime), SlicedFruit(Mango), SlicedFruit(Pineapple)]
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
But initially, this will raise a few new errors for us to clear, starting with
|
|
237
|
+
`uninitialized constant PrepsFruitsTest::Fruit`. Let's clear it:
|
|
238
|
+
|
|
239
|
+
```ruby
|
|
240
|
+
class Fruit
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
class Lime < Fruit
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
class Mango < Fruit
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
class Pineapple < Fruit
|
|
250
|
+
end
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Here's the next error:
|
|
254
|
+
|
|
255
|
+
```
|
|
256
|
+
No method `SlicesFruit#slice' exists for call:
|
|
257
|
+
|
|
258
|
+
slice(is_a(Fruit))
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
So let's go and create one!
|
|
262
|
+
|
|
263
|
+
```ruby
|
|
264
|
+
class SlicesFruit
|
|
265
|
+
def slice(fruit)
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
And we're back to passing.
|
|
271
|
+
|
|
272
|
+
## Act: invoking our subject
|
|
273
|
+
|
|
274
|
+
Impatient readers will note in frustration that we've created eight classes but
|
|
275
|
+
still haven't _even defined the method to be tested_, `PrepsFood#prep`!
|
|
276
|
+
|
|
277
|
+
Now that our basic stubs are in place, let's invoke that method as our test's
|
|
278
|
+
"[act](support/glossary.md#arrange-act-assert)" phase:
|
|
279
|
+
|
|
280
|
+
```ruby
|
|
281
|
+
def test_prep
|
|
282
|
+
fruits = [Lime.new, Mango.new, Pineapple.new]
|
|
283
|
+
stubs { @fetches_fruits.fetch([:lime, :mango, :pineapple]) }.with { fruits }
|
|
284
|
+
stubs { |m| @slices_fruit.slice(m.is_a(Fruit)) }.with { |call|
|
|
285
|
+
SlicedFruit.new(call.args.first)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
@subject.prep([:lime, :mango, :pineapple])
|
|
289
|
+
|
|
290
|
+
# …TODO
|
|
291
|
+
end
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
As you might expect, the test is telling us to create the method we're here to
|
|
295
|
+
implement: `undefined method 'prep' for #<PrepsFruits:0x0000000105008060>`, so
|
|
296
|
+
let's make it:
|
|
297
|
+
|
|
298
|
+
```ruby
|
|
299
|
+
class PrepsFruits
|
|
300
|
+
def prep(fruit_types)
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
And we're passing again.
|
|
306
|
+
|
|
307
|
+
That's it! The "Act" phase is usually a one line invocation, because ideally the
|
|
308
|
+
subject should be able to do its job being told only once.
|
|
309
|
+
|
|
310
|
+
## Assert: verifying our interactions
|
|
311
|
+
|
|
312
|
+
Now that we've completed the Arrange and Act, we can deal with the
|
|
313
|
+
[Assert](support/glossary.md#arrange-act-assert) phase of our test.
|
|
314
|
+
|
|
315
|
+
But how should we assert that the sliced fruit gets stored? None of the code
|
|
316
|
+
exists yet, so the assertion we encode into our test will specify the API of the
|
|
317
|
+
class responsible for storing fruit. This can make some folks feel a little
|
|
318
|
+
queasy, because in the context of traditional TDD, the subject should be a black
|
|
319
|
+
box—meaning tests should not be aware of, much less determine the subject's
|
|
320
|
+
implementation details. But isolated TDD flips things inside-out: the test
|
|
321
|
+
becomes a sounding board for iterating rapidly on the public APIs of not only
|
|
322
|
+
the subject, but of the layer of dependencies beneath it. This gives the test
|
|
323
|
+
author the opportunity to play with a new API contract (name, parameters, and
|
|
324
|
+
return value) via a lightweight
|
|
325
|
+
[demonstration](support/glossary.md#demonstration) of a method that doesn't even
|
|
326
|
+
exist yet, which means every new method is created through _actual, necessary
|
|
327
|
+
use_ instead of being typed into a blank class listing. Letting usage determine
|
|
328
|
+
the API confers the same benefits as [readme-driven
|
|
329
|
+
development](https://tom.preston-werner.com/2010/08/23/readme-driven-development.html)
|
|
330
|
+
by working outside-in and making mistakes cheap to fix. If a method doesn't feel
|
|
331
|
+
right, reconfiguring the stubbing in place doesn't require switching files,
|
|
332
|
+
renaming references, or moving parameters around. Put differently, if
|
|
333
|
+
refactoring is the third step in classical TDD's "red-green-refactor", it's the
|
|
334
|
+
_first_ step when practicing isolated TDD. (Prefactoring?)
|
|
335
|
+
|
|
336
|
+
To illustrate how the assertions we choose can impact the API of our subject and
|
|
337
|
+
its dependencies, we're going to show two different ways to finish writing this
|
|
338
|
+
test, starting with an approach that verifies a call occurred and finishing with
|
|
339
|
+
assertions of a return value from the subject.
|
|
340
|
+
|
|
341
|
+
### Approach 1: verifying the method was called
|
|
342
|
+
|
|
343
|
+
One approach would be to verify that `StoresFruit#store` is invoked for each
|
|
344
|
+
`SlicedFruit` instance using [verify](support/api.md#mocktailverify). Let's play
|
|
345
|
+
that out here.
|
|
346
|
+
|
|
347
|
+
```ruby
|
|
348
|
+
def test_prep
|
|
349
|
+
fruits = [Lime.new, Mango.new, Pineapple.new]
|
|
350
|
+
stubs { @fetches_fruits.fetch([:lime, :mango, :pineapple]) }.with { fruits }
|
|
351
|
+
stubs { |m| @slices_fruit.slice(m.is_a(Fruit)) }.with { |call|
|
|
352
|
+
SlicedFruit.new(call.args.first)
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
@subject.prep([:lime, :mango, :pineapple])
|
|
356
|
+
|
|
357
|
+
verify { |m|
|
|
358
|
+
@stores_fruit.store(m.that { |sliced_fruit| sliced_fruit.type == Lime })
|
|
359
|
+
}
|
|
360
|
+
verify { |m|
|
|
361
|
+
@stores_fruit.store(m.that { |sliced_fruit| sliced_fruit.type == Mango })
|
|
362
|
+
}
|
|
363
|
+
verify { |m|
|
|
364
|
+
@stores_fruit.store(m.that { |sliced_fruit| sliced_fruit.type == Pineapple })
|
|
365
|
+
}
|
|
366
|
+
end
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
Above, we're using another matcher, [m.that](support/api.md#mthat), to ensure
|
|
370
|
+
that each `store` was called with one of each type of fruit. (`m.that` takes a
|
|
371
|
+
block parameter that receives the actual argument it stands in for, passing the
|
|
372
|
+
verification when it returns truthy and failing it otherwise.)
|
|
373
|
+
|
|
374
|
+
We could have implemented this verification more simply by assigning
|
|
375
|
+
`SlicedFruit` instances in the test and stubbing & verifying them by reference.
|
|
376
|
+
This would have added another set of variables to track, but would eliminate the
|
|
377
|
+
need for using any `m` argument matchers. (The purpose of these docs is to teach
|
|
378
|
+
Mocktail's API, so it errs on the side of leaning harder into the library's
|
|
379
|
+
features.)
|
|
380
|
+
|
|
381
|
+
Running our test yields our next error `No method 'StoresFruit#store' exists`.
|
|
382
|
+
We can fix that:
|
|
383
|
+
|
|
384
|
+
```ruby
|
|
385
|
+
class StoresFruit
|
|
386
|
+
def store(fruit)
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
Finally, this yields an actual assertion failure:
|
|
392
|
+
|
|
393
|
+
```
|
|
394
|
+
Mocktail::VerificationError: Expected mocktail of `StoresFruit#store' to be called like:
|
|
395
|
+
|
|
396
|
+
store(that {…})
|
|
397
|
+
|
|
398
|
+
But it was never called.
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
This is a big deal! It means it's _finally_ time to start implementing the
|
|
402
|
+
subject method based on all the decisions driven out by our test so far. We can
|
|
403
|
+
write most of these interactions in one fell swoop, because the test setup
|
|
404
|
+
already forced us to make most of the decisions of consequence about the code
|
|
405
|
+
itself.
|
|
406
|
+
|
|
407
|
+
Here's how the `PrepsFruit` class might shake out:
|
|
408
|
+
|
|
409
|
+
```ruby
|
|
410
|
+
class PrepsFruits
|
|
411
|
+
def initialize
|
|
412
|
+
@fetches_fruits = FetchesFruits.new
|
|
413
|
+
@slices_fruit = SlicesFruit.new
|
|
414
|
+
@stores_fruit = StoresFruit.new
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
def prep(fruit_types)
|
|
418
|
+
@fetches_fruits.fetch(fruit_types).each do |fruit|
|
|
419
|
+
sliced_fruit = @slices_fruit.slice(fruit)
|
|
420
|
+
@stores_fruit.store(sliced_fruit)
|
|
421
|
+
end
|
|
422
|
+
end
|
|
423
|
+
end
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
This may feel like a lot of code to write in one go, but it was all preordained
|
|
427
|
+
by the test, so it kind of just writes itself.
|
|
428
|
+
|
|
429
|
+
Does it work? No! But it changed the message to something we might not have
|
|
430
|
+
realized we hadn't created yet: `uninitialized constant
|
|
431
|
+
PrepsFruitsTest::SlicedFruit`.
|
|
432
|
+
|
|
433
|
+
This is actually good news! Because `SlicedFruit.new` is only refrenced inside
|
|
434
|
+
a `stubs…with {}` block, it means the implementation above is successfully
|
|
435
|
+
invoking the first two of three dependencies.
|
|
436
|
+
|
|
437
|
+
Let's implement our `SlicedFruit` [value object](support/glossary.md#value)
|
|
438
|
+
next, noting that its initializer takes a basic `Fruit` object and (per what we
|
|
439
|
+
specified in our `verify` block), exposes the fruit's class via a `type` method:
|
|
440
|
+
|
|
441
|
+
```ruby
|
|
442
|
+
class SlicedFruit
|
|
443
|
+
def initialize(fruit)
|
|
444
|
+
@fruit = fruit
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
def type
|
|
448
|
+
@fruit.class
|
|
449
|
+
end
|
|
450
|
+
end
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
And… boom! The test passes. That means all of our setup and assertions worked
|
|
454
|
+
and the implementation passes the test!
|
|
455
|
+
|
|
456
|
+
That said, _never trust a test you haven't seen fail_. To be sure the test's
|
|
457
|
+
passing isn't an indication of a faulty assertion, let's jiggle the handle by
|
|
458
|
+
tweaking one of those `verify` calls:
|
|
459
|
+
|
|
460
|
+
```ruby
|
|
461
|
+
verify { |m|
|
|
462
|
+
@stores_fruit.store(m.that { |sliced_fruit| sliced_fruit.type == :nonsense })
|
|
463
|
+
}
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
Running the test again, we get an error that tells us that the code is doing
|
|
467
|
+
exactly what we want… yay!
|
|
468
|
+
|
|
469
|
+
```
|
|
470
|
+
Mocktail::VerificationError: Expected mocktail of `StoresFruit#store' to be called like:
|
|
471
|
+
|
|
472
|
+
store(that {…})
|
|
473
|
+
|
|
474
|
+
It was called differently 3 times:
|
|
475
|
+
|
|
476
|
+
store(#<SlicedFruit:0x00000001036357a0 @fruit=#<Lime:0x00000001036d1998>>)
|
|
477
|
+
|
|
478
|
+
store(#<SlicedFruit:0x000000010622df10 @fruit=#<Mango:0x00000001036d1808>>)
|
|
479
|
+
|
|
480
|
+
store(#<SlicedFruit:0x0000000106226580 @fruit=#<Pineapple:0x00000001036d16f0>>)
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
We could stop here and call the job done. We have a working `PrepsFruit` class
|
|
484
|
+
that does what it set out to do by loading, slicing, and storing fruits. But
|
|
485
|
+
because this is a tutorial, let's take a moment to reflect on where this test
|
|
486
|
+
led us and how things could have played out differently.
|
|
487
|
+
|
|
488
|
+
### Approach 2: asserting a result value
|
|
489
|
+
|
|
490
|
+
Because `each` doesn't return a meaningful value, whenever we see an `each`
|
|
491
|
+
block, whatever it's doing must be a side effect. And since side effects are
|
|
492
|
+
generally less desirable than returning values, it's worth pausing and asking if
|
|
493
|
+
there was a different approach we could have taken.
|
|
494
|
+
|
|
495
|
+
It turns out, there is! Even though Mocktail offers a robust `verify` method,
|
|
496
|
+
it should be used sparingly, because—like `each`—its only real utility is to
|
|
497
|
+
specify interactions that don't return a value. Methods that return values are
|
|
498
|
+
generally more useful, so let's rewind the clock, delete the production code
|
|
499
|
+
we just wrote, and set up our assertions to interrogate a return value instead
|
|
500
|
+
of verifying calls to `StoresFruit#store`.
|
|
501
|
+
|
|
502
|
+
Here's where the test stood after completing the Act phase:
|
|
503
|
+
|
|
504
|
+
```ruby
|
|
505
|
+
def test_prep
|
|
506
|
+
fruits = [Lime.new, Mango.new, Pineapple.new]
|
|
507
|
+
stubs { @fetches_fruits.fetch([:lime, :mango, :pineapple]) }.with { fruits }
|
|
508
|
+
stubs { |m| @slices_fruit.slice(m.is_a(Fruit)) }.with { |call|
|
|
509
|
+
SlicedFruit.new(call.args.first)
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
result = @subject.prep([:lime, :mango, :pineapple])
|
|
513
|
+
end
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
And here's an alternate assertion we might have written, this time stubbing
|
|
517
|
+
`StoresFruit#store` and asserting the return value:
|
|
518
|
+
|
|
519
|
+
```ruby
|
|
520
|
+
def test_prep
|
|
521
|
+
fruits = [Lime.new, Mango.new, Pineapple.new]
|
|
522
|
+
stubs { @fetches_fruits.fetch([:lime, :mango, :pineapple]) }.with { fruits }
|
|
523
|
+
stubs { |m| @slices_fruit.slice(m.is_a(Fruit)) }.with { |call|
|
|
524
|
+
SlicedFruit.new(call.args.first)
|
|
525
|
+
}
|
|
526
|
+
stubs { |m| @stores_fruit.store(m.is_a(SlicedFruit)) }.with { |call|
|
|
527
|
+
StoredFruit.new("ID for #{call.args.first.type}", call.args.first)
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
result = @subject.prep([:lime, :mango, :pineapple])
|
|
531
|
+
|
|
532
|
+
assert_equal 3, result.size
|
|
533
|
+
assert_equal "ID for Lime", result[0].id
|
|
534
|
+
assert_equal Lime, result[0].fruit.type
|
|
535
|
+
assert_equal "ID for Mango", result[1].id
|
|
536
|
+
assert_equal Mango, result[1].fruit.type
|
|
537
|
+
assert_equal "ID for Pineapple", result[2].id
|
|
538
|
+
assert_equal Pineapple, result[2].fruit.type
|
|
539
|
+
end
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
In the reimagined test above, we decided that instead of being a fire-and-forget
|
|
543
|
+
call, a value representing a stored fruit with a unique ID could be returned
|
|
544
|
+
from `StoresFruit.store`. Of course, the stubbing _could_ have mutated the
|
|
545
|
+
`SlicedFruit` passed to it (just as `SlicedFruit#slice` could have mutated the
|
|
546
|
+
`Fruit` it received), but if we're going to go to great lengths to return values
|
|
547
|
+
instead of have side effects, we may as well go the extra mile and avoid
|
|
548
|
+
mutating those values.
|
|
549
|
+
|
|
550
|
+
After clearing out `PrepsFruit#prep`, the test will error with `undefined method
|
|
551
|
+
'size' for nil:NilClass`, since it's not returning anything.
|
|
552
|
+
|
|
553
|
+
Let's try our hand at a new implementation that should pass the test:
|
|
554
|
+
|
|
555
|
+
```ruby
|
|
556
|
+
def prep(fruit_types)
|
|
557
|
+
@fetches_fruits.fetch(fruit_types).map { |fruit|
|
|
558
|
+
fruit = @slices_fruit.slice(fruit)
|
|
559
|
+
@stores_fruit.store(fruit)
|
|
560
|
+
}
|
|
561
|
+
end
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
But it errors! The `StoredFruit` value hasn't been created yet. Let's clear the
|
|
565
|
+
`uninitialized constant PrepsFruitsTest::StoredFruit` message by implementing it
|
|
566
|
+
as a `Struct`:
|
|
567
|
+
|
|
568
|
+
```ruby
|
|
569
|
+
StoredFruit = Struct.new(:id, :fruit)
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
And now things are passing!
|
|
573
|
+
|
|
574
|
+
If we're paranoid, we can quickly check that everything's working by changing
|
|
575
|
+
out one of the test's final assertions to see the failure it produces:
|
|
576
|
+
|
|
577
|
+
```ruby
|
|
578
|
+
assert_equal "ID for Zebras", result[2].id
|
|
579
|
+
#=>
|
|
580
|
+
# PrepsFruitsTest#test_prep [example.rb:29]:
|
|
581
|
+
# Expected: "ID for Zebras"
|
|
582
|
+
# Actual: "ID for Pineapple"
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
And that's the kind of failure that we'd expect to see if everything was working
|
|
586
|
+
as we expected. Great job!
|
|
587
|
+
|
|
588
|
+
There are a few places you could explore next:
|
|
589
|
+
|
|
590
|
+
**Run the [test code for this tutorial yourself](support/example_test.rb).**
|
|
591
|
+
|
|
592
|
+
**Learn about Mocktail's [debugging and introspection APIs](support/api.md#debugging).**
|
|
593
|
+
|
|
594
|
+
**Check out an advanced feature not covered in this guide: [argument captors](support/api.md#mocktailcaptor).**
|