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.
Files changed (443) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/main.yml +6 -5
  3. data/.gitignore +3 -0
  4. data/.standard.yml +8 -0
  5. data/CHANGELOG.md +19 -0
  6. data/Gemfile +8 -2
  7. data/Gemfile.lock +105 -38
  8. data/README.md +20 -922
  9. data/Rakefile +0 -1
  10. data/bin/console +1 -2
  11. data/bin/tapioca +29 -0
  12. data/docs/code_of_conduct.md +8 -0
  13. data/docs/configuring_minitest.md +20 -0
  14. data/docs/configuring_rspec.md +19 -0
  15. data/docs/example_test.md +594 -0
  16. data/docs/faq/existing_tests.md +47 -0
  17. data/docs/faq/mocking_http.md +151 -0
  18. data/docs/faq/mocking_the_subject.md +161 -0
  19. data/docs/faq/mocking_time.md +34 -0
  20. data/docs/faq/partial_mocks.md +34 -0
  21. data/docs/faq/verifying_real_interactions.md +52 -0
  22. data/docs/img/delegator_tree.png +0 -0
  23. data/docs/img/example_test.png +0 -0
  24. data/docs/img/extract_transform_load.png +0 -0
  25. data/docs/img/mocktail_sorbet.jpg +0 -0
  26. data/docs/img/mocktail_untyped.jpg +0 -0
  27. data/docs/img/spacer.png +0 -0
  28. data/docs/installation_sorbet.md +99 -0
  29. data/docs/installation_untyped.md +29 -0
  30. data/docs/other_uses.md +32 -0
  31. data/docs/stubbing_and_verifying.md +515 -0
  32. data/docs/support/api.md +819 -0
  33. data/docs/support/example_test.rb +94 -0
  34. data/docs/support/glossary.md +426 -0
  35. data/docs/tdd/class_methods.md +44 -0
  36. data/docs/tdd/poro/dependency_inception.md +136 -0
  37. data/docs/tdd/poro/dependency_injection.md +82 -0
  38. data/docs/tdd/poro.md +15 -0
  39. data/docs/tdd/third_party.md +220 -0
  40. data/docs/tdd.md +15 -0
  41. data/lib/mocktail/collects_calls.rb +2 -0
  42. data/lib/mocktail/debug.rb +16 -12
  43. data/lib/mocktail/dsl.rb +2 -0
  44. data/lib/mocktail/errors.rb +2 -0
  45. data/lib/mocktail/explains_nils.rb +2 -0
  46. data/lib/mocktail/explains_thing.rb +7 -4
  47. data/lib/mocktail/grabs_original_method_parameters.rb +30 -0
  48. data/lib/mocktail/handles_dry_call/fulfills_stubbing/describes_unsatisfied_stubbing.rb +3 -1
  49. data/lib/mocktail/handles_dry_call/fulfills_stubbing/finds_satisfaction.rb +5 -1
  50. data/lib/mocktail/handles_dry_call/fulfills_stubbing.rb +2 -0
  51. data/lib/mocktail/handles_dry_call/logs_call.rb +2 -0
  52. data/lib/mocktail/handles_dry_call/validates_arguments.rb +6 -4
  53. data/lib/mocktail/handles_dry_call.rb +2 -0
  54. data/lib/mocktail/handles_dry_new_call.rb +2 -0
  55. data/lib/mocktail/imitates_type/ensures_imitation_support.rb +2 -0
  56. data/lib/mocktail/imitates_type/makes_double/declares_dry_class/reconstructs_call.rb +4 -1
  57. data/lib/mocktail/imitates_type/makes_double/declares_dry_class.rb +32 -20
  58. data/lib/mocktail/imitates_type/makes_double/gathers_fakeable_instance_methods.rb +2 -0
  59. data/lib/mocktail/imitates_type/makes_double.rb +3 -0
  60. data/lib/mocktail/imitates_type.rb +3 -1
  61. data/lib/mocktail/initialize_based_on_type_system_mode_switching.rb +9 -0
  62. data/lib/mocktail/initializes_mocktail.rb +5 -0
  63. data/lib/mocktail/matcher_presentation.rb +4 -2
  64. data/lib/mocktail/matchers/any.rb +4 -3
  65. data/lib/mocktail/matchers/base.rb +10 -2
  66. data/lib/mocktail/matchers/captor.rb +9 -0
  67. data/lib/mocktail/matchers/includes.rb +2 -0
  68. data/lib/mocktail/matchers/includes_hash.rb +9 -0
  69. data/lib/mocktail/matchers/includes_key.rb +9 -0
  70. data/lib/mocktail/matchers/includes_string.rb +9 -0
  71. data/lib/mocktail/matchers/is_a.rb +2 -0
  72. data/lib/mocktail/matchers/matches.rb +2 -0
  73. data/lib/mocktail/matchers/not.rb +2 -0
  74. data/lib/mocktail/matchers/numeric.rb +5 -4
  75. data/lib/mocktail/matchers/that.rb +2 -0
  76. data/lib/mocktail/matchers.rb +3 -0
  77. data/lib/mocktail/raises_neato_no_method_error.rb +2 -0
  78. data/lib/mocktail/records_demonstration.rb +2 -0
  79. data/lib/mocktail/registers_matcher.rb +8 -3
  80. data/lib/mocktail/registers_stubbing.rb +2 -0
  81. data/lib/mocktail/replaces_next.rb +7 -1
  82. data/lib/mocktail/replaces_type/redefines_new.rb +3 -1
  83. data/lib/mocktail/replaces_type/redefines_singleton_methods.rb +14 -2
  84. data/lib/mocktail/replaces_type/runs_sorbet_sig_blocks_before_replacement.rb +37 -0
  85. data/lib/mocktail/replaces_type.rb +6 -0
  86. data/lib/mocktail/resets_state.rb +2 -0
  87. data/lib/mocktail/share/bind.rb +7 -5
  88. data/lib/mocktail/share/cleans_backtrace.rb +3 -5
  89. data/lib/mocktail/share/creates_identifier.rb +16 -9
  90. data/lib/mocktail/share/determines_matching_calls.rb +4 -2
  91. data/lib/mocktail/share/stringifies_call.rb +6 -3
  92. data/lib/mocktail/share/stringifies_method_name.rb +3 -1
  93. data/lib/mocktail/simulates_argument_error/reconciles_args_with_params.rb +2 -0
  94. data/lib/mocktail/simulates_argument_error/recreates_message.rb +2 -0
  95. data/lib/mocktail/simulates_argument_error/transforms_params.rb +16 -9
  96. data/lib/mocktail/simulates_argument_error.rb +2 -0
  97. data/lib/mocktail/sorbet/mocktail/collects_calls.rb +18 -0
  98. data/lib/mocktail/sorbet/mocktail/debug.rb +55 -0
  99. data/lib/mocktail/sorbet/mocktail/dsl.rb +46 -0
  100. data/lib/mocktail/sorbet/mocktail/errors.rb +19 -0
  101. data/lib/mocktail/sorbet/mocktail/explains_nils.rb +41 -0
  102. data/lib/mocktail/sorbet/mocktail/explains_thing.rb +137 -0
  103. data/lib/mocktail/sorbet/mocktail/grabs_original_method_parameters.rb +33 -0
  104. data/lib/mocktail/sorbet/mocktail/handles_dry_call/fulfills_stubbing/describes_unsatisfied_stubbing.rb +27 -0
  105. data/lib/mocktail/sorbet/mocktail/handles_dry_call/fulfills_stubbing/finds_satisfaction.rb +24 -0
  106. data/lib/mocktail/sorbet/mocktail/handles_dry_call/fulfills_stubbing.rb +45 -0
  107. data/lib/mocktail/sorbet/mocktail/handles_dry_call/logs_call.rb +12 -0
  108. data/lib/mocktail/sorbet/mocktail/handles_dry_call/validates_arguments.rb +45 -0
  109. data/lib/mocktail/sorbet/mocktail/handles_dry_call.rb +25 -0
  110. data/lib/mocktail/sorbet/mocktail/handles_dry_new_call.rb +42 -0
  111. data/lib/mocktail/sorbet/mocktail/imitates_type/ensures_imitation_support.rb +16 -0
  112. data/lib/mocktail/sorbet/mocktail/imitates_type/makes_double/declares_dry_class/reconstructs_call.rb +73 -0
  113. data/lib/mocktail/sorbet/mocktail/imitates_type/makes_double/declares_dry_class.rb +136 -0
  114. data/lib/mocktail/sorbet/mocktail/imitates_type/makes_double/gathers_fakeable_instance_methods.rb +28 -0
  115. data/lib/mocktail/sorbet/mocktail/imitates_type/makes_double.rb +29 -0
  116. data/lib/mocktail/sorbet/mocktail/imitates_type.rb +29 -0
  117. data/lib/mocktail/sorbet/mocktail/initialize_based_on_type_system_mode_switching.rb +11 -0
  118. data/lib/mocktail/sorbet/mocktail/initializes_mocktail.rb +25 -0
  119. data/lib/mocktail/sorbet/mocktail/matcher_presentation.rb +21 -0
  120. data/lib/mocktail/sorbet/mocktail/matchers/any.rb +27 -0
  121. data/lib/mocktail/sorbet/mocktail/matchers/base.rb +39 -0
  122. data/lib/mocktail/sorbet/mocktail/matchers/captor.rb +76 -0
  123. data/lib/mocktail/sorbet/mocktail/matchers/includes.rb +32 -0
  124. data/lib/mocktail/sorbet/mocktail/matchers/includes_hash.rb +12 -0
  125. data/lib/mocktail/sorbet/mocktail/matchers/includes_key.rb +12 -0
  126. data/lib/mocktail/sorbet/mocktail/matchers/includes_string.rb +12 -0
  127. data/lib/mocktail/sorbet/mocktail/matchers/is_a.rb +17 -0
  128. data/lib/mocktail/sorbet/mocktail/matchers/matches.rb +19 -0
  129. data/lib/mocktail/sorbet/mocktail/matchers/not.rb +17 -0
  130. data/lib/mocktail/sorbet/mocktail/matchers/numeric.rb +27 -0
  131. data/lib/mocktail/sorbet/mocktail/matchers/that.rb +32 -0
  132. data/lib/mocktail/sorbet/mocktail/matchers.rb +19 -0
  133. data/lib/mocktail/sorbet/mocktail/raises_neato_no_method_error.rb +93 -0
  134. data/lib/mocktail/sorbet/mocktail/records_demonstration.rb +43 -0
  135. data/lib/mocktail/sorbet/mocktail/registers_matcher.rb +65 -0
  136. data/lib/mocktail/sorbet/mocktail/registers_stubbing.rb +31 -0
  137. data/lib/mocktail/sorbet/mocktail/replaces_next.rb +55 -0
  138. data/lib/mocktail/sorbet/mocktail/replaces_type/redefines_new.rb +32 -0
  139. data/lib/mocktail/sorbet/mocktail/replaces_type/redefines_singleton_methods.rb +80 -0
  140. data/lib/mocktail/sorbet/mocktail/replaces_type/runs_sorbet_sig_blocks_before_replacement.rb +39 -0
  141. data/lib/mocktail/sorbet/mocktail/replaces_type.rb +36 -0
  142. data/lib/mocktail/sorbet/mocktail/resets_state.rb +14 -0
  143. data/lib/mocktail/sorbet/mocktail/share/bind.rb +18 -0
  144. data/lib/mocktail/sorbet/mocktail/share/cleans_backtrace.rb +22 -0
  145. data/lib/mocktail/sorbet/mocktail/share/creates_identifier.rb +39 -0
  146. data/lib/mocktail/sorbet/mocktail/share/determines_matching_calls.rb +72 -0
  147. data/lib/mocktail/sorbet/mocktail/share/stringifies_call.rb +84 -0
  148. data/lib/mocktail/sorbet/mocktail/share/stringifies_method_name.rb +16 -0
  149. data/lib/mocktail/sorbet/mocktail/simulates_argument_error/reconciles_args_with_params.rb +27 -0
  150. data/lib/mocktail/sorbet/mocktail/simulates_argument_error/recreates_message.rb +34 -0
  151. data/lib/mocktail/sorbet/mocktail/simulates_argument_error/transforms_params.rb +58 -0
  152. data/lib/mocktail/sorbet/mocktail/simulates_argument_error.rb +36 -0
  153. data/lib/mocktail/sorbet/mocktail/sorbet.rb +3 -0
  154. data/lib/mocktail/sorbet/mocktail/stringifies_method_signature.rb +53 -0
  155. data/lib/mocktail/sorbet/mocktail/typed.rb +5 -0
  156. data/lib/mocktail/sorbet/mocktail/value/cabinet.rb +91 -0
  157. data/lib/mocktail/sorbet/mocktail/value/call.rb +51 -0
  158. data/lib/mocktail/sorbet/mocktail/value/demo_config.rb +10 -0
  159. data/lib/mocktail/sorbet/mocktail/value/double.rb +10 -0
  160. data/lib/mocktail/sorbet/mocktail/value/double_data.rb +15 -0
  161. data/lib/mocktail/sorbet/mocktail/value/explanation.rb +68 -0
  162. data/lib/mocktail/sorbet/mocktail/value/explanation_data.rb +19 -0
  163. data/lib/mocktail/sorbet/mocktail/value/fake_method_data.rb +11 -0
  164. data/lib/mocktail/sorbet/mocktail/value/matcher_registry.rb +27 -0
  165. data/lib/mocktail/sorbet/mocktail/value/no_explanation_data.rb +20 -0
  166. data/lib/mocktail/sorbet/mocktail/value/signature.rb +35 -0
  167. data/lib/mocktail/sorbet/mocktail/value/stubbing.rb +26 -0
  168. data/lib/mocktail/sorbet/mocktail/value/top_shelf.rb +79 -0
  169. data/lib/mocktail/sorbet/mocktail/value/type_replacement.rb +11 -0
  170. data/lib/mocktail/sorbet/mocktail/value/type_replacement_data.rb +19 -0
  171. data/lib/mocktail/sorbet/mocktail/value/unsatisfying_call.rb +9 -0
  172. data/lib/mocktail/sorbet/mocktail/value/unsatisfying_call_explanation.rb +24 -0
  173. data/lib/mocktail/sorbet/mocktail/value.rb +19 -0
  174. data/lib/mocktail/sorbet/mocktail/verifies_call/finds_verifiable_calls.rb +21 -0
  175. data/lib/mocktail/sorbet/mocktail/verifies_call/raises_verification_error/gathers_calls_of_method.rb +15 -0
  176. data/lib/mocktail/sorbet/mocktail/verifies_call/raises_verification_error.rb +74 -0
  177. data/lib/mocktail/sorbet/mocktail/verifies_call.rb +37 -0
  178. data/lib/mocktail/sorbet/mocktail/version.rb +12 -0
  179. data/lib/mocktail/sorbet/mocktail.rb +154 -0
  180. data/lib/mocktail/sorbet.rb +1 -0
  181. data/lib/mocktail/stringifies_method_signature.rb +2 -0
  182. data/lib/mocktail/typed.rb +3 -0
  183. data/lib/mocktail/value/cabinet.rb +8 -1
  184. data/lib/mocktail/value/call.rb +44 -12
  185. data/lib/mocktail/value/demo_config.rb +6 -7
  186. data/lib/mocktail/value/double.rb +6 -7
  187. data/lib/mocktail/value/double_data.rb +11 -7
  188. data/lib/mocktail/value/explanation.rb +28 -3
  189. data/lib/mocktail/value/explanation_data.rb +14 -0
  190. data/lib/mocktail/value/fake_method_data.rb +7 -6
  191. data/lib/mocktail/value/matcher_registry.rb +2 -0
  192. data/lib/mocktail/value/no_explanation_data.rb +16 -0
  193. data/lib/mocktail/value/signature.rb +19 -27
  194. data/lib/mocktail/value/stubbing.rb +11 -12
  195. data/lib/mocktail/value/top_shelf.rb +5 -0
  196. data/lib/mocktail/value/type_replacement.rb +7 -8
  197. data/lib/mocktail/value/type_replacement_data.rb +10 -7
  198. data/lib/mocktail/value/unsatisfying_call.rb +5 -6
  199. data/lib/mocktail/value/unsatisfying_call_explanation.rb +18 -0
  200. data/lib/mocktail/value.rb +5 -2
  201. data/lib/mocktail/verifies_call/finds_verifiable_calls.rb +2 -0
  202. data/lib/mocktail/verifies_call/raises_verification_error/gathers_calls_of_method.rb +2 -0
  203. data/lib/mocktail/verifies_call/raises_verification_error.rb +2 -0
  204. data/lib/mocktail/verifies_call.rb +3 -0
  205. data/lib/mocktail/version.rb +8 -1
  206. data/lib/mocktail.rb +46 -5
  207. data/mocktail.gemspec +8 -4
  208. data/rbi/mocktail-pregenerated.rbi +1923 -0
  209. data/rbi/mocktail.rbi +77 -0
  210. data/rbi/sorbet-runtime.rbi +29 -0
  211. data/script/build +47 -0
  212. data/script/setup +17 -0
  213. data/script/spoom_me +7 -0
  214. data/script/strip_sigils +17 -0
  215. data/script/test +33 -0
  216. data/script/test_double_require_warnings +46 -0
  217. data/script/update +21 -0
  218. data/sorbet/config +12 -0
  219. data/sorbet/rbi/annotations/rainbow.rbi +269 -0
  220. data/sorbet/rbi/gems/ast@2.4.2.rbi +584 -0
  221. data/sorbet/rbi/gems/diff-lcs@1.5.0.rbi +1083 -0
  222. data/sorbet/rbi/gems/docile@1.4.0.rbi +376 -0
  223. data/sorbet/rbi/gems/json@2.6.3.rbi +1533 -0
  224. data/sorbet/rbi/gems/language_server-protocol@3.17.0.3.rbi +14237 -0
  225. data/sorbet/rbi/gems/lint_roller@1.0.0.rbi +230 -0
  226. data/sorbet/rbi/gems/m@1.6.1.rbi +257 -0
  227. data/sorbet/rbi/gems/method_source@1.0.0.rbi +272 -0
  228. data/sorbet/rbi/gems/minitest@5.18.0.rbi +1491 -0
  229. data/sorbet/rbi/gems/netrc@0.11.0.rbi +158 -0
  230. data/sorbet/rbi/gems/parallel@1.23.0.rbi +273 -0
  231. data/sorbet/rbi/gems/parser@3.2.2.1.rbi +7253 -0
  232. data/sorbet/rbi/gems/rainbow@3.1.1.rbi +402 -0
  233. data/sorbet/rbi/gems/rake@13.0.6.rbi +3024 -0
  234. data/sorbet/rbi/gems/rbi@0.0.16.rbi +3049 -0
  235. data/sorbet/rbi/gems/regexp_parser@2.8.0.rbi +3748 -0
  236. data/sorbet/rbi/gems/rexml@3.2.5.rbi +4717 -0
  237. data/sorbet/rbi/gems/rubocop-ast@1.29.0.rbi +7003 -0
  238. data/sorbet/rbi/gems/rubocop-performance@1.18.0.rbi +8 -0
  239. data/sorbet/rbi/gems/rubocop-sorbet@0.7.0.rbi +1043 -0
  240. data/sorbet/rbi/gems/rubocop@1.52.0.rbi +55500 -0
  241. data/sorbet/rbi/gems/ruby-progressbar@1.13.0.rbi +1317 -0
  242. data/sorbet/rbi/gems/simplecov-html@0.12.3.rbi +216 -0
  243. data/sorbet/rbi/gems/simplecov@0.22.0.rbi +2148 -0
  244. data/sorbet/rbi/gems/simplecov_json_formatter@0.1.4.rbi +8 -0
  245. data/sorbet/rbi/gems/spoom@1.2.1.rbi +2536 -0
  246. data/sorbet/rbi/gems/standard-custom@1.0.1.rbi +63 -0
  247. data/sorbet/rbi/gems/standard-performance@1.1.0.rbi +8 -0
  248. data/sorbet/rbi/gems/standard@1.29.0.rbi +850 -0
  249. data/sorbet/rbi/gems/tapioca@0.11.6.rbi +3301 -0
  250. data/sorbet/rbi/gems/thor@1.2.2.rbi +3965 -0
  251. data/sorbet/rbi/gems/unicode-display_width@2.4.2.rbi +65 -0
  252. data/sorbet/rbi/gems/unparser@0.6.7.rbi +4524 -0
  253. data/sorbet/rbi/gems/yard-sorbet@0.8.1.rbi +428 -0
  254. data/sorbet/rbi/gems/yard@0.9.34.rbi +18219 -0
  255. data/sorbet/tapioca/config.yml +13 -0
  256. data/sorbet/tapioca/require.rb +4 -0
  257. data/spoom_data/05f0c49.json +1 -0
  258. data/spoom_data/1657012.json +1 -0
  259. data/spoom_data/167434b.json +1 -0
  260. data/spoom_data/1d0ba5b.json +1 -0
  261. data/spoom_data/1ffa724.json +1 -0
  262. data/spoom_data/2129b3d.json +1 -0
  263. data/spoom_data/24b1c92.json +1 -0
  264. data/spoom_data/2c46aee.json +1 -0
  265. data/spoom_data/305ec0b.json +1 -0
  266. data/spoom_data/30e9528.json +1 -0
  267. data/spoom_data/4638cd5.json +1 -0
  268. data/spoom_data/47c7dad.json +1 -0
  269. data/spoom_data/4b1edef.json +1 -0
  270. data/spoom_data/4de157f.json +1 -0
  271. data/spoom_data/526e7db.json +1 -0
  272. data/spoom_data/5d093b9.json +1 -0
  273. data/spoom_data/5db3b43.json +1 -0
  274. data/spoom_data/5fe2a65.json +1 -0
  275. data/spoom_data/6891312.json +1 -0
  276. data/spoom_data/6b0fef4.json +1 -0
  277. data/spoom_data/6b83d12.json +1 -0
  278. data/spoom_data/74c83c2.json +1 -0
  279. data/spoom_data/7644ff4.json +1 -0
  280. data/spoom_data/79054db.json +1 -0
  281. data/spoom_data/814e515.json +1 -0
  282. data/spoom_data/88c3b60.json +1 -0
  283. data/spoom_data/8bd4b6e.json +1 -0
  284. data/spoom_data/93f8153.json +1 -0
  285. data/spoom_data/95242fe.json +1 -0
  286. data/spoom_data/97f4c09.json +1 -0
  287. data/spoom_data/a13d150.json +1 -0
  288. data/spoom_data/a17f215.json +1 -0
  289. data/spoom_data/b705a9d.json +1 -0
  290. data/spoom_data/ba19195.json +1 -0
  291. data/spoom_data/bef51ca.json +1 -0
  292. data/spoom_data/dbb595a.json +1 -0
  293. data/spoom_data/e8fab92.json +1 -0
  294. data/spoom_data/f166c87.json +1 -0
  295. data/spoom_data/f57992d.json +1 -0
  296. data/spoom_data/f5a1e40.json +1 -0
  297. data/spoom_data/f6d6431.json +1 -0
  298. data/spoom_data/f72b67c.json +1 -0
  299. data/spoom_data/fc2f231.json +1 -0
  300. data/spoom_report.html +1248 -0
  301. data/src/mocktail/collects_calls.rb +18 -0
  302. data/src/mocktail/debug.rb +55 -0
  303. data/src/mocktail/dsl.rb +46 -0
  304. data/src/mocktail/errors.rb +19 -0
  305. data/src/mocktail/explains_nils.rb +41 -0
  306. data/src/mocktail/explains_thing.rb +137 -0
  307. data/src/mocktail/grabs_original_method_parameters.rb +33 -0
  308. data/src/mocktail/handles_dry_call/fulfills_stubbing/describes_unsatisfied_stubbing.rb +27 -0
  309. data/src/mocktail/handles_dry_call/fulfills_stubbing/finds_satisfaction.rb +24 -0
  310. data/src/mocktail/handles_dry_call/fulfills_stubbing.rb +45 -0
  311. data/src/mocktail/handles_dry_call/logs_call.rb +12 -0
  312. data/src/mocktail/handles_dry_call/validates_arguments.rb +45 -0
  313. data/src/mocktail/handles_dry_call.rb +25 -0
  314. data/src/mocktail/handles_dry_new_call.rb +42 -0
  315. data/src/mocktail/imitates_type/ensures_imitation_support.rb +16 -0
  316. data/src/mocktail/imitates_type/makes_double/declares_dry_class/reconstructs_call.rb +73 -0
  317. data/src/mocktail/imitates_type/makes_double/declares_dry_class.rb +136 -0
  318. data/src/mocktail/imitates_type/makes_double/gathers_fakeable_instance_methods.rb +28 -0
  319. data/src/mocktail/imitates_type/makes_double.rb +29 -0
  320. data/src/mocktail/imitates_type.rb +29 -0
  321. data/src/mocktail/initialize_based_on_type_system_mode_switching.rb +11 -0
  322. data/src/mocktail/initializes_mocktail.rb +25 -0
  323. data/src/mocktail/matcher_presentation.rb +21 -0
  324. data/src/mocktail/matchers/any.rb +27 -0
  325. data/src/mocktail/matchers/base.rb +39 -0
  326. data/src/mocktail/matchers/captor.rb +76 -0
  327. data/src/mocktail/matchers/includes.rb +32 -0
  328. data/src/mocktail/matchers/includes_hash.rb +12 -0
  329. data/src/mocktail/matchers/includes_key.rb +12 -0
  330. data/src/mocktail/matchers/includes_string.rb +12 -0
  331. data/src/mocktail/matchers/is_a.rb +17 -0
  332. data/src/mocktail/matchers/matches.rb +19 -0
  333. data/src/mocktail/matchers/not.rb +17 -0
  334. data/src/mocktail/matchers/numeric.rb +27 -0
  335. data/src/mocktail/matchers/that.rb +32 -0
  336. data/src/mocktail/matchers.rb +19 -0
  337. data/src/mocktail/raises_neato_no_method_error.rb +93 -0
  338. data/src/mocktail/records_demonstration.rb +43 -0
  339. data/src/mocktail/registers_matcher.rb +65 -0
  340. data/src/mocktail/registers_stubbing.rb +31 -0
  341. data/src/mocktail/replaces_next.rb +55 -0
  342. data/src/mocktail/replaces_type/redefines_new.rb +32 -0
  343. data/src/mocktail/replaces_type/redefines_singleton_methods.rb +80 -0
  344. data/src/mocktail/replaces_type/runs_sorbet_sig_blocks_before_replacement.rb +39 -0
  345. data/src/mocktail/replaces_type.rb +36 -0
  346. data/src/mocktail/resets_state.rb +14 -0
  347. data/src/mocktail/share/bind.rb +18 -0
  348. data/src/mocktail/share/cleans_backtrace.rb +22 -0
  349. data/src/mocktail/share/creates_identifier.rb +39 -0
  350. data/src/mocktail/share/determines_matching_calls.rb +72 -0
  351. data/src/mocktail/share/stringifies_call.rb +84 -0
  352. data/src/mocktail/share/stringifies_method_name.rb +16 -0
  353. data/src/mocktail/simulates_argument_error/reconciles_args_with_params.rb +27 -0
  354. data/src/mocktail/simulates_argument_error/recreates_message.rb +34 -0
  355. data/src/mocktail/simulates_argument_error/transforms_params.rb +58 -0
  356. data/src/mocktail/simulates_argument_error.rb +36 -0
  357. data/src/mocktail/sorbet.rb +3 -0
  358. data/src/mocktail/stringifies_method_signature.rb +53 -0
  359. data/src/mocktail/typed.rb +5 -0
  360. data/src/mocktail/value/cabinet.rb +91 -0
  361. data/src/mocktail/value/call.rb +51 -0
  362. data/src/mocktail/value/demo_config.rb +10 -0
  363. data/src/mocktail/value/double.rb +10 -0
  364. data/src/mocktail/value/double_data.rb +15 -0
  365. data/src/mocktail/value/explanation.rb +68 -0
  366. data/src/mocktail/value/explanation_data.rb +19 -0
  367. data/src/mocktail/value/fake_method_data.rb +11 -0
  368. data/src/mocktail/value/matcher_registry.rb +27 -0
  369. data/src/mocktail/value/no_explanation_data.rb +20 -0
  370. data/src/mocktail/value/signature.rb +35 -0
  371. data/src/mocktail/value/stubbing.rb +26 -0
  372. data/src/mocktail/value/top_shelf.rb +79 -0
  373. data/src/mocktail/value/type_replacement.rb +11 -0
  374. data/src/mocktail/value/type_replacement_data.rb +19 -0
  375. data/src/mocktail/value/unsatisfying_call.rb +9 -0
  376. data/src/mocktail/value/unsatisfying_call_explanation.rb +24 -0
  377. data/src/mocktail/value.rb +19 -0
  378. data/src/mocktail/verifies_call/finds_verifiable_calls.rb +21 -0
  379. data/src/mocktail/verifies_call/raises_verification_error/gathers_calls_of_method.rb +15 -0
  380. data/src/mocktail/verifies_call/raises_verification_error.rb +74 -0
  381. data/src/mocktail/verifies_call.rb +37 -0
  382. data/src/mocktail/version.rb +12 -0
  383. data/src/mocktail.rb +154 -0
  384. data/sub_projects/rbi_generator/Gemfile +4 -0
  385. data/sub_projects/rbi_generator/Gemfile.lock +63 -0
  386. data/sub_projects/rbi_generator/sorbet/rbi/gems/.gitattributes +1 -0
  387. data/sub_projects/sorbet_user/Gemfile +13 -0
  388. data/sub_projects/sorbet_user/Gemfile.lock +126 -0
  389. data/sub_projects/sorbet_user/Rakefile +10 -0
  390. data/sub_projects/sorbet_user/bin/tapioca +27 -0
  391. data/sub_projects/sorbet_user/rbi/mocktail.rbi +9 -0
  392. data/sub_projects/sorbet_user/script/test +13 -0
  393. data/sub_projects/sorbet_user/sorbet/config +4 -0
  394. data/sub_projects/sorbet_user/sorbet/rbi/gems/.gitattributes +1 -0
  395. data/sub_projects/sorbet_user/sorbet/rbi/gems/ast@2.4.3.rbi +585 -0
  396. data/sub_projects/sorbet_user/sorbet/rbi/gems/benchmark@0.4.0.rbi +618 -0
  397. data/sub_projects/sorbet_user/sorbet/rbi/gems/erubi@1.13.1.rbi +155 -0
  398. data/sub_projects/sorbet_user/sorbet/rbi/gems/json@2.21.2.rbi +2140 -0
  399. data/sub_projects/sorbet_user/sorbet/rbi/gems/language_server-protocol@3.17.0.4.rbi +14244 -0
  400. data/sub_projects/sorbet_user/sorbet/rbi/gems/lint_roller@1.1.0.rbi +239 -0
  401. data/sub_projects/sorbet_user/sorbet/rbi/gems/logger@1.7.0.rbi +963 -0
  402. data/sub_projects/sorbet_user/sorbet/rbi/gems/m@1.6.2.rbi +258 -0
  403. data/sub_projects/sorbet_user/sorbet/rbi/gems/method_source@1.1.0.rbi +9 -0
  404. data/sub_projects/sorbet_user/sorbet/rbi/gems/minitest@5.27.0.rbi +1549 -0
  405. data/sub_projects/sorbet_user/sorbet/rbi/gems/mocktail@3.0.0.rbi +1923 -0
  406. data/sub_projects/sorbet_user/sorbet/rbi/gems/netrc@0.11.0.rbi +158 -0
  407. data/sub_projects/sorbet_user/sorbet/rbi/gems/parallel@1.26.3.rbi +291 -0
  408. data/sub_projects/sorbet_user/sorbet/rbi/gems/parser@3.3.7.4.rbi +5451 -0
  409. data/sub_projects/sorbet_user/sorbet/rbi/gems/prism@1.4.0.rbi +41732 -0
  410. data/sub_projects/sorbet_user/sorbet/rbi/gems/racc@1.8.1.rbi +164 -0
  411. data/sub_projects/sorbet_user/sorbet/rbi/gems/rainbow@3.1.1.rbi +402 -0
  412. data/sub_projects/sorbet_user/sorbet/rbi/gems/rake@13.2.1.rbi +3028 -0
  413. data/sub_projects/sorbet_user/sorbet/rbi/gems/rbi@0.3.1.rbi +6599 -0
  414. data/sub_projects/sorbet_user/sorbet/rbi/gems/rbs@3.9.2.rbi +6976 -0
  415. data/sub_projects/sorbet_user/sorbet/rbi/gems/regexp_parser@2.10.0.rbi +3795 -0
  416. data/sub_projects/sorbet_user/sorbet/rbi/gems/rubocop-ast@1.43.0.rbi +7717 -0
  417. data/sub_projects/sorbet_user/sorbet/rbi/gems/rubocop-performance@1.24.0.rbi +9 -0
  418. data/sub_projects/sorbet_user/sorbet/rbi/gems/rubocop-sorbet@0.9.0.rbi +9 -0
  419. data/sub_projects/sorbet_user/sorbet/rbi/gems/rubocop@1.73.2.rbi +61306 -0
  420. data/sub_projects/sorbet_user/sorbet/rbi/gems/ruby-progressbar@1.13.0.rbi +1317 -0
  421. data/sub_projects/sorbet_user/sorbet/rbi/gems/sorbet-eraser@0.3.1.rbi +8 -0
  422. data/sub_projects/sorbet_user/sorbet/rbi/gems/spoom@1.6.1.rbi +7274 -0
  423. data/sub_projects/sorbet_user/sorbet/rbi/gems/standard-custom@1.0.2.rbi +8 -0
  424. data/sub_projects/sorbet_user/sorbet/rbi/gems/standard-performance@1.7.0.rbi +9 -0
  425. data/sub_projects/sorbet_user/sorbet/rbi/gems/standard-sorbet@0.0.3.rbi +53 -0
  426. data/sub_projects/sorbet_user/sorbet/rbi/gems/standard@1.47.0.rbi +935 -0
  427. data/sub_projects/sorbet_user/sorbet/rbi/gems/tapioca@0.16.11.rbi +3628 -0
  428. data/sub_projects/sorbet_user/sorbet/rbi/gems/thor@1.3.2.rbi +4378 -0
  429. data/sub_projects/sorbet_user/sorbet/rbi/gems/unicode-display_width@3.1.4.rbi +132 -0
  430. data/sub_projects/sorbet_user/sorbet/rbi/gems/unicode-emoji@4.2.0.rbi +254 -0
  431. data/sub_projects/sorbet_user/sorbet/rbi/gems/yard-sorbet@0.9.0.rbi +435 -0
  432. data/sub_projects/sorbet_user/sorbet/rbi/gems/yard@0.9.45.rbi +18916 -0
  433. data/sub_projects/sorbet_user/sorbet/tapioca/config.yml +13 -0
  434. data/sub_projects/sorbet_user/sorbet/tapioca/require.rb +4 -0
  435. data/sub_projects/sorbet_user/test/ensure_type_safety_test.rb +21 -0
  436. data/sub_projects/sorbet_user/test/paint_by_number_test.rb +113 -0
  437. data/sub_projects/sorbet_user/test/sorbet_test.rb +289 -0
  438. data/sub_projects/sorbet_user/test/test_helper.rb +35 -0
  439. data/sub_projects/untyped_user/Gemfile +4 -0
  440. data/sub_projects/untyped_user/Gemfile.lock +25 -0
  441. data/sub_projects/untyped_user/antitype_test.rb +54 -0
  442. data/sub_projects/untyped_user/script/test +5 -0
  443. metadata +392 -9
@@ -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.**
@@ -1,5 +1,7 @@
1
1
  module Mocktail
2
2
  class CollectsCalls
3
+ extend T::Sig
4
+
3
5
  def collect(double, method_name)
4
6
  calls = ExplainsThing.new.explain(double).reference.calls
5
7
 
@@ -1,5 +1,7 @@
1
1
  module Mocktail
2
2
  module Debug
3
+ extend T::Sig
4
+
3
5
  # It would be easy and bad for the mocktail lib to call something like
4
6
  #
5
7
  # double == other_double
@@ -12,26 +14,28 @@ module Mocktail
12
14
  # happens unintentionally. This works in conjunction with the test
13
15
  # MockingMethodfulClassesTest, because it mocks every defined method on the
14
16
  # mocked BasicObject
17
+
15
18
  def self.guard_against_mocktail_accidentally_calling_mocks_if_debugging!
16
19
  return unless ENV["MOCKTAIL_DEBUG_ACCIDENTAL_INTERNAL_MOCK_CALLS"]
17
- raise
18
- rescue => e
20
+ raise Mocktail::Error
21
+ rescue Mocktail::Error => e
19
22
  base_path = Pathname.new(__FILE__).dirname.to_s
20
- backtrace_minus_this_and_whoever_called_this = e.backtrace[2..]
21
- internal_call_sites = backtrace_minus_this_and_whoever_called_this.take_while { |call_site|
22
- # the "in `block" is very confusing but necessary to include lines after
23
+ backtrace_minus_this_and_whoever_called_this = e.backtrace&.[](2..)
24
+ internal_call_sites = backtrace_minus_this_and_whoever_called_this&.take_while { |call_site|
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
23
27
  # a stubs { blah.foo }.with { … } call, since that's when most of the
24
28
  # good stuff happens
25
- call_site.start_with?(base_path) || call_site.include?("in `block")
26
- }.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/) } || []
27
31
 
28
32
  approved_call_sites = [
29
- "fulfills_stubbing.rb:14",
30
- "validates_arguments.rb:16",
31
- "validates_arguments.rb:19"
33
+ /fulfills_stubbing.rb:(16|20)/,
34
+ /validates_arguments.rb:(18|23)/,
35
+ /validates_arguments.rb:(21|26)/
32
36
  ]
33
37
  if internal_call_sites.any? && approved_call_sites.none? { |approved_call_site|
34
- internal_call_sites.first.include?(approved_call_site)
38
+ internal_call_sites.first&.match?(approved_call_site)
35
39
  }
36
40
  raise Error.new <<~MSG
37
41
  Unauthorized internal call of a mock internally by Mocktail itself:
@@ -40,7 +44,7 @@ module Mocktail
40
44
 
41
45
  Offending call's complete stack trace:
42
46
 
43
- #{backtrace_minus_this_and_whoever_called_this.join("\n")}
47
+ #{backtrace_minus_this_and_whoever_called_this&.join("\n")}
44
48
  ==END OFFENDING TRACE==
45
49
  MSG
46
50
  end
data/lib/mocktail/dsl.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  module Mocktail
2
2
  module DSL
3
+ extend T::Sig
4
+
3
5
  def stubs(ignore_block: false, ignore_extra_args: false, ignore_arity: false, times: nil, &demo)
4
6
  RegistersStubbing.new.register(demo, DemoConfig.new(
5
7
  ignore_block: ignore_block,
@@ -12,4 +12,6 @@ module Mocktail
12
12
  class InvalidMatcherError < Error; end
13
13
 
14
14
  class VerificationError < Error; end
15
+
16
+ class TypeCheckingError < Error; end
15
17
  end
@@ -3,6 +3,8 @@ require_relative "share/stringifies_call"
3
3
 
4
4
  module Mocktail
5
5
  class ExplainsNils
6
+ extend T::Sig
7
+
6
8
  def initialize
7
9
  @stringifies_method_name = StringifiesMethodName.new
8
10
  @stringifies_call = StringifiesCall.new
@@ -3,6 +3,8 @@ require_relative "share/stringifies_call"
3
3
 
4
4
  module Mocktail
5
5
  class ExplainsThing
6
+ extend T::Sig
7
+
6
8
  def initialize
7
9
  @stringifies_method_name = StringifiesMethodName.new
8
10
  @stringifies_call = StringifiesCall.new
@@ -11,7 +13,8 @@ module Mocktail
11
13
  def explain(thing)
12
14
  if (double = Mocktail.cabinet.double_for_instance(thing))
13
15
  double_explanation(double)
14
- elsif (type_replacement = TopShelf.instance.type_replacement_if_exists_for(thing))
16
+ elsif (thing.is_a?(Module) || thing.is_a?(Class)) &&
17
+ (type_replacement = TopShelf.instance.type_replacement_if_exists_for(thing))
15
18
  replaced_type_explanation(type_replacement)
16
19
  elsif (fake_method_explanation = fake_method_explanation_for(thing))
17
20
  fake_method_explanation
@@ -67,7 +70,7 @@ module Mocktail
67
70
  def data_for_type_replacement(type_replacement)
68
71
  TypeReplacementData.new(
69
72
  type: type_replacement.type,
70
- replaced_method_names: type_replacement.replacement_methods.map(&:name).sort,
73
+ replaced_method_names: type_replacement.replacement_methods&.map(&:name)&.sort || [],
71
74
  calls: Mocktail.cabinet.calls.select { |call|
72
75
  call.double == type_replacement.type
73
76
  },
@@ -81,7 +84,7 @@ module Mocktail
81
84
  type_replacement_data = data_for_type_replacement(type_replacement)
82
85
 
83
86
  ReplacedTypeExplanation.new(type_replacement_data, <<~MSG)
84
- `#{type_replacement.type}' is a #{type_replacement.type.class.to_s.downcase} that has had its singleton methods faked.
87
+ `#{type_replacement.type}' is a #{type_replacement.type.class.to_s.downcase} that has had its methods faked.
85
88
 
86
89
  It has these mocked methods:
87
90
  #{type_replacement_data.replaced_method_names.map { |method| " - #{method}" }.join("\n")}
@@ -116,7 +119,7 @@ module Mocktail
116
119
  end
117
120
 
118
121
  def no_explanation(thing)
119
- NoExplanation.new(thing,
122
+ NoExplanation.new(NoExplanationData.new(thing: thing),
120
123
  "Unfortunately, Mocktail doesn't know what this thing is: #{thing.inspect}")
121
124
  end
122
125
  end
@@ -0,0 +1,30 @@
1
+ module Mocktail
2
+ class GrabsOriginalMethodParameters
3
+ extend T::Sig
4
+
5
+ # Sorbet wraps the original method in a sig wrapper, so we need to unwrap it.
6
+ # The value returned from `owner.instance_method(method_name)` does not have
7
+ # the real parameters values available, as they'll have been erased
8
+ #
9
+ # If the method isn't wrapped by Sorbet, this will return the #instance_method,
10
+ # per usual
11
+
12
+ def grab(method)
13
+ return [] unless method
14
+
15
+ if (wrapped_method = sorbet_wrapped_method(method))
16
+ wrapped_method.parameters
17
+ else
18
+ method.parameters
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def sorbet_wrapped_method(method)
25
+ return unless defined?(::T::Private::Methods)
26
+
27
+ T::Private::Methods.signature_for_method(method)
28
+ end
29
+ end
30
+ end
@@ -3,6 +3,8 @@ require_relative "../../share/bind"
3
3
 
4
4
  module Mocktail
5
5
  class DescribesUnsatisfiedStubbing
6
+ extend T::Sig
7
+
6
8
  def initialize
7
9
  @cleans_backtrace = CleansBacktrace.new
8
10
  end
@@ -14,7 +16,7 @@ module Mocktail
14
16
  Bind.call(dry_call.double, :==, stubbing.recording.double) &&
15
17
  dry_call.method == stubbing.recording.method
16
18
  },
17
- backtrace: @cleans_backtrace.clean(Error.new).backtrace
19
+ backtrace: @cleans_backtrace.clean(Error.new).backtrace || []
18
20
  )
19
21
  end
20
22
  end
@@ -2,14 +2,18 @@ require_relative "../../share/determines_matching_calls"
2
2
 
3
3
  module Mocktail
4
4
  class FindsSatisfaction
5
+ extend T::Sig
6
+
5
7
  def initialize
6
8
  @determines_matching_calls = DeterminesMatchingCalls.new
7
9
  end
8
10
 
9
11
  def find(dry_call)
10
12
  Mocktail.cabinet.stubbings.reverse.find { |stubbing|
13
+ demo_config_times = stubbing.demo_config.times
14
+
11
15
  @determines_matching_calls.determine(dry_call, stubbing.recording, stubbing.demo_config) &&
12
- (stubbing.demo_config.times.nil? || stubbing.demo_config.times > stubbing.satisfaction_count)
16
+ (demo_config_times.nil? || demo_config_times > stubbing.satisfaction_count)
13
17
  }
14
18
  end
15
19
  end