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