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,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).**
@@ -0,0 +1,47 @@
1
+ # Using mocks to fix a broken test you can't understand
2
+
3
+ Suppose you have a large suite of tests and one of them just broke the build.
4
+ You didn't write this test or the thing it's testing. You just know you've gotta
5
+ figure out a way to fix it. The test is sprawling, confusing, and unclear. The
6
+ code it's testing isn't much better. You could fight to understand what is going
7
+ on, ascertain why things are breaking, and then fix it in a way that left things
8
+ better than you found them, but your first impression is that could take you
9
+ _days_ to accomplish.
10
+
11
+ A thought occurs: you could make the test go green by replacing the source of
12
+ the error with a mock and then stubbing whatever responses might be necessary
13
+ to make the test pass again.
14
+
15
+ Which is how you wound up installing Mocktail and then clicking through its
16
+ silly choose-your-own-adventure README, and here we are.
17
+
18
+ As discussed elsewhere, Mocktail is designed with [test-driven
19
+ development](../tdd.md) of _new_ tests and classes in mind, and not as a tool to
20
+ assist in legacy rescue, fixing broken tests, or otherwise coping with existing
21
+ complexity.
22
+
23
+ Situations like the one described above are sadly _very common_, but there is no
24
+ alternative to putting in the work to grapple with the complexity and tame it
25
+ enough to understand _why_ a test is failing and then ensuring the fix doesn't
26
+ undermine the purpose of the test. Put differently, if you were to make the test
27
+ pass by plugging a leak in the dike with a mock object, there's no way to know
28
+ that the originating failure wasn't indicating an actual real-world bug or
29
+ problem in the code. Additionally, the comprehensibility and value of the test
30
+ would almost certainly be left worse off after the fix is applied.
31
+
32
+ Cases like this are one of the reason most experienced developers hate dealing
33
+ with mock objects, because they're so often made to paper over underlying
34
+ problems by abusing their ability to make the computer lie to you.
35
+
36
+ As a result, if you have an existing test that wasn't built from the ground-up
37
+ as an isolated unit test that used [test
38
+ doubles](../support/glossary.md#test-double) to achieve isolation from the
39
+ [subject](../support/glossary.md#subject-under-test)'s
40
+ [dependencies](../support/glossary.md#dependency), adding mocks to it is almost
41
+ always a worse long-term solution than disabling the test entirely.
42
+
43
+ **If you've heard enough, you can go back and consider [non-TDD use cases for Mocktail](../other_uses.md).**
44
+
45
+ **Or if you're finally ready to walk the golden path, you can revisit [Mocktail as a TDD tool](../tdd.md).**
46
+
47
+