├── .credo.exs ├── .formatter.exs ├── .github └── workflows │ └── main.yml ├── .gitignore ├── LICENSE ├── README.md ├── lib ├── mimic.ex └── mimic │ ├── application.ex │ ├── cover.ex │ ├── dsl.ex │ ├── error.ex │ ├── module.ex │ ├── server.ex │ ├── type_check.ex │ ├── unexpected_call_error.ex │ └── verification_error.ex ├── logo.png ├── mix.exs ├── mix.lock └── test ├── dsl_test.exs ├── edge_case_test.exs ├── mimic └── type_check_test.exs ├── mimic_test.exs ├── mimic_type_check_test.exs ├── support ├── test_cover.ex └── test_modules.ex └── test_helper.exs /.credo.exs: -------------------------------------------------------------------------------- 1 | # This file contains the configuration for Credo and you are probably reading 2 | # this after creating it with `mix credo.gen.config`. 3 | # 4 | # If you find anything wrong or unclear in this file, please report an 5 | # issue on GitHub: https://github.com/rrrene/credo/issues 6 | # 7 | %{ 8 | # 9 | # You can have as many configs as you like in the `configs:` field. 10 | configs: [ 11 | %{ 12 | # 13 | # Run any config using `mix credo -C `. If no config name is given 14 | # "default" is used. 15 | # 16 | name: "default", 17 | # 18 | # These are the files included in the analysis: 19 | files: %{ 20 | # 21 | # You can give explicit globs or simply directories. 22 | # In the latter case `**/*.{ex,exs}` will be used. 23 | # 24 | included: [ 25 | "lib/", 26 | "src/", 27 | "test/", 28 | "web/", 29 | "apps/*/lib/", 30 | "apps/*/src/", 31 | "apps/*/test/", 32 | "apps/*/web/" 33 | ], 34 | excluded: [~r"/_build/", ~r"/deps/", ~r"/node_modules/"] 35 | }, 36 | # 37 | # Load and configure plugins here: 38 | # 39 | plugins: [], 40 | # 41 | # If you create your own checks, you must specify the source files for 42 | # them here, so they can be loaded by Credo before running the analysis. 43 | # 44 | requires: [], 45 | # 46 | # If you want to enforce a style guide and need a more traditional linting 47 | # experience, you can change `strict` to `true` below: 48 | # 49 | strict: false, 50 | # 51 | # To modify the timeout for parsing files, change this value: 52 | # 53 | parse_timeout: 5000, 54 | # 55 | # If you want to use uncolored output by default, you can change `color` 56 | # to `false` below: 57 | # 58 | color: true, 59 | # 60 | # You can customize the parameters of any check by adding a second element 61 | # to the tuple. 62 | # 63 | # To disable a check put `false` as second element: 64 | # 65 | # {Credo.Check.Design.DuplicatedCode, false} 66 | # 67 | checks: %{ 68 | enabled: [ 69 | # 70 | ## Consistency Checks 71 | # 72 | {Credo.Check.Consistency.ExceptionNames, []}, 73 | {Credo.Check.Consistency.LineEndings, []}, 74 | {Credo.Check.Consistency.ParameterPatternMatching, []}, 75 | {Credo.Check.Consistency.SpaceAroundOperators, []}, 76 | {Credo.Check.Consistency.SpaceInParentheses, []}, 77 | {Credo.Check.Consistency.TabsOrSpaces, []}, 78 | 79 | # 80 | ## Design Checks 81 | # 82 | # You can customize the priority of any check 83 | # Priority values are: `low, normal, high, higher` 84 | # 85 | {Credo.Check.Design.AliasUsage, 86 | [priority: :low, if_nested_deeper_than: 2, if_called_more_often_than: 0]}, 87 | # You can also customize the exit_status of each check. 88 | # If you don't want TODO comments to cause `mix credo` to fail, just 89 | # set this value to 0 (zero). 90 | # 91 | {Credo.Check.Design.TagTODO, [exit_status: 2]}, 92 | {Credo.Check.Design.TagFIXME, []}, 93 | 94 | # 95 | ## Readability Checks 96 | # 97 | {Credo.Check.Readability.AliasOrder, []}, 98 | {Credo.Check.Readability.FunctionNames, []}, 99 | {Credo.Check.Readability.LargeNumbers, []}, 100 | {Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 120]}, 101 | {Credo.Check.Readability.ModuleAttributeNames, []}, 102 | {Credo.Check.Readability.ModuleDoc, []}, 103 | {Credo.Check.Readability.ModuleNames, []}, 104 | {Credo.Check.Readability.ParenthesesInCondition, []}, 105 | {Credo.Check.Readability.ParenthesesOnZeroArityDefs, []}, 106 | {Credo.Check.Readability.PipeIntoAnonymousFunctions, []}, 107 | {Credo.Check.Readability.PredicateFunctionNames, []}, 108 | {Credo.Check.Readability.PreferImplicitTry, []}, 109 | {Credo.Check.Readability.RedundantBlankLines, []}, 110 | {Credo.Check.Readability.Semicolons, []}, 111 | {Credo.Check.Readability.SpaceAfterCommas, []}, 112 | {Credo.Check.Readability.StringSigils, []}, 113 | {Credo.Check.Readability.TrailingBlankLine, []}, 114 | {Credo.Check.Readability.TrailingWhiteSpace, []}, 115 | {Credo.Check.Readability.UnnecessaryAliasExpansion, []}, 116 | {Credo.Check.Readability.VariableNames, []}, 117 | {Credo.Check.Readability.WithSingleClause, []}, 118 | 119 | # 120 | ## Refactoring Opportunities 121 | # 122 | {Credo.Check.Refactor.Apply, false}, 123 | {Credo.Check.Refactor.CondStatements, []}, 124 | {Credo.Check.Refactor.CyclomaticComplexity, []}, 125 | {Credo.Check.Refactor.FunctionArity, []}, 126 | {Credo.Check.Refactor.LongQuoteBlocks, []}, 127 | {Credo.Check.Refactor.MatchInCondition, []}, 128 | {Credo.Check.Refactor.MapJoin, []}, 129 | {Credo.Check.Refactor.NegatedConditionsInUnless, []}, 130 | {Credo.Check.Refactor.NegatedConditionsWithElse, []}, 131 | {Credo.Check.Refactor.Nesting, []}, 132 | {Credo.Check.Refactor.UnlessWithElse, []}, 133 | {Credo.Check.Refactor.WithClauses, []}, 134 | {Credo.Check.Refactor.FilterFilter, []}, 135 | {Credo.Check.Refactor.RejectReject, []}, 136 | {Credo.Check.Refactor.RedundantWithClauseResult, []}, 137 | 138 | # 139 | ## Warnings 140 | # 141 | {Credo.Check.Warning.ApplicationConfigInModuleAttribute, []}, 142 | {Credo.Check.Warning.BoolOperationOnSameValues, []}, 143 | {Credo.Check.Warning.ExpensiveEmptyEnumCheck, []}, 144 | {Credo.Check.Warning.IExPry, []}, 145 | {Credo.Check.Warning.IoInspect, []}, 146 | {Credo.Check.Warning.OperationOnSameValues, []}, 147 | {Credo.Check.Warning.OperationWithConstantResult, []}, 148 | {Credo.Check.Warning.RaiseInsideRescue, []}, 149 | {Credo.Check.Warning.SpecWithStruct, []}, 150 | {Credo.Check.Warning.WrongTestFileExtension, []}, 151 | {Credo.Check.Warning.UnusedEnumOperation, []}, 152 | {Credo.Check.Warning.UnusedFileOperation, []}, 153 | {Credo.Check.Warning.UnusedKeywordOperation, []}, 154 | {Credo.Check.Warning.UnusedListOperation, []}, 155 | {Credo.Check.Warning.UnusedPathOperation, []}, 156 | {Credo.Check.Warning.UnusedRegexOperation, []}, 157 | {Credo.Check.Warning.UnusedStringOperation, []}, 158 | {Credo.Check.Warning.UnusedTupleOperation, []}, 159 | {Credo.Check.Warning.UnsafeExec, []} 160 | ], 161 | disabled: [ 162 | # 163 | # Checks scheduled for next check update (opt-in for now, just replace `false` with `[]`) 164 | 165 | # 166 | # Controversial and experimental checks (opt-in, just move the check to `:enabled` 167 | # and be sure to use `mix credo --strict` to see low priority checks) 168 | # 169 | {Credo.Check.Consistency.MultiAliasImportRequireUse, []}, 170 | {Credo.Check.Consistency.UnusedVariableNames, []}, 171 | {Credo.Check.Design.DuplicatedCode, []}, 172 | {Credo.Check.Design.SkipTestWithoutComment, []}, 173 | {Credo.Check.Readability.AliasAs, []}, 174 | {Credo.Check.Readability.BlockPipe, []}, 175 | {Credo.Check.Readability.ImplTrue, []}, 176 | {Credo.Check.Readability.MultiAlias, []}, 177 | {Credo.Check.Readability.NestedFunctionCalls, []}, 178 | {Credo.Check.Readability.SeparateAliasRequire, []}, 179 | {Credo.Check.Readability.SingleFunctionToBlockPipe, []}, 180 | {Credo.Check.Readability.SinglePipe, []}, 181 | {Credo.Check.Readability.Specs, []}, 182 | {Credo.Check.Readability.StrictModuleLayout, []}, 183 | {Credo.Check.Readability.WithCustomTaggedTuple, []}, 184 | {Credo.Check.Refactor.ABCSize, []}, 185 | {Credo.Check.Refactor.AppendSingleItem, []}, 186 | {Credo.Check.Refactor.DoubleBooleanNegation, []}, 187 | {Credo.Check.Refactor.FilterReject, []}, 188 | {Credo.Check.Refactor.IoPuts, []}, 189 | {Credo.Check.Refactor.MapMap, []}, 190 | {Credo.Check.Refactor.ModuleDependencies, []}, 191 | {Credo.Check.Refactor.NegatedIsNil, []}, 192 | {Credo.Check.Refactor.PipeChainStart, []}, 193 | {Credo.Check.Refactor.RejectFilter, []}, 194 | {Credo.Check.Refactor.VariableRebinding, []}, 195 | {Credo.Check.Warning.LazyLogging, []}, 196 | {Credo.Check.Warning.LeakyEnvironment, []}, 197 | {Credo.Check.Warning.MapGetUnsafePass, []}, 198 | {Credo.Check.Warning.MixEnv, []}, 199 | {Credo.Check.Warning.UnsafeToAtom, []} 200 | 201 | # {Credo.Check.Refactor.MapInto, []}, 202 | 203 | # 204 | # Custom checks can be created using `mix credo.gen.check`. 205 | # 206 | ] 207 | } 208 | } 209 | ] 210 | } 211 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"], 4 | locals_without_parens: [allow: :*, expect: :*], 5 | export: [ 6 | locals_without_parens: [allow: :*, expect: :*] 7 | ] 8 | ] 9 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | format: 7 | name: Format & credo 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2.3.1 11 | 12 | - name: Install OTP and Elixir 13 | uses: erlef/setup-beam@v1 14 | with: 15 | otp-version: 26.x 16 | elixir-version: 1.16.x 17 | 18 | - name: Install dependencies 19 | run: mix deps.get 20 | 21 | - name: Compile with --warnings-as-errors 22 | run: mix compile --warnings-as-errors 23 | 24 | - name: Run "mix format" 25 | run: mix format --check-formatted 26 | 27 | - name: Credo 28 | run: mix credo --strict 29 | 30 | test: 31 | name: Test (Elixir ${{matrix.elixir}} | Erlang/OTP ${{matrix.otp}}) 32 | runs-on: ubuntu-latest 33 | strategy: 34 | fail-fast: false 35 | matrix: 36 | include: 37 | - otp: 26.x 38 | elixir: 1.16.x 39 | coverage: true 40 | - otp: 27.x 41 | elixir: 1.17.x 42 | - otp: 27.x 43 | elixir: 1.18.x 44 | env: 45 | MIX_ENV: test 46 | steps: 47 | - uses: actions/checkout@v2.3.1 48 | 49 | - name: Install OTP and Elixir 50 | uses: erlef/setup-beam@v1 51 | with: 52 | otp-version: ${{matrix.otp}} 53 | elixir-version: ${{matrix.elixir}} 54 | 55 | - name: Install dependencies 56 | run: mix deps.get --only test 57 | 58 | - name: Run tests 59 | run: mix test --trace 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | mimic-*.tar 24 | 25 | # Temporary files for e.g. tests. 26 | /tmp/ 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2012 Plataformatec 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Mimic logo](logo.png) 2 | # Mimic 3 | 4 | [![CI](https://github.com/edgurgel/mimic/actions/workflows/main.yml/badge.svg)](https://github.com/edgurgel/mimic/actions/workflows/main.yml) 5 | [![Module Version](https://img.shields.io/hexpm/v/mimic.svg)](https://hex.pm/packages/mimic) 6 | [![Hex Docs](https://img.shields.io/badge/hex-docs-lightgreen.svg)](https://hexdocs.pm/mimic/) 7 | [![Total Download](https://img.shields.io/hexpm/dt/mimic.svg)](https://hex.pm/packages/mimic) 8 | [![License](https://img.shields.io/hexpm/l/mimic.svg)](https://github.com/edgurgel/mimic/blob/master/LICENSE) 9 | [![Last Updated](https://img.shields.io/github/last-commit/edgurgel/mimic.svg)](https://github.com/edgurgel/mimic/commits/master) 10 | 11 | A sane way of using mocks in Elixir. It borrows a lot from both Meck & Mox! Thanks [@eproxus](https://twitter.com/eproxus) & [@josevalim](https://twitter.com/josevalim). 12 | 13 | ## Installation 14 | 15 | Just add `:mimic` to your list of dependencies in `mix.exs`: 16 | 17 | ```elixir 18 | def deps do 19 | [ 20 | {:mimic, "~> 1.12", only: :test} 21 | ] 22 | end 23 | ``` 24 | 25 | If `:applications` key is defined inside your `mix.exs` or you run `mix test --no-start`, you probably want to add `Application.ensure_all_started(:mimic)` in your `test_helper.exs` 26 | 27 | ## Using 28 | 29 | Modules need to be prepared so that they can be used. 30 | 31 | You must first call `copy` in your `test_helper.exs` for 32 | each module that may have the behaviour changed. 33 | 34 | ```elixir 35 | Mimic.copy(Calculator) 36 | 37 | ExUnit.start() 38 | ``` 39 | 40 | Calling `copy` will not change the behaviour of the module. 41 | 42 | The user must call `stub/1`, `stub/3`, `expect/4` or `reject/1` so that the functions can 43 | behave differently. 44 | 45 | Then for the actual tests one could use it like this: 46 | 47 | ```elixir 48 | use ExUnit.Case, async: true 49 | use Mimic 50 | 51 | test "invokes mult once and add twice" do 52 | Calculator 53 | |> stub(:add, fn x, y -> :stub end) 54 | |> expect(:add, fn x, y -> x + y end) 55 | |> expect(:mult, 2, fn x, y -> x * y end) 56 | 57 | assert Calculator.add(2, 3) == 5 58 | assert Calculator.mult(2, 3) == 6 59 | 60 | assert Calculator.add(2, 3) == :stub 61 | end 62 | ``` 63 | 64 | ## Stub, Expect and Reject 65 | 66 | ### Stub 67 | 68 | `stub/1` will change every module function to throw an exception if called. 69 | 70 | ```elixir 71 | stub(Calculator) 72 | 73 | ** (Mimic.UnexpectedCallError) Stub! Unexpected call to Calculator.add(3, 7) from #PID<0.187.0> 74 | code: assert Calculator.add(3, 7) == 10 75 | ``` 76 | 77 | `stub/3` changes a specific function to behave differently. If the function is not called no verification error will happen. 78 | 79 | ### Expect 80 | 81 | `expect/4` changes a specific function and it works like a queue of operations. It has precedence over stubs and if not called a verification error will be thrown. 82 | 83 | If the same function is called with `expect/4` the order will be respected: 84 | 85 | ```elixir 86 | Calculator 87 | |> stub(:add, fn _x, _y -> :stub end) 88 | |> expect(:add, fn _, _ -> :expected_1 end) 89 | |> expect(:add, fn _, _ -> :expected_2 end) 90 | 91 | assert Calculator.add(1, 1) == :expected_1 92 | assert Calculator.add(1, 1) == :expected_2 93 | assert Calculator.add(1, 1) == :stub 94 | ``` 95 | 96 | `expect/4` has an optional parameter which is the amount of calls expected: 97 | 98 | ```elixir 99 | Calculator 100 | |> expect(:add, 2, fn x, y -> {:add, x, y} end) 101 | 102 | assert Calculator.add(1, 3) == {:add, 1, 3} 103 | assert Calculator.add(4, 5) == {:add, 4, 5} 104 | ``` 105 | 106 | With `use Mimic`, verification `expect/4` function call of is done automatically on test case end. `verify!/1` can be used in case custom verification timing required: 107 | 108 | ```elixir 109 | Calculator 110 | |> expect(:add, 2, fn x, y -> {:add, x, y} end) 111 | 112 | # Will raise error because Calculator.add is not called 113 | # ** (Mimic.VerificationError) error while verifying mocks for #PID<0.3182.0>: 114 | # * expected Calculator.add/2 to be invoked 1 time(s) but it has been called 0 time(s) 115 | verify!() 116 | ``` 117 | 118 | Using `expect/4` on intra-module functions will not work, unless the function is referenced by it's fully qualified name. 119 | 120 | ```elixir 121 | defmodule Calculator do 122 | def mult(x, y) do 123 | x * y 124 | end 125 | 126 | def negation(x) do 127 | mult(x, -1) 128 | end 129 | end 130 | 131 | Calculator 132 | |> expect(:mult, fn x, y -> x + y end) 133 | 134 | assert Calculator.negation(5) == -5 135 | 136 | # Will raise error because because BEAM optimises this case and jumps directly to the appropriate bytecode. 137 | # ** (Mimic.VerificationError) error while verifying mocks for #PID<0.207.0>: 138 | # * expected Calculator.mult/2 to be invoked 1 time(s) but it has been called 0 time(s) 139 | verify!() 140 | ``` 141 | 142 | To ensure that the stubbed Mimic function is called, it can be referenced by `Calculator.mult/2` instead of `mult/2`. 143 | 144 | ### Reject 145 | 146 | One may want to reject calls to a specific function. `reject/1` can be used to achieved this behaviour. 147 | 148 | ```elixir 149 | reject(&Calculator.add/2) 150 | assert_raise Mimic.UnexpectedCallError, fn -> Calculator.add(4, 2) end 151 | ``` 152 | 153 | ### Calls 154 | 155 | `calls/3` returns a list of args for each call to a stubbed Mimic function. 156 | 157 | ```elixir 158 | defmodule Calculator do 159 | def mult(x, y) do 160 | x * y 161 | end 162 | end 163 | 164 | Calculator 165 | |> expect(:mult, fn x, y -> x + y end) 166 | 167 | [] = calls(Calculator, :mult, 2) 168 | 169 | 9 = Calculator.mult(3, 3) 170 | 171 | [[3, 3]] = calls(Calculator, :mult, 2) 172 | ``` 173 | 174 | `calls/1` works the same way, but with a capture of the function: 175 | 176 | ```elixir 177 | defmodule Calculator do 178 | def mult(x, y) do 179 | x * y 180 | end 181 | end 182 | 183 | Calculator 184 | |> expect(:mult, fn x, y -> x + y end) 185 | 186 | [] = calls(&Calculator.mult/2) 187 | 188 | 9 = Calculator.mult(3, 3) 189 | 190 | [[3, 3]] = calls(&Calculator.mult/2) 191 | ``` 192 | 193 | When `calls` is called they are popped out of the list of calls. Next time `calls` is used it will only 194 | return new calls since the last time that `calls` was used. 195 | 196 | ## Private and Global mode 197 | 198 | The default mode is private which means that only the process 199 | and explicitly allowed process will see the different behaviour. 200 | 201 | Calling `allow/2` will permit a different pid to call the stubs and expects from the original process. 202 | 203 | If you are using `Task` there is no need to use global mode as Tasks can see the same expectations and stubs from the calling process. 204 | 205 | Global mode can be used with `set_mimic_global` like this: 206 | 207 | ```elixir 208 | setup :set_mimic_global 209 | 210 | test "invokes add and mult" do 211 | Calculator 212 | |> expect(:add, fn x, y -> x + y end) 213 | |> expect(:mult, fn x, y -> x * y end) 214 | 215 | parent_pid = self() 216 | 217 | spawn_link(fn -> 218 | assert Calculator.add(2, 3) == 5 219 | assert Calculator.mult(2, 3) == 6 220 | 221 | send parent_pid, :ok 222 | end) 223 | 224 | assert_receive :ok 225 | end 226 | ``` 227 | 228 | This means that all processes will get the same behaviour 229 | defined with expect & stub. This option is simpler but tests running 230 | concurrently will have undefined behaviour. It is important to run with `async: false`. 231 | One could use `:set_mimic_from_context` instead of using `:set_mimic_global` or `:set_mimic_private`. It will be private if `async: true`, global otherwise. 232 | 233 | ## DSL Mode 234 | To use DSL Mode `use Mimic.DSL` rather than `use Mimic` in your test. DSL Mode enables a more expressive api to the Mimic functionality. 235 | 236 | ```elixir 237 | use Mimic.DSL 238 | 239 | test "basic example" do 240 | stub Calculator.add(_x, _y), do: :stub 241 | expect Calculator.add(x, y), do: x + y 242 | expect Calculator.mult(x, y), do: x * y 243 | 244 | assert Calculator.add(2, 3) == 5 245 | assert Calculator.mult(2, 3) == 6 246 | 247 | assert Calculator.add(2, 3) == :stub 248 | end 249 | ``` 250 | 251 | ## Stubs with fake module 252 | `stub_with/2` enable substitute function call of a module with another similar module. 253 | 254 | ```elixir 255 | defmodule BadCalculator do 256 | def add(x, y), do: x*y 257 | def mult(x, y), do: x+y 258 | end 259 | 260 | test "basic example" do 261 | stub_with(Calculator, BadCalculator) 262 | 263 | assert Calculator.add(2, 3) == 6 264 | assert Calculator.mult(2, 3) == 5 265 | end 266 | ``` 267 | 268 | ## Calling the original 269 | `call_original/3` allows to call original unmocked version of the function. 270 | 271 | ```elixir 272 | setup :set_mimic_private 273 | 274 | test "calls original function even if it has been is stubbed" do 275 | stub_with(Calculator, InverseCalculator) 276 | 277 | assert call_original(Calculator, :add, [1, 2]) == 3 278 | end 279 | ``` 280 | 281 | ## Experimental type checking for copied modules 282 | 283 | One can pass `type_check: true` when a module is copied to also get the function expected/stubbed to 284 | validate the arguments and return value using [Ham](https://github.com/edgurgel/ham) which is essentially 285 | what [Hammox](https://github.com/msz/hammox) improved on Mox. 286 | 287 | ```elixir 288 | Mimic.copy(:cowboy_req, type_check: true) 289 | ``` 290 | 291 | If there is any problem with the arguments or return values of the stubbed functions on your tests you might see 292 | an error like this one: 293 | 294 | ```elixir 295 | ** (Mimic.TypeCheckError) :cowboy_req.parse_qs/1: 1st argument value %{} does not match 1st parameter's type :cowboy_req.req(). 296 | Could not find a map entry matching required(:method) => binary(). 297 | ``` 298 | 299 | This feature is experimental at the moment which means that it might change a little bit how this 300 | is configured and used. Feedback is welcome! 301 | 302 | ## Implementation Details & Performance 303 | 304 | After calling `Mimic.copy(MyModule)`, calls to functions belonging to this module will first go through an ETS table to check which pid sees what (stubs, expects or call original). 305 | 306 | It is really fast but it won't be as fast as calling a no-op function. Here's a very simple benchmark: 307 | 308 | ```elixir 309 | defmodule Enumerator do 310 | def to_list(x, y), do: Enum.to_list(x..y) 311 | end 312 | ``` 313 | 314 | Benchmarking `Enumerator.to_list(1, 100)` : 315 | 316 | ``` 317 | Name ips average deviation median 99th % 318 | mimic 116.00 K 8.62 μs ±729.13% 5 μs 29 μs 319 | original 19.55 K 51.15 μs ±302.46% 34 μs 264 μs 320 | 321 | Comparison: 322 | mimic 116.00 K 323 | original 19.55 K - 5.93x slower 324 | ``` 325 | 326 | Benchmarking `Enumerator.to_list(1, 250)` : 327 | 328 | ``` 329 | Name ips average deviation median 99th % 330 | original 131.49 K 7.61 μs ±167.90% 7 μs 16 μs 331 | mimic 105.47 K 9.48 μs ±145.21% 9 μs 27 μs 332 | 333 | Comparison: 334 | original 131.49 K 335 | mimic 105.47 K - 1.25x slower 336 | ``` 337 | 338 | There's a small fixed price to pay when mimic is used but it is unnoticeable for tests purposes. 339 | 340 | ## Acknowledgements 341 | 342 | Thanks to [@jimsynz](https://github.com/jimsynz) and [@alissonsales](http://github.com/alissonsales) for all the help! :tada: 343 | 344 | Thanks to [@mendokusai](https://github.com/mendokusai) for the nice logo! 345 | 346 | ## Copyright and License 347 | 348 | Copyright (c) 2016 Eduardo Gurgel 349 | 350 | Licensed under the Apache License, Version 2.0 (the "License"); 351 | you may not use this file except in compliance with the License. 352 | You may obtain a copy of the License at [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) 353 | 354 | Unless required by applicable law or agreed to in writing, software 355 | distributed under the License is distributed on an "AS IS" BASIS, 356 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 357 | See the License for the specific language governing permissions and 358 | limitations under the License. 359 | -------------------------------------------------------------------------------- /lib/mimic.ex: -------------------------------------------------------------------------------- 1 | defmodule Mimic do 2 | @moduledoc """ 3 | Mimic is a library that simplifies the usage of mocks in Elixir. 4 | 5 | Mimic is mostly API compatible with [mox](https://hex.pm/packages/mox) but 6 | doesn't require explicit contract checking with behaviours. It's also faster. 7 | You're welcome. 8 | 9 | Mimic works by copying your module out of the way and replacing it with one of 10 | it's own which can delegate calls back to the original or to a mock function 11 | as required. 12 | 13 | In order to prepare a module for mocking you must call `copy/1` with the 14 | module as an argument. We suggest that you do this in your 15 | `test/test_helper.exs`: 16 | 17 | ```elixir 18 | Mimic.copy(Calculator) 19 | ExUnit.start() 20 | ``` 21 | 22 | Importantly calling `copy/1` will not change the behaviour of the module. When 23 | writing tests you can then use `stub/3` or `expect/3` to add mocks and 24 | assertions. 25 | 26 | ## Multi-process collaboration 27 | 28 | Mimic supports multi-process collaboration via two mechanisms: 29 | 30 | 1. Explicit allows. 31 | 2. Global mode. 32 | 33 | Using explicit allows is generally preferred as these stubs can be run 34 | concurrently, whereas global mode tests must be run exclusively. 35 | 36 | ## Explicit allows 37 | 38 | Using `allow/3` you can give other processes permission to use stubs and 39 | expectations from where they were not defined. 40 | 41 | ```elixir 42 | test "invokes add from a process" do 43 | Calculator 44 | |> expect(:add, fn x, y -> x + y end) 45 | 46 | parent_pid = self() 47 | 48 | spawn_link(fn -> 49 | Calculator |> allow(parent_pid, self()) 50 | assert Calculator.add(2, 3) == 5 51 | 52 | send parent_pid, :ok 53 | end) 54 | 55 | assert_receive :ok 56 | end 57 | ``` 58 | 59 | If you are using `Task` the expectations and stubs are automatically allowed 60 | 61 | ## Global mode 62 | 63 | When set in global mode any process is able to call the stubs and expectations 64 | defined in your tests. 65 | 66 | **Warning: If using global mode you should remove `async: true` from your tests** 67 | 68 | Enable global mode using `set_mimic_global/1`. 69 | 70 | ```elixir 71 | setup :set_mimic_global 72 | setup :verify_on_exit! 73 | 74 | test "invokes add from a task" do 75 | Calculator 76 | |> expect(:add, fn x, y -> x + y end) 77 | 78 | Task.async(fn -> 79 | assert Calculator.add(2, 3) == 5 80 | end) 81 | |> Task.await 82 | end 83 | ``` 84 | """ 85 | alias ExUnit.Callbacks 86 | alias Mimic.{Server, VerificationError} 87 | 88 | @doc false 89 | defmacro __using__(_opts \\ []) do 90 | quote do 91 | import Mimic 92 | setup :verify_on_exit! 93 | end 94 | end 95 | 96 | @doc """ 97 | Define a stub function for a copied module. 98 | 99 | ## Arguments: 100 | 101 | * `module` - the name of the module in which we're adding the stub. 102 | * `function_name` - the name of the function we're stubbing. 103 | * `function` - the function to use as a replacement. 104 | 105 | ## Raises: 106 | 107 | * If `module` is not copied. 108 | * If `function_name` is not publicly exported from `module` with the same arity. 109 | 110 | ## Example 111 | 112 | iex> Calculator.add(2, 4) 113 | 6 114 | 115 | iex> Mimic.stub(Calculator, :add, fn x, y -> x * y end) 116 | ...> Calculator.add(2, 4) 117 | 8 118 | 119 | """ 120 | @spec stub(module(), atom(), function()) :: module 121 | def stub(module, function_name, function) do 122 | arity = Function.info(function)[:arity] 123 | raise_if_not_exported_function!(module, function_name, arity) 124 | 125 | module 126 | |> Server.stub(function_name, arity, function) 127 | |> validate_server_response(:stub) 128 | end 129 | 130 | @doc """ 131 | Replace all public functions in `module` with stubs. 132 | 133 | The stubbed functions will raise if they are called. 134 | 135 | ## Arguments: 136 | 137 | * `module` - The name of the module to stub. 138 | 139 | ## Raises: 140 | 141 | * If `module` is not copied. 142 | * If `function` is not called by the stubbing process. 143 | 144 | ## Example 145 | 146 | iex> Mimic.stub(Calculator) 147 | ...> Calculator.add(2, 4) 148 | ** (ArgumentError) Module Calculator has not been copied. See docs for Mimic.copy/1 149 | 150 | """ 151 | @spec stub(module()) :: module() 152 | def stub(module) do 153 | module 154 | |> Server.stub() 155 | |> validate_server_response(:stub) 156 | end 157 | 158 | @doc """ 159 | Replace all public functions in `module` with public function in `mocking_module`. 160 | 161 | If there's any public function in `module` that are not in `mocking_module`, it will raise if it's called 162 | 163 | ## Arguments: 164 | 165 | * `module` - The name of the module to stub. 166 | * `mocking_module` - The name of the mocking module to stub the original module. 167 | 168 | ## Raises: 169 | 170 | * If `module` is not copied. 171 | * If `function` is not called by the stubbing process. 172 | 173 | ## Example 174 | defmodule InverseCalculator do 175 | def add(a, b), do: a - b 176 | end 177 | 178 | iex> Mimic.stub_with(Calculator, InverseCalculator) 179 | ...> Calculator.add(2, 4) 180 | ...> -2 181 | 182 | """ 183 | @spec stub_with(module(), module()) :: module() 184 | def stub_with(module, mocking_module) do 185 | raise_if_module_not_defined!(mocking_module) 186 | 187 | module 188 | |> Server.stub_with(mocking_module) 189 | |> validate_server_response(:stub) 190 | end 191 | 192 | @doc """ 193 | Define a stub which must be called within an example. 194 | 195 | This function is almost identical to `stub/3` except that the replacement 196 | function must be called within the lifetime of the calling `pid` (i.e. the 197 | test example). 198 | 199 | ## Arguments: 200 | 201 | * `module` - the name of the module in which we're adding the stub. 202 | * `function_name` - the name of the function we're stubbing. 203 | * `function` - the function to use as a replacement. 204 | 205 | ## Raises: 206 | 207 | * If `module` is not copied. 208 | * If `function_name` is not publicly exported from `module` with the same 209 | arity. 210 | * If `function` is not called by the stubbing process. 211 | 212 | ## Example 213 | 214 | iex> Calculator.add(2, 4) 215 | 6 216 | 217 | iex> Mimic.expect(Calculator, :add, fn x, y -> x * y end) 218 | ...> Calculator.add(2, 4) 219 | 8 220 | """ 221 | @spec expect(atom, atom, non_neg_integer, function) :: module 222 | def expect(module, fn_name, num_calls \\ 1, func) 223 | 224 | def expect(_module, _fn_name, 0, _func) do 225 | raise ArgumentError, "Expecting 0 calls should be done through Mimic.reject/1" 226 | end 227 | 228 | def expect(module, fn_name, num_calls, func) 229 | when is_atom(module) and is_atom(fn_name) and is_integer(num_calls) and num_calls >= 1 and 230 | is_function(func) do 231 | arity = Function.info(func)[:arity] 232 | raise_if_not_exported_function!(module, fn_name, arity) 233 | 234 | module 235 | |> Server.expect(fn_name, arity, num_calls, func) 236 | |> validate_server_response(:expect) 237 | end 238 | 239 | @doc """ 240 | Define a stub which must not be called. 241 | 242 | This function allows you do define a stub which must not be called during the 243 | course of this test. If it is called then the verification step will raise. 244 | 245 | ## Arguments: 246 | 247 | * `function` - A capture of the function which must not be called. 248 | 249 | ## Raises: 250 | 251 | * If `function` is not called by the stubbing process while calling `verify!/1`. 252 | 253 | ## Example: 254 | 255 | iex> Mimic.reject(&Calculator.add/2) 256 | Calculator 257 | 258 | """ 259 | @spec reject(function) :: module 260 | def reject(function) when is_function(function) do 261 | fun_info = Function.info(function) 262 | arity = fun_info[:arity] 263 | module = fun_info[:module] 264 | fn_name = fun_info[:name] 265 | raise_if_not_exported_function!(module, fn_name, arity) 266 | 267 | module 268 | |> Server.expect(fn_name, arity, 0, function) 269 | |> validate_server_response(:reject) 270 | end 271 | 272 | @doc """ 273 | Define a stub which must not be called. 274 | 275 | This function allows you do define a stub which must not be called during the 276 | course of this test. If it is called then the verification step will raise. 277 | 278 | ## Arguments: 279 | 280 | * `module` - the name of the module in which we're adding the stub. 281 | * `function_name` - the name of the function we're stubbing. 282 | * `arity` - the arity of the function we're stubbing. 283 | 284 | ## Raises: 285 | 286 | * If `function` is not called by the stubbing process while calling `verify!/1`. 287 | 288 | ## Example: 289 | 290 | iex> Mimic.reject(Calculator, :add, 2) 291 | Calculator 292 | 293 | """ 294 | @spec reject(module, atom, non_neg_integer) :: module 295 | def reject(module, function_name, arity) do 296 | raise_if_not_exported_function!(module, function_name, arity) 297 | func = Function.capture(module, function_name, arity) 298 | 299 | module 300 | |> Server.expect(function_name, arity, 0, func) 301 | |> validate_server_response(:reject) 302 | end 303 | 304 | @doc """ 305 | Allow other processes to share expectations and stubs defined by another 306 | process. 307 | 308 | ## Arguments: 309 | 310 | * `module` - the copied module. 311 | * `owner_pid` - the process ID of the process which created the stub. 312 | * `allowed_pid` - the process ID of the process which should also be allowed 313 | to use this stub. 314 | 315 | ## Raises: 316 | 317 | * If Mimic is running in global mode. 318 | 319 | Allows other processes to share expectations and stubs defined by another 320 | process. 321 | 322 | ## Example 323 | 324 | ```elixir 325 | test "invokes add from a task" do 326 | Calculator 327 | |> expect(:add, fn x, y -> x + y end) 328 | 329 | parent_pid = self() 330 | 331 | Task.async(fn -> 332 | Calculator |> allow(parent_pid, self()) 333 | assert Calculator.add(2, 3) == 5 334 | end) 335 | |> Task.await 336 | end 337 | ``` 338 | """ 339 | @spec allow(module(), pid(), pid()) :: module() | {:error, atom()} 340 | def allow(module, owner_pid, allowed_pid) do 341 | module 342 | |> Server.allow(owner_pid, allowed_pid) 343 | |> validate_server_response(:allow) 344 | end 345 | 346 | @doc """ 347 | Prepare `module` for mocking. 348 | 349 | Ideally, don't call this function twice for the same module, but in case you do, this function 350 | is idempotent. It will not delete any `stub`s or `expect`s that you've set up. 351 | 352 | ## Arguments: 353 | 354 | * `module` - the name of the module to copy. 355 | * `opts` - Extra options 356 | 357 | ## Options: 358 | 359 | * `type_check` - Must be a boolean defaulting to `false`. If `true` the arguments and return value 360 | are validated against the module typespecs or the callback typespecs in case of a behaviour implementation. 361 | """ 362 | @spec copy(module(), keyword) :: :ok | no_return 363 | def copy(module, opts \\ []) do 364 | with :ok <- ensure_module_not_copied(module), 365 | {:module, module} <- Code.ensure_compiled(module), 366 | :ok <- Mimic.Server.mark_to_copy(module, opts) do 367 | if repeat_until_failure?() do 368 | ExUnit.after_suite(fn _ -> Mimic.Server.soft_reset(module) end) 369 | else 370 | ExUnit.after_suite(fn _ -> Mimic.Server.reset(module) end) 371 | end 372 | 373 | :ok 374 | else 375 | {:error, {:module_already_copied, _module}} -> 376 | :ok 377 | 378 | {:error, reason} 379 | when reason in [:embedded, :badfile, :nofile, :on_load_failure, :unavailable] -> 380 | raise ArgumentError, "Module #{inspect(module)} is not available" 381 | 382 | error -> 383 | validate_server_response(error, :copy) 384 | end 385 | end 386 | 387 | @doc """ 388 | Call original implementation of a function. 389 | 390 | This function allows you to call the original implementation of a function, 391 | even if it has been stubbed, rejected or expected. 392 | 393 | ## Arguments: 394 | 395 | * `module` - the name of the module in which we're calling. 396 | * `function_name` - the name of the function we're calling. 397 | * `args` - the arguments of the function we're calling. 398 | 399 | ## Raises: 400 | 401 | * If `function_name` does not exist in `module`. 402 | 403 | ## Example: 404 | 405 | iex> Mimic.call_original(Calculator, :add, [1, 2]) 406 | 3 407 | 408 | """ 409 | @spec call_original(module, atom, list) :: any 410 | def call_original(module, function_name, args) do 411 | arity = length(args) 412 | 413 | raise_if_not_exported_function!(module, function_name, arity) 414 | func = Function.capture(Mimic.Module.original(module), function_name, arity) 415 | 416 | Kernel.apply(func, args) 417 | end 418 | 419 | @doc """ 420 | Verifies the current process after it exits. 421 | 422 | If you want to verify expectations for all tests, you can use 423 | `verify_on_exit!/1` as a setup callback: 424 | 425 | ```elixir 426 | setup :verify_on_exit! 427 | ``` 428 | """ 429 | @spec verify_on_exit!(map()) :: :ok | no_return() 430 | def verify_on_exit!(_context \\ %{}) do 431 | pid = self() 432 | 433 | Server.verify_on_exit(pid) 434 | 435 | Callbacks.on_exit(Mimic, fn -> 436 | verify!(pid) 437 | Server.exit(pid) 438 | end) 439 | end 440 | 441 | @doc """ 442 | Sets the mode to private. Mocks can be set and used by the process 443 | 444 | ```elixir 445 | setup :set_mimic_private 446 | ``` 447 | """ 448 | @spec set_mimic_private(map()) :: :ok 449 | def set_mimic_private(_context \\ %{}), do: Server.set_private_mode() 450 | 451 | @doc """ 452 | Sets the mode to global. Mocks can be set and used by all processes 453 | 454 | ```elixir 455 | setup :set_mimic_global 456 | ``` 457 | """ 458 | @spec set_mimic_global(map()) :: :ok 459 | def set_mimic_global(_context \\ %{}), do: Server.set_global_mode(self()) 460 | 461 | @doc """ 462 | Chooses the mode based on ExUnit context. If `async` is `true` then 463 | the mode is private, otherwise global. 464 | 465 | ```elixir 466 | setup :set_mimic_from_context 467 | ``` 468 | """ 469 | @spec set_mimic_from_context(map()) :: :ok 470 | def set_mimic_from_context(%{async: true}), do: set_mimic_private() 471 | def set_mimic_from_context(_context), do: set_mimic_global() 472 | 473 | @doc """ 474 | Verify if expectations were fulfilled for a process `pid` 475 | """ 476 | @spec verify!(pid()) :: :ok 477 | def verify!(pid \\ self()) do 478 | pending = Server.verify(pid) 479 | 480 | messages = 481 | for {{module, name, arity}, num_calls, num_applied_calls} <- pending do 482 | mfa = Exception.format_mfa(module, name, arity) 483 | 484 | " * expected #{mfa} to be invoked #{num_calls} time(s) " <> 485 | "but it has been called #{num_applied_calls} time(s)" 486 | end 487 | 488 | if messages != [] do 489 | raise VerificationError, 490 | "error while verifying mocks for #{inspect(pid)}:\n\n" <> Enum.join(messages, "\n") 491 | end 492 | 493 | :ok 494 | end 495 | 496 | @doc "Returns the current mode (`:global` or `:private`)" 497 | @spec mode() :: :private | :global 498 | def mode do 499 | Server.get_mode() 500 | end 501 | 502 | @doc """ 503 | Get the list of calls made to a mocked/stubbed function. 504 | 505 | This function returns a list of all arguments passed to the function during each call. 506 | If the function has not been mocked/stubbed or has not been called, it will raise an error. 507 | 508 | ## Arguments: 509 | 510 | * `function` - A capture of the function to get the calls for. 511 | 512 | ## Returns: 513 | 514 | * A list of lists, where each inner list contains the arguments from one call. 515 | 516 | ## Raises: 517 | 518 | * If the function has not been mocked/stubbed. 519 | * If the function does not exist in the module. 520 | 521 | ## Example: 522 | 523 | iex> Calculator.add(1, 2) 524 | 3 525 | iex> Mimic.calls(&Calculator.add/2) 526 | [[1, 2]] 527 | 528 | """ 529 | @spec calls(function) :: [[any]] | {:error, :atom} 530 | def calls(function) do 531 | fun_info = Function.info(function) 532 | module = fun_info[:module] 533 | fn_name = fun_info[:name] 534 | arity = fun_info[:arity] 535 | 536 | calls(module, fn_name, arity) 537 | end 538 | 539 | @doc """ 540 | Get the list of calls made to a mocked/stubbed function. 541 | 542 | This function returns a list of all arguments passed to the function during each call. 543 | If the function has not been mocked/stubbed or has not been called, it will raise an error. 544 | 545 | ## Arguments: 546 | 547 | * `module` - the name of the module containing the function. 548 | * `function_name` - the name of the function. 549 | * `arity` - the arity of the function. 550 | 551 | ## Returns: 552 | 553 | * A list of lists, where each inner list contains the arguments from one call. 554 | 555 | ## Raises: 556 | 557 | * If the function has not been mocked/stubbed. 558 | * If the function does not exist in the module. 559 | 560 | ## Example: 561 | 562 | iex> Calculator.add(1, 2) 563 | 3 564 | iex> Mimic.calls(Calculator, :add, 2) 565 | [[1, 2]] 566 | 567 | """ 568 | @spec calls(module, atom, non_neg_integer) :: [[any]] | {:error, :atom} 569 | def calls(module, function_name, arity) do 570 | raise_if_not_exported_function!(module, function_name, arity) 571 | 572 | result = 573 | Server.get_calls(module, function_name, arity) 574 | |> validate_server_response(:calls) 575 | 576 | with {:ok, calls} <- result do 577 | calls 578 | end 579 | end 580 | 581 | defp ensure_module_not_copied(module) do 582 | case Server.marked_to_copy?(module) do 583 | false -> :ok 584 | true -> {:error, {:module_already_copied, module}} 585 | end 586 | end 587 | 588 | defp repeat_until_failure? do 589 | case ExUnit.configuration()[:repeat_until_failure] do 590 | 0 -> false 591 | repeat when is_integer(repeat) -> true 592 | _ -> false 593 | end 594 | end 595 | 596 | defp raise_if_not_exported_function!(module, fn_name, arity) do 597 | # This can return {:error, :nofile}, but we don't care about that 598 | Code.ensure_loaded(module) 599 | 600 | unless function_exported?(module, fn_name, arity) do 601 | raise ArgumentError, "Function #{fn_name}/#{arity} not defined for #{inspect(module)}" 602 | end 603 | end 604 | 605 | defp raise_if_module_not_defined!(module) do 606 | unless Code.ensure_loaded?(module) do 607 | raise ArgumentError, "Module #{inspect(module)} not defined" 608 | end 609 | end 610 | 611 | defp validate_server_response({:ok, module}, _action), do: module 612 | 613 | defp validate_server_response({:error, :not_global_owner}, :reject) do 614 | raise ArgumentError, 615 | "Reject cannot be called by the current process. Only the global owner is allowed." 616 | end 617 | 618 | defp validate_server_response({:error, :not_global_owner}, :expect) do 619 | raise ArgumentError, 620 | "Expect cannot be called by the current process. Only the global owner is allowed." 621 | end 622 | 623 | defp validate_server_response({:error, :not_global_owner}, :stub) do 624 | raise ArgumentError, 625 | "Stub cannot be called by the current process. Only the global owner is allowed." 626 | end 627 | 628 | defp validate_server_response({:error, :not_global_owner}, :allow) do 629 | raise ArgumentError, 630 | "Allow cannot be called by the current process. Only the global owner is allowed." 631 | end 632 | 633 | defp validate_server_response({:error, :global}, :allow) do 634 | raise ArgumentError, "Allow must not be called when mode is global." 635 | end 636 | 637 | defp validate_server_response({:error, {:module_not_copied, module}}, _action) do 638 | raise ArgumentError, 639 | "Module #{inspect(module)} has not been copied. See docs for Mimic.copy/1" 640 | end 641 | 642 | defp validate_server_response(_, :copy) do 643 | raise ArgumentError, 644 | "Failed to copy module. See docs for Mimic.copy/1" 645 | end 646 | end 647 | -------------------------------------------------------------------------------- /lib/mimic/application.ex: -------------------------------------------------------------------------------- 1 | defmodule Mimic.Application do 2 | use Application 3 | alias Mimic.Server 4 | @moduledoc false 5 | 6 | def start(_, _) do 7 | children = [Server] 8 | Supervisor.start_link(children, name: Mimic.Supervisor, strategy: :one_for_one) 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/mimic/cover.ex: -------------------------------------------------------------------------------- 1 | defmodule Mimic.Cover do 2 | @moduledoc """ 3 | Ensures mocked modules still get coverage data despite being replaced by `Mimic.Module`. 4 | Also ensures that coverage data from before moving the module around is not lost 5 | """ 6 | 7 | @spec enabled_for?(module) :: boolean 8 | def enabled_for?(module) do 9 | :cover.is_compiled(module) != false 10 | end 11 | 12 | @doc false 13 | # Hack to allow us to use private functions on the :cover module. 14 | # Recompiles the :cover module but with all private functions as public. 15 | # Completely based on meck's solution: 16 | # https://github.com/eproxus/meck/blob/2c7ba603416e95401500d7e116c5a829cb558665/src/meck_cover.erl#L67-L91 17 | # Is idempotent. 18 | def export_private_functions do 19 | if not private_functions_exported?() do 20 | {_, binary, _} = :code.get_object_code(:cover) 21 | {:ok, {_, [{_, {_, abstract_code}}]}} = :beam_lib.chunks(binary, [:abstract_code]) 22 | {:ok, module, binary} = :compile.forms(abstract_code, [:export_all]) 23 | {:module, :cover} = :code.load_binary(module, ~c"", binary) 24 | end 25 | 26 | :ok 27 | end 28 | 29 | @doc false 30 | # Resets the module and ensures we haven't lost its coverdata 31 | def clear_module_and_import_coverdata!(module, original_beam_path, original_coverdata_path) do 32 | path = module |> Mimic.Module.original() |> export_coverdata!() 33 | rewrite_coverdata!(path, module) 34 | 35 | Mimic.Module.clear!(module) 36 | # Put back cover-compiled status for original module (don't need the private 37 | # compile_beams function here because the file should exist for the original module) 38 | :cover.compile_beam(original_beam_path) 39 | 40 | # Original module's coverdata would be lost due to purging it otherwise 41 | :ok = :cover.import(String.to_charlist(path)) 42 | # Load coverdata from module from before the test 43 | :ok = :cover.import(String.to_charlist(original_coverdata_path)) 44 | 45 | File.rm(path) 46 | File.rm(original_coverdata_path) 47 | end 48 | 49 | @doc false 50 | def export_coverdata!(module) do 51 | path = Path.expand("#{module}-#{:os.getpid()}.coverdata", ".") 52 | :ok = :cover.export(String.to_charlist(path), module) 53 | path 54 | end 55 | 56 | defp private_functions_exported? do 57 | function_exported?(:cover, :get_term, 1) 58 | end 59 | 60 | defp rewrite_coverdata!(path, module) do 61 | terms = get_terms(path) 62 | terms = replace_module_name(terms, module) 63 | write_coverdata!(path, terms) 64 | end 65 | 66 | defp replace_module_name(terms, module) do 67 | Enum.map(terms, fn term -> do_replace_module_name(term, module) end) 68 | end 69 | 70 | defp do_replace_module_name({:file, old, file}, module) do 71 | {:file, module, String.replace(file, to_string(old), to_string(module))} 72 | end 73 | 74 | defp do_replace_module_name({bump = {:bump, _mod, _, _, _, _}, value}, module) do 75 | {put_elem(bump, 1, module), value} 76 | end 77 | 78 | defp do_replace_module_name({_mod, clauses}, module) do 79 | {module, replace_module_name(clauses, module)} 80 | end 81 | 82 | defp do_replace_module_name(clause = {_mod, _, _, _, _}, module) do 83 | put_elem(clause, 0, module) 84 | end 85 | 86 | defp get_terms(path) do 87 | {:ok, resource} = File.open(path, [:binary, :read, :raw]) 88 | terms = get_terms(resource, []) 89 | File.close(resource) 90 | terms 91 | end 92 | 93 | defp get_terms(resource, terms) do 94 | case apply(:cover, :get_term, [resource]) do 95 | :eof -> terms 96 | term -> get_terms(resource, [term | terms]) 97 | end 98 | end 99 | 100 | defp write_coverdata!(path, terms) do 101 | {:ok, resource} = File.open(path, [:write, :binary, :raw]) 102 | Enum.each(terms, fn term -> apply(:cover, :write, [term, resource]) end) 103 | File.close(resource) 104 | end 105 | end 106 | -------------------------------------------------------------------------------- /lib/mimic/dsl.ex: -------------------------------------------------------------------------------- 1 | defmodule Mimic.DSL do 2 | @moduledoc """ 3 | Stubs and expectations can be expressed in a more natural way. 4 | 5 | ```elixir 6 | use Mimic.DSL 7 | ``` 8 | 9 | ```elixir 10 | test "basic example" do 11 | stub Calculator.add(_x, _y), do: :stub 12 | expect Calculator.add(x, y), do: x + y 13 | expect Calculator.mult(x, y), do: x * y 14 | 15 | assert Calculator.add(2, 3) == 5 16 | assert Calculator.mult(2, 3) == 6 17 | 18 | assert Calculator.add(2, 3) == :stub 19 | end 20 | ``` 21 | 22 | Support for expecting multiple calls: 23 | 24 | ```elixir 25 | expect Calculator.add(x, y), num_calls: 2 do 26 | x + y 27 | end 28 | ``` 29 | 30 | """ 31 | 32 | @doc false 33 | defmacro __using__(_opts) do 34 | quote do 35 | import Mimic, except: [stub: 3, expect: 3, expect: 4] 36 | import Mimic.DSL 37 | setup :verify_on_exit! 38 | end 39 | end 40 | 41 | defmacro stub({{:., _, [module, f]}, _, args}, opts) do 42 | body = Keyword.fetch!(opts, :do) 43 | 44 | function = 45 | quote do 46 | fn unquote_splicing(args) -> 47 | unquote(body) 48 | end 49 | end 50 | 51 | quote do 52 | Mimic.stub(unquote(module), unquote(f), unquote(function)) 53 | end 54 | end 55 | 56 | defmacro stub({:when, _, [{{:., _, [module, f]}, _, args}, guard_args]}, opts) do 57 | body = Keyword.fetch!(opts, :do) 58 | 59 | function = 60 | quote do 61 | fn unquote_splicing(args) when unquote(guard_args) -> 62 | unquote(body) 63 | end 64 | end 65 | 66 | quote do 67 | Mimic.stub(unquote(module), unquote(f), unquote(function)) 68 | end 69 | end 70 | 71 | defmacro expect(ast, opts \\ [], do_block) 72 | 73 | defmacro expect({{:., _, [module, f]}, _, args}, opts, do_opts) do 74 | num_calls = 75 | Keyword.get_lazy(opts, :num_calls, fn -> 76 | Keyword.get(do_opts, :num_calls, 1) 77 | end) 78 | 79 | body = Keyword.fetch!(do_opts, :do) 80 | 81 | function = 82 | quote do 83 | fn unquote_splicing(args) -> 84 | unquote(body) 85 | end 86 | end 87 | 88 | quote do 89 | Mimic.expect(unquote(module), unquote(f), unquote(num_calls), unquote(function)) 90 | end 91 | end 92 | 93 | defmacro expect({:when, _, [{{:., _, [module, f]}, _, args}, guard_args]}, opts, do_opts) do 94 | num_calls = 95 | Keyword.get_lazy(opts, :num_calls, fn -> 96 | Keyword.get(do_opts, :num_calls, 1) 97 | end) 98 | 99 | body = Keyword.fetch!(do_opts, :do) 100 | 101 | function = 102 | quote do 103 | fn unquote_splicing(args) when unquote(guard_args) -> 104 | unquote(body) 105 | end 106 | end 107 | 108 | quote do 109 | Mimic.expect(unquote(module), unquote(f), unquote(num_calls), unquote(function)) 110 | end 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /lib/mimic/error.ex: -------------------------------------------------------------------------------- 1 | defmodule Mimic.Error do 2 | defexception ~w(module fn_name arity)a 3 | @moduledoc false 4 | 5 | def message(e) do 6 | mfa = Exception.format_mfa(e.module, e.fn_name, e.arity) 7 | "#{mfa} cannot be stubbed as original module does not export such function" 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/mimic/module.ex: -------------------------------------------------------------------------------- 1 | defmodule Mimic.Module do 2 | alias Mimic.{Cover, Server} 3 | 4 | @elixir_version System.version() |> Float.parse() |> elem(0) 5 | @moduledoc false 6 | 7 | @spec original(module) :: module 8 | def original(module), do: "#{module}.Mimic.Original.Module" |> String.to_atom() 9 | 10 | @spec clear!(module) :: :ok 11 | def clear!(module) do 12 | :code.purge(module) 13 | :code.delete(module) 14 | :code.purge(original(module)) 15 | :code.delete(original(module)) 16 | :cover.reset(original(module)) 17 | :ok 18 | end 19 | 20 | @spec replace!(module, keyword) :: :ok | {:cover.file(), binary} 21 | def replace!(module, opts) do 22 | backup_module = original(module) 23 | 24 | result = 25 | case :cover.is_compiled(module) do 26 | {:file, beam_file} -> 27 | # We don't want to wipe the coverdata for this module in the process of 28 | # renaming it. Save it for later 29 | coverdata_path = Cover.export_coverdata!(module) 30 | 31 | {beam_file, coverdata_path} 32 | 33 | false -> 34 | :ok 35 | end 36 | 37 | rename_module(module, backup_module) 38 | Code.compiler_options(ignore_module_conflict: true) 39 | create_mock(module, Map.new(opts)) 40 | Code.compiler_options(ignore_module_conflict: false) 41 | 42 | result 43 | end 44 | 45 | @spec copied?(module) :: boolean 46 | def copied?(module) do 47 | function_exported?(module, :__mimic_info__, 0) 48 | end 49 | 50 | defp rename_module(module, new_module) do 51 | beam_code = beam_code(module) 52 | 53 | {:ok, {_, [{:abstract_code, {:raw_abstract_v1, forms}}]}} = 54 | :beam_lib.chunks(beam_code, [:abstract_code]) 55 | 56 | forms = rename_attribute(forms, new_module) 57 | 58 | case :compile.forms(forms, compiler_options(module)) do 59 | {:ok, module_name, binary} -> 60 | load_binary(module_name, binary, Cover.enabled_for?(module)) 61 | binary 62 | 63 | {:ok, module_name, binary, _warnings} -> 64 | load_binary(module_name, binary, Cover.enabled_for?(module)) 65 | binary 66 | end 67 | end 68 | 69 | defp beam_code(module) do 70 | # Note: If the module was compiled with :cover, this loads the version of the module pre 71 | # coverage 72 | case :code.get_object_code(module) do 73 | {_, binary, _filename} -> binary 74 | _error -> throw({:object_code_not_found, module}) 75 | end 76 | end 77 | 78 | defp compiler_options(module) do 79 | options = 80 | module.module_info(:compile) 81 | |> Keyword.get(:options) 82 | |> Enum.filter(&(&1 != :from_core)) 83 | 84 | [:return_errors | [:debug_info | options]] 85 | end 86 | 87 | defp load_binary(module, binary, enable_cover?) do 88 | case :code.load_binary(module, ~c"", binary) do 89 | {:module, ^module} -> :ok 90 | {:error, reason} -> exit({:error_loading_module, module, reason}) 91 | end 92 | 93 | if enable_cover? do 94 | Cover.export_private_functions() 95 | # Call dynamically to avoid compiler warning about private function being called 96 | # (compile_beams) which the above function exported. See export_private_functions's comment 97 | # for more info. 98 | # 99 | # beam_code/1 loads the not-cover-compiled version of the module, so we compile the 100 | # renamed module using cover. This is so we can collect coverage data on the 101 | # original module (which is called by the mock) 102 | apply(:cover, :compile_beams, [[{module, binary}]]) 103 | end 104 | end 105 | 106 | defp rename_attribute([{:attribute, line, :module, {_, vars}} | t], new_name) do 107 | [{:attribute, line, :module, {new_name, vars}} | t] 108 | end 109 | 110 | defp rename_attribute([{:attribute, line, :module, _} | t], new_name) do 111 | [{:attribute, line, :module, new_name} | t] 112 | end 113 | 114 | defp rename_attribute([h | t], new_name), do: [h | rename_attribute(t, new_name)] 115 | 116 | defp create_mock(module, opts) do 117 | mimic_info = module_mimic_info(opts) 118 | mimic_behaviours = generate_mimic_behaviours(module) 119 | mimic_functions = generate_mimic_functions(module) 120 | mimic_struct = generate_mimic_struct(module) 121 | quoted = [mimic_info, mimic_struct | mimic_behaviours ++ mimic_functions] 122 | Module.create(module, quoted, Macro.Env.location(__ENV__)) 123 | module 124 | end 125 | 126 | if @elixir_version >= 1.18 do 127 | defp generate_mimic_struct(module) do 128 | if function_exported?(module, :__info__, 1) && module.__info__(:struct) != nil do 129 | struct_info = module.__info__(:struct) 130 | 131 | struct_template = Map.from_struct(module.__struct__()) 132 | 133 | struct_params = 134 | for %{field: field} <- struct_info, 135 | do: {field, Macro.escape(struct_template[field])} 136 | 137 | quote do 138 | defstruct unquote(struct_params) 139 | end 140 | end 141 | end 142 | else 143 | defp generate_mimic_struct(module) do 144 | if function_exported?(module, :__info__, 1) && module.__info__(:struct) != nil do 145 | struct_info = 146 | module.__info__(:struct) 147 | |> Enum.split_with(& &1.required) 148 | |> Tuple.to_list() 149 | |> List.flatten() 150 | 151 | required_fields = for %{field: field, required: true} <- struct_info, do: field 152 | struct_template = Map.from_struct(module.__struct__()) 153 | 154 | struct_params = 155 | for %{field: field, required: required} <- struct_info do 156 | if required do 157 | field 158 | else 159 | {field, Macro.escape(struct_template[field])} 160 | end 161 | end 162 | 163 | quote do 164 | @enforce_keys unquote(required_fields) 165 | defstruct unquote(struct_params) 166 | end 167 | end 168 | end 169 | end 170 | 171 | defp module_mimic_info(opts) do 172 | quote do: def(__mimic_info__, do: {:ok, unquote(Macro.escape(opts))}) 173 | end 174 | 175 | defp generate_mimic_functions(module) do 176 | internal_functions = [__info__: 1, module_info: 0, module_info: 1] 177 | 178 | for {fn_name, arity} <- module.module_info(:exports), 179 | {fn_name, arity} not in internal_functions do 180 | args = Macro.generate_arguments(arity, module) 181 | 182 | quote do 183 | def unquote(fn_name)(unquote_splicing(args)) do 184 | Server.apply(__MODULE__, unquote(fn_name), unquote(args)) 185 | end 186 | end 187 | end 188 | end 189 | 190 | defp generate_mimic_behaviours(module) do 191 | module.module_info(:attributes) 192 | |> Keyword.get_values(:behaviour) 193 | |> List.flatten() 194 | |> Enum.map(fn behaviour -> 195 | quote do 196 | @behaviour unquote(behaviour) 197 | end 198 | end) 199 | end 200 | end 201 | -------------------------------------------------------------------------------- /lib/mimic/server.ex: -------------------------------------------------------------------------------- 1 | defmodule Mimic.Server do 2 | use GenServer 3 | alias Mimic.Cover 4 | @moduledoc false 5 | 6 | defmodule State do 7 | @moduledoc false 8 | defstruct verify_on_exit: MapSet.new(), 9 | mode: :private, 10 | global_pid: nil, 11 | stubs: %{}, 12 | expectations: %{}, 13 | modules_beam: %{}, 14 | modules_to_be_copied: MapSet.new(), 15 | reset_tasks: %{}, 16 | modules_opts: %{}, 17 | call_history: %{} 18 | end 19 | 20 | defmodule Expectation do 21 | @moduledoc false 22 | defstruct func: nil, num_applied_calls: 0, num_calls: nil 23 | end 24 | 25 | @long_timeout Application.compile_env(:mimic, :server_timeout, 60_000) 26 | 27 | @spec allow(module, pid, pid) :: {:ok, module} | {:error, :global} 28 | def allow(module, owner_pid, allowed_pid) do 29 | GenServer.call(__MODULE__, {:allow, module, owner_pid, allowed_pid}) 30 | end 31 | 32 | @spec verify(pid) :: non_neg_integer 33 | def verify(pid) do 34 | GenServer.call(__MODULE__, {:verify, pid}, @long_timeout) 35 | end 36 | 37 | @spec verify_on_exit(pid) :: :ok 38 | def verify_on_exit(pid) do 39 | GenServer.call(__MODULE__, {:verify_on_exit, pid}, @long_timeout) 40 | end 41 | 42 | @spec stub(module, atom, arity, function) :: 43 | {:ok, module} | {:error, :not_global_owner} | {:error, {:module_not_copied, module}} 44 | def stub(module, fn_name, arity, func) do 45 | GenServer.call(__MODULE__, {:stub, module, fn_name, func, arity, self()}, @long_timeout) 46 | end 47 | 48 | @spec stub(module) :: 49 | {:ok, module} | {:error, :not_global_owner} | {:error, {:module_not_copied, module}} 50 | def stub(module) do 51 | GenServer.call(__MODULE__, {:stub, module, self()}, @long_timeout) 52 | end 53 | 54 | @spec stub_with(module, module) :: 55 | {:ok, module} | {:error, :not_global_owner} | {:error, {:module_not_copied, module}} 56 | def stub_with(module, mocking_module) do 57 | GenServer.call(__MODULE__, {:stub_with, module, mocking_module, self()}, @long_timeout) 58 | end 59 | 60 | @spec expect(module, atom, arity, non_neg_integer, function) :: 61 | {:ok, module} | {:error, :not_global_owner} | {:error, {:module_not_copied, module}} 62 | def expect(module, fn_name, arity, num_calls, func) do 63 | GenServer.call( 64 | __MODULE__, 65 | {:expect, {module, fn_name, func, arity}, num_calls, self()}, 66 | @long_timeout 67 | ) 68 | end 69 | 70 | @spec set_global_mode(pid) :: :ok 71 | def set_global_mode(owner_pid) do 72 | GenServer.call(__MODULE__, {:set_global_mode, owner_pid}, @long_timeout) 73 | end 74 | 75 | @spec set_private_mode :: :ok 76 | def set_private_mode do 77 | GenServer.call(__MODULE__, :set_private_mode, @long_timeout) 78 | end 79 | 80 | @spec get_mode :: :private | :global 81 | def get_mode do 82 | GenServer.call(__MODULE__, :get_mode, @long_timeout) 83 | end 84 | 85 | @spec exit(pid) :: :ok 86 | def exit(pid) do 87 | GenServer.cast(__MODULE__, {:exit, pid}) 88 | end 89 | 90 | @spec reset(module) :: :ok 91 | def reset(module) do 92 | GenServer.call(__MODULE__, {:reset, module}, @long_timeout) 93 | end 94 | 95 | @spec soft_reset(module) :: :ok 96 | def soft_reset(module) do 97 | GenServer.call(__MODULE__, {:soft_reset, module}, @long_timeout) 98 | end 99 | 100 | @spec mark_to_copy(module, keyword) :: :ok | {:error, {:module_already_copied, module}} 101 | def mark_to_copy(module, opts) do 102 | GenServer.call(__MODULE__, {:mark_to_copy, module, opts}, @long_timeout) 103 | end 104 | 105 | @spec marked_to_copy?(module) :: boolean 106 | def marked_to_copy?(module) do 107 | GenServer.call(__MODULE__, {:marked_to_copy?, module}, @long_timeout) 108 | end 109 | 110 | @spec get_calls(module, atom, arity) :: {:ok, list(list(term))} | {:error, :not_found} 111 | def get_calls(module, fn_name, arity) do 112 | GenServer.call(__MODULE__, {:get_calls, {module, fn_name, arity}, self()}) 113 | end 114 | 115 | def apply(module, fn_name, args) do 116 | arity = Enum.count(args) 117 | original_module = Mimic.Module.original(module) 118 | 119 | if function_exported?(original_module, fn_name, arity) do 120 | caller_pids = [self() | Process.get(:"$callers", [])] 121 | 122 | case allowed_pid(caller_pids, module) do 123 | {:ok, owner_pid} -> 124 | do_apply(owner_pid, module, fn_name, arity, args) 125 | 126 | _ -> 127 | apply_original(module, fn_name, args) 128 | end 129 | else 130 | raise Mimic.Error, module: module, fn_name: fn_name, arity: arity 131 | end 132 | end 133 | 134 | defp do_apply(owner_pid, module, fn_name, arity, args) do 135 | case GenServer.call(__MODULE__, {:apply, owner_pid, module, fn_name, arity, args}, :infinity) do 136 | {:ok, func} -> 137 | Kernel.apply(func, args) 138 | 139 | :original -> 140 | apply_original(module, fn_name, args) 141 | 142 | {:unexpected, num_calls, num_applied_calls} -> 143 | mfa = Exception.format_mfa(module, fn_name, arity) 144 | 145 | raise Mimic.UnexpectedCallError, 146 | "expected #{mfa} to be called #{num_calls} time(s) " <> 147 | "but it has been called #{num_applied_calls} time(s) in process #{inspect(self())}" 148 | end 149 | end 150 | 151 | defp apply_original(module, fn_name, args), 152 | do: Kernel.apply(Mimic.Module.original(module), fn_name, args) 153 | 154 | defp allowed_pid(pids, module) do 155 | case :ets.lookup(__MODULE__, :mode) do 156 | [{:mode, :private}] -> 157 | match = match_spec(pids, module) 158 | 159 | case :ets.select(__MODULE__, match) do 160 | [] -> :none 161 | [owner_pid | _] -> {:ok, owner_pid} 162 | end 163 | 164 | [{:mode, :global, global_pid}] -> 165 | case :ets.lookup(__MODULE__, {global_pid, module}) do 166 | [] -> :none 167 | [{{^global_pid, ^module}, owner_pid}] -> {:ok, owner_pid} 168 | end 169 | end 170 | end 171 | 172 | defp match_spec(pids, module) do 173 | guards = Enum.map(pids, fn pid -> {:==, :"$1", pid} end) 174 | orelse = List.to_tuple([:orelse | guards]) 175 | [{{{:"$1", module}, :"$2"}, [orelse], [:"$2"]}] 176 | end 177 | 178 | def start_link(_) do 179 | GenServer.start_link(__MODULE__, [], name: __MODULE__) 180 | end 181 | 182 | def init([]) do 183 | :ets.new(__MODULE__, [:named_table, :protected, :set]) 184 | state = do_set_private_mode(%State{}) 185 | {:ok, state} 186 | end 187 | 188 | def handle_cast({:exit, pid}, state) do 189 | {:noreply, clear_data_from_pid(pid, state)} 190 | end 191 | 192 | def handle_info({:DOWN, _ref, :process, pid, _reason}, state) do 193 | new_state = 194 | if MapSet.member?(state.verify_on_exit, pid) do 195 | state 196 | else 197 | clear_data_from_pid(pid, state) 198 | end 199 | 200 | {:noreply, new_state} 201 | end 202 | 203 | # Reset task has successfully finished 204 | def handle_info({ref, :ok}, state) do 205 | reset_tasks = Map.delete(state.reset_tasks, ref) 206 | 207 | {:noreply, %{state | reset_tasks: reset_tasks}} 208 | end 209 | 210 | def handle_info(msg, state) do 211 | IO.puts("handle_info with #{inspect(msg)} not handled") 212 | {:noreply, state} 213 | end 214 | 215 | defp clear_data_from_pid(pid, state) do 216 | expectations = Map.delete(state.expectations, pid) 217 | stubs = Map.delete(state.stubs, pid) 218 | 219 | select = [{{{pid, :_}}, [], [true]}, {{{:_, :_}, pid}, [], [true]}] 220 | 221 | :ets.select_delete(__MODULE__, select) 222 | 223 | state = 224 | if pid == state.global_pid do 225 | do_set_private_mode(state) 226 | else 227 | state 228 | end 229 | 230 | call_history = Map.delete(state.call_history, pid) 231 | 232 | %{state | expectations: expectations, stubs: stubs, call_history: call_history} 233 | end 234 | 235 | defp find_stub(stubs, module, fn_name, arity, caller) do 236 | case get_in(stubs, [caller, {module, fn_name, arity}]) do 237 | func when is_function(func) -> {:ok, func} 238 | nil -> :unexpected 239 | end 240 | end 241 | 242 | defp get_call_history(state, caller, module, fn_name, arity) do 243 | get_in(state.call_history, [Access.key(caller, %{}), {module, fn_name, arity}]) 244 | end 245 | 246 | defp put_call_history(state, caller, module, fn_name, arity, args) do 247 | call_history = get_call_history(state, caller, module, fn_name, arity) || [] 248 | 249 | %{ 250 | state 251 | | call_history: 252 | put_in( 253 | state.call_history, 254 | [Access.key(caller, %{}), {module, fn_name, arity}], 255 | [ 256 | args | call_history 257 | ] 258 | ) 259 | } 260 | end 261 | 262 | def handle_call({:apply, owner_pid, module, fn_name, arity, args}, _from, state) do 263 | caller = 264 | if state.mode == :private do 265 | owner_pid 266 | else 267 | state.global_pid 268 | end 269 | 270 | case get_in(state.expectations, [Access.key(caller, %{}), {module, fn_name, arity}]) do 271 | [expectation | _] = expectations -> 272 | case apply_call_to_expectations(expectations, expectation) do 273 | {:ok, func, new_expectations} -> 274 | expectations = 275 | put_in(state.expectations, [caller, {module, fn_name, arity}], new_expectations) 276 | 277 | # Track call history 278 | state = put_call_history(state, caller, module, fn_name, arity, args) 279 | 280 | {:reply, {:ok, func}, %{state | expectations: expectations}} 281 | 282 | {:unexpected, num_calls, num_applied_calls} -> 283 | {:reply, {:unexpected, num_calls, num_applied_calls}, state} 284 | end 285 | 286 | _ -> 287 | case find_stub(state.stubs, module, fn_name, arity, caller) do 288 | :unexpected -> 289 | {:reply, :original, state} 290 | 291 | {:ok, func} -> 292 | # Track call history for stubs too 293 | state = put_call_history(state, caller, module, fn_name, arity, args) 294 | 295 | {:reply, {:ok, func}, state} 296 | end 297 | end 298 | end 299 | 300 | def handle_call({:stub, module, fn_name, func, arity, owner}, _from, state) do 301 | with {:ok, state} <- ensure_module_copied(module, state), 302 | true <- valid_mode?(state, owner), 303 | func <- maybe_typecheck_func(module, fn_name, func) do 304 | monitor_if_not_verify_on_exit(owner, state.verify_on_exit) 305 | 306 | :ets.insert_new(__MODULE__, {{owner, module}, owner}) 307 | 308 | {:reply, {:ok, module}, 309 | %{ 310 | state 311 | | stubs: put_in(state.stubs, [Access.key(owner, %{}), {module, fn_name, arity}], func) 312 | }} 313 | else 314 | {:error, reason} -> 315 | {:reply, {:error, reason}, state} 316 | 317 | false -> 318 | {:reply, {:error, :not_global_owner}, state} 319 | end 320 | end 321 | 322 | def handle_call({:stub, module, owner}, _from, state) do 323 | with {:ok, state} <- ensure_module_copied(module, state), 324 | true <- valid_mode?(state, owner) do 325 | monitor_if_not_verify_on_exit(owner, state.verify_on_exit) 326 | 327 | :ets.insert_new(__MODULE__, {{owner, module}, owner}) 328 | 329 | internal_functions = [__info__: 1, module_info: 0, module_info: 1] 330 | 331 | stubs = 332 | module.module_info(:exports) 333 | |> Enum.filter(&(&1 not in internal_functions)) 334 | |> Enum.reduce(state.stubs, fn {fn_name, arity}, stubs -> 335 | func = stub_function(module, fn_name, arity) 336 | put_in(stubs, [Access.key(owner, %{}), {module, fn_name, arity}], func) 337 | end) 338 | 339 | {:reply, {:ok, module}, %{state | stubs: stubs}} 340 | else 341 | {:error, reason} -> 342 | {:reply, {:error, reason}, state} 343 | 344 | false -> 345 | {:reply, {:error, :not_global_owner}, state} 346 | end 347 | end 348 | 349 | def handle_call({:stub_with, mocked_module, mocking_module, owner}, _from, state) do 350 | with {:ok, state} <- ensure_module_copied(mocked_module, state), 351 | true <- valid_mode?(state, owner) do 352 | monitor_if_not_verify_on_exit(owner, state.verify_on_exit) 353 | 354 | :ets.insert_new(__MODULE__, {{owner, mocked_module}, owner}) 355 | 356 | original_module = Mimic.Module.original(mocked_module) 357 | 358 | internal_functions = [__info__: 1, module_info: 0, module_info: 1] 359 | 360 | mocked_public_functions = 361 | original_module.module_info(:exports) 362 | |> Enum.filter(&(&1 not in internal_functions)) 363 | |> MapSet.new() 364 | 365 | mocking_public_functions = 366 | mocking_module.module_info(:exports) 367 | |> Enum.filter(&(&1 not in internal_functions)) 368 | |> MapSet.new() 369 | 370 | will_be_mocked_functions = 371 | MapSet.intersection(mocking_public_functions, mocked_public_functions) 372 | 373 | will_be_stubbed_functions = 374 | MapSet.difference(mocked_public_functions, mocking_public_functions) 375 | 376 | stubs = 377 | will_be_mocked_functions 378 | |> Enum.reduce(state.stubs, fn {fn_name, arity}, stubs -> 379 | func = anonymize_module_function(mocking_module, fn_name, arity) 380 | func = maybe_typecheck_func(mocked_module, fn_name, func) 381 | put_in(stubs, [Access.key(owner, %{}), {mocked_module, fn_name, arity}], func) 382 | end) 383 | 384 | stubs = 385 | will_be_stubbed_functions 386 | |> Enum.reduce(stubs, fn {fn_name, arity}, stubs -> 387 | func = stub_function(mocked_module, fn_name, arity) 388 | put_in(stubs, [Access.key(owner, %{}), {mocked_module, fn_name, arity}], func) 389 | end) 390 | 391 | {:reply, {:ok, mocked_module}, %{state | stubs: stubs}} 392 | else 393 | {:error, reason} -> 394 | {:reply, {:error, reason}, state} 395 | 396 | false -> 397 | {:reply, {:error, :not_global_owner}, state} 398 | end 399 | end 400 | 401 | def handle_call({:expect, {module, fn_name, func, arity}, num_calls, owner}, _from, state) do 402 | with {:ok, state} <- ensure_module_copied(module, state), 403 | true <- valid_mode?(state, owner), 404 | func <- maybe_typecheck_func(module, fn_name, func) do 405 | monitor_if_not_verify_on_exit(owner, state.verify_on_exit) 406 | 407 | :ets.insert_new(__MODULE__, {{owner, module}, owner}) 408 | 409 | expectation = %Expectation{func: func, num_calls: num_calls} 410 | 411 | expectations = 412 | update_in( 413 | state.expectations, 414 | [Access.key(owner, %{}), {module, fn_name, arity}], 415 | &((&1 || []) ++ [expectation]) 416 | ) 417 | 418 | {:reply, {:ok, module}, %{state | expectations: expectations}} 419 | else 420 | {:error, reason} -> 421 | {:reply, {:error, reason}, state} 422 | 423 | false -> 424 | {:reply, {:error, :not_global_owner}, state} 425 | end 426 | end 427 | 428 | def handle_call({:set_global_mode, owner_pid}, _from, state) do 429 | {:reply, :ok, do_set_global_mode(owner_pid, state)} 430 | end 431 | 432 | def handle_call(:set_private_mode, _from, state) do 433 | {:reply, :ok, do_set_private_mode(state)} 434 | end 435 | 436 | def handle_call(:get_mode, _from, state) do 437 | {:reply, state.mode, state} 438 | end 439 | 440 | def handle_call({:allow, module, owner_pid, allowed_pid}, _from, state = %State{mode: :private}) do 441 | case :ets.lookup(__MODULE__, {owner_pid, module}) do 442 | [{{^owner_pid, ^module}, actual_owner_pid}] -> 443 | :ets.insert(__MODULE__, {{allowed_pid, module}, actual_owner_pid}) 444 | 445 | [] -> 446 | :ets.insert(__MODULE__, {{allowed_pid, module}, owner_pid}) 447 | end 448 | 449 | {:reply, {:ok, module}, state} 450 | end 451 | 452 | def handle_call( 453 | {:allow, _module, _owner_pid, _allowed_pid}, 454 | _from, 455 | state = %State{mode: :global} 456 | ) do 457 | {:reply, {:error, :global}, state} 458 | end 459 | 460 | def handle_call({:verify, pid}, _from, state) do 461 | expectations = state.expectations[pid] || %{} 462 | 463 | pending = 464 | for {{module, fn_name, arity}, mfa_expectations} <- expectations, 465 | expectation = %Expectation{num_applied_calls: num_applied_calls, num_calls: num_calls} <- 466 | mfa_expectations, 467 | num_calls != num_applied_calls do 468 | {{module, fn_name, arity}, expectation.num_calls, expectation.num_applied_calls} 469 | end 470 | 471 | {:reply, pending, state} 472 | end 473 | 474 | def handle_call({:verify_on_exit, pid}, _from, state) do 475 | {:reply, :ok, %{state | verify_on_exit: MapSet.put(state.verify_on_exit, pid)}} 476 | end 477 | 478 | def handle_call({:soft_reset, _module}, _from, state) do 479 | state = %{state | expectations: %{}, stubs: %{}, mode: :private, global_pid: nil} 480 | {:reply, :ok, state} 481 | end 482 | 483 | def handle_call({:reset, module}, _from, state) do 484 | state = %{state | modules_to_be_copied: MapSet.delete(state.modules_to_be_copied, module)} 485 | 486 | tasks = 487 | if Mimic.Module.copied?(module) do 488 | task = Task.async(fn -> do_reset(module, state) end) 489 | 490 | Map.put(state.reset_tasks, task.ref, task) 491 | else 492 | state.reset_tasks 493 | end 494 | 495 | # Clear the beam modules after starting the tasks (they read the state) 496 | # This is important for umbrella apps since they'll run app after app 497 | # and the modules that need to be covered will change between apps 498 | state = %{state | modules_beam: Map.delete(state.modules_beam, module)} 499 | 500 | # All modules have been reset. We should await all tasks now 501 | if state.modules_to_be_copied == MapSet.new() do 502 | tasks 503 | |> Map.values() 504 | |> Task.await_many(@long_timeout) 505 | 506 | {:reply, :ok, %{state | reset_tasks: %{}}} 507 | else 508 | {:reply, :ok, %{state | reset_tasks: tasks}} 509 | end 510 | end 511 | 512 | def handle_call({:marked_to_copy?, module}, _from, state) do 513 | {:reply, marked_to_copy?(module, state), state} 514 | end 515 | 516 | def handle_call({:mark_to_copy, module, opts}, _from, state) do 517 | if marked_to_copy?(module, state) do 518 | {:reply, {:error, {:module_already_copied, module}}, state} 519 | else 520 | # If cover is enabled call ensure_module_copied now 521 | # Otherwise just store that the module that will be copied 522 | # and ensure_module_copied/2 will copy it when 523 | # expect, stub, reject is called 524 | state = %{ 525 | state 526 | | modules_to_be_copied: MapSet.put(state.modules_to_be_copied, module), 527 | modules_opts: Map.put(state.modules_opts, module, opts) 528 | } 529 | 530 | state = 531 | if Cover.enabled_for?(module) do 532 | {:ok, state} = ensure_module_copied(module, state) 533 | state 534 | else 535 | state 536 | end 537 | 538 | {:reply, :ok, state} 539 | end 540 | end 541 | 542 | def handle_call({:get_calls, {module, fn_name, arity}, owner_pid}, _from, state) do 543 | caller_pids = [self() | Process.get(:"$callers", [])] 544 | 545 | caller_pid = 546 | case allowed_pid(caller_pids, module) do 547 | {:ok, owner_pid} -> owner_pid 548 | _ -> owner_pid 549 | end 550 | 551 | case ensure_module_copied(module, state) do 552 | {:ok, state} -> 553 | case pop_in(state.call_history, [Access.key(caller_pid, %{}), {module, fn_name, arity}]) do 554 | {calls, call_history} when is_list(calls) -> 555 | {:reply, {:ok, Enum.reverse(calls)}, %{state | call_history: call_history}} 556 | 557 | {nil, _} -> 558 | {:reply, {:ok, []}, state} 559 | end 560 | 561 | {:error, reason} -> 562 | {:reply, {:error, reason}, state} 563 | end 564 | end 565 | 566 | defp maybe_typecheck_func(module, fn_name, func) do 567 | case module.__mimic_info__() do 568 | {:ok, %{type_check: true}} -> 569 | Mimic.TypeCheck.wrap(module, fn_name, func) 570 | 571 | _ -> 572 | func 573 | end 574 | end 575 | 576 | defp marked_to_copy?(module, state) do 577 | MapSet.member?(state.modules_to_be_copied, module) 578 | end 579 | 580 | defp do_reset(module, state) do 581 | case state.modules_beam[module] do 582 | {beam, coverdata} -> Cover.clear_module_and_import_coverdata!(module, beam, coverdata) 583 | _ -> Mimic.Module.clear!(module) 584 | end 585 | end 586 | 587 | defp ensure_module_copied(module, state) do 588 | cond do 589 | Mimic.Module.copied?(module) -> 590 | {:ok, state} 591 | 592 | MapSet.member?(state.modules_to_be_copied, module) -> 593 | case Mimic.Module.replace!(module, state.modules_opts[module]) do 594 | {beam_file, coverdata_path} -> 595 | modules_beam = Map.put(state.modules_beam, module, {beam_file, coverdata_path}) 596 | {:ok, %{state | modules_beam: modules_beam}} 597 | 598 | :ok -> 599 | {:ok, state} 600 | end 601 | 602 | true -> 603 | {:error, {:module_not_copied, module}} 604 | end 605 | end 606 | 607 | defp apply_call_to_expectations( 608 | expectations, 609 | expectation = %Expectation{num_applied_calls: num_applied_calls, num_calls: num_calls} 610 | ) do 611 | cond do 612 | num_applied_calls + 1 == num_calls -> 613 | {:ok, expectation.func, tl(expectations)} 614 | 615 | num_applied_calls + 1 < num_calls -> 616 | {:ok, expectation.func, 617 | [%{expectation | num_applied_calls: num_applied_calls + 1} | tl(expectations)]} 618 | 619 | true -> 620 | {:unexpected, expectation.num_calls, expectation.num_applied_calls + 1} 621 | end 622 | end 623 | 624 | defp valid_mode?(state, caller) do 625 | state.mode == :private or (state.mode == :global and state.global_pid == caller) 626 | end 627 | 628 | def monitor_if_not_verify_on_exit(pid, verify_on_exit) do 629 | unless MapSet.member?(verify_on_exit, pid) do 630 | Process.monitor(pid) 631 | end 632 | end 633 | 634 | defp stub_function(module, fn_name, arity) do 635 | args = 636 | 0..arity 637 | |> Enum.to_list() 638 | |> tl 639 | |> Enum.map(fn i -> Macro.var(String.to_atom("arg_#{i}"), nil) end) 640 | 641 | clause = 642 | quote do 643 | unquote_splicing(args) -> 644 | mfa = Exception.format_mfa(unquote(module), unquote(fn_name), unquote(args)) 645 | 646 | raise Mimic.UnexpectedCallError, 647 | "Stub! Unexpected call to #{mfa} from #{inspect(self())}" 648 | end 649 | 650 | {fun, _} = Code.eval_quoted({:fn, [], clause}) 651 | fun 652 | end 653 | 654 | defp anonymize_module_function(module, fn_name, arity) do 655 | args = 656 | 0..arity 657 | |> Enum.to_list() 658 | |> tl 659 | |> Enum.map(fn i -> Macro.var(String.to_atom("arg_#{i}"), nil) end) 660 | 661 | clause = 662 | quote do 663 | unquote_splicing(args) -> 664 | apply(unquote(module), unquote(fn_name), [unquote_splicing(args)]) 665 | end 666 | 667 | {fun, _} = Code.eval_quoted({:fn, [], clause}) 668 | fun 669 | end 670 | 671 | defp do_set_global_mode(owner_pid, state) do 672 | :ets.insert(__MODULE__, {:mode, :global, owner_pid}) 673 | %{state | global_pid: owner_pid, mode: :global} 674 | end 675 | 676 | defp do_set_private_mode(state) do 677 | :ets.insert(__MODULE__, {:mode, :private}) 678 | %{state | global_pid: nil, mode: :private} 679 | end 680 | end 681 | -------------------------------------------------------------------------------- /lib/mimic/type_check.ex: -------------------------------------------------------------------------------- 1 | defmodule Mimic.TypeCheckError do 2 | @moduledoc false 3 | defexception [:mfa, :reasons] 4 | 5 | @doc false 6 | @impl Exception 7 | def exception([mfa, reasons]), do: %__MODULE__{mfa: mfa, reasons: reasons} 8 | 9 | @doc false 10 | @impl Exception 11 | def message(exception) do 12 | {module, name, arity} = exception.mfa 13 | mfa = Exception.format_mfa(module, name, arity) 14 | "#{mfa}: #{Ham.TypeMatchError.message(exception)}" 15 | end 16 | end 17 | 18 | defmodule Mimic.TypeCheck do 19 | @moduledoc false 20 | 21 | # Wrap an anoynomous function with type checking provided by Ham 22 | @doc false 23 | @spec wrap(module, atom, (... -> term)) :: (... -> term) 24 | def wrap(module, fn_name, func) do 25 | arity = :erlang.fun_info(func)[:arity] 26 | 27 | behaviours = 28 | module.module_info(:attributes) 29 | |> Keyword.get_values(:behaviour) 30 | |> List.flatten() 31 | 32 | do_wrap(module, behaviours, fn_name, func, arity) 33 | end 34 | 35 | defp do_wrap(module, behaviours, fn_name, func, 0) do 36 | fn -> 37 | apply_and_check(module, behaviours, fn_name, func, []) 38 | end 39 | end 40 | 41 | defp do_wrap(module, behaviours, fn_name, func, 1) do 42 | fn arg1 -> 43 | apply_and_check(module, behaviours, fn_name, func, [arg1]) 44 | end 45 | end 46 | 47 | defp do_wrap(module, behaviours, fn_name, func, 2) do 48 | fn arg1, arg2 -> 49 | apply_and_check(module, behaviours, fn_name, func, [arg1, arg2]) 50 | end 51 | end 52 | 53 | defp do_wrap(module, behaviours, fn_name, func, 3) do 54 | fn arg1, arg2, arg3 -> 55 | apply_and_check(module, behaviours, fn_name, func, [arg1, arg2, arg3]) 56 | end 57 | end 58 | 59 | defp do_wrap(module, behaviours, fn_name, func, 4) do 60 | fn arg1, arg2, arg3, arg4 -> 61 | apply_and_check(module, behaviours, fn_name, func, [arg1, arg2, arg3, arg4]) 62 | end 63 | end 64 | 65 | defp do_wrap(module, behaviours, fn_name, func, 5) do 66 | fn arg1, arg2, arg3, arg4, arg5 -> 67 | apply_and_check(module, behaviours, fn_name, func, [arg1, arg2, arg3, arg4, arg5]) 68 | end 69 | end 70 | 71 | defp do_wrap(module, behaviours, fn_name, func, 6) do 72 | fn arg1, arg2, arg3, arg4, arg5, arg6 -> 73 | apply_and_check(module, behaviours, fn_name, func, [arg1, arg2, arg3, arg4, arg5, arg6]) 74 | end 75 | end 76 | 77 | defp do_wrap(module, behaviours, fn_name, func, 7) do 78 | fn arg1, arg2, arg3, arg4, arg5, arg6, arg7 -> 79 | apply_and_check(module, behaviours, fn_name, func, [ 80 | arg1, 81 | arg2, 82 | arg3, 83 | arg4, 84 | arg5, 85 | arg6, 86 | arg7 87 | ]) 88 | end 89 | end 90 | 91 | defp do_wrap(module, behaviours, fn_name, func, 8) do 92 | fn arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8 -> 93 | apply_and_check(module, behaviours, fn_name, func, [ 94 | arg1, 95 | arg2, 96 | arg3, 97 | arg4, 98 | arg5, 99 | arg6, 100 | arg7, 101 | arg8 102 | ]) 103 | end 104 | end 105 | 106 | defp do_wrap(module, behaviours, fn_name, func, 9) do 107 | fn arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9 -> 108 | apply_and_check(module, behaviours, fn_name, func, [ 109 | arg1, 110 | arg2, 111 | arg3, 112 | arg4, 113 | arg5, 114 | arg6, 115 | arg7, 116 | arg8, 117 | arg9 118 | ]) 119 | end 120 | end 121 | 122 | defp do_wrap(_module, _behaviours, _fn_name, _func, arity) when arity > 9 do 123 | raise "Too many arguments!" 124 | end 125 | 126 | defp apply_and_check(module, behaviours, fn_name, func, args) do 127 | return_value = Kernel.apply(func, args) 128 | 129 | case Ham.validate(module, fn_name, args, return_value, behaviours: behaviours) do 130 | :ok -> 131 | :ok 132 | 133 | {:error, error} -> 134 | mfa = {module, fn_name, length(args)} 135 | raise Mimic.TypeCheckError, [mfa, error.reasons] 136 | end 137 | 138 | return_value 139 | end 140 | end 141 | -------------------------------------------------------------------------------- /lib/mimic/unexpected_call_error.ex: -------------------------------------------------------------------------------- 1 | defmodule Mimic.UnexpectedCallError do 2 | defexception [:message] 3 | @moduledoc false 4 | end 5 | -------------------------------------------------------------------------------- /lib/mimic/verification_error.ex: -------------------------------------------------------------------------------- 1 | defmodule Mimic.VerificationError do 2 | defexception [:message] 3 | @moduledoc false 4 | end 5 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edgurgel/mimic/6438f760bb397e3318d774bb20059736ffa80edf/logo.png -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Mimic.Mixfile do 2 | use Mix.Project 3 | 4 | @source_url "https://github.com/edgurgel/mimic" 5 | @version "1.12.0" 6 | 7 | def project do 8 | [ 9 | app: :mimic, 10 | version: @version, 11 | elixir: "~> 1.16", 12 | build_embedded: Mix.env() == :prod, 13 | start_permanent: Mix.env() == :prod, 14 | elixirc_paths: elixirc_paths(Mix.env()), 15 | name: "Mimic", 16 | deps: deps(), 17 | package: package(), 18 | docs: docs(), 19 | test_coverage: [tool: Mimic.TestCover] 20 | ] 21 | end 22 | 23 | def application do 24 | [ 25 | extra_applications: [:logger, :tools], 26 | mod: {Mimic.Application, []} 27 | ] 28 | end 29 | 30 | defp elixirc_paths(:test), do: ["lib", "test/support"] 31 | defp elixirc_paths(_), do: ["lib"] 32 | 33 | defp deps do 34 | [ 35 | {:ham, "~> 0.2"}, 36 | {:ex_doc, ">= 0.0.0", only: :dev, runtime: false}, 37 | {:credo, "~> 1.0", only: :dev} 38 | ] 39 | end 40 | 41 | defp package do 42 | [ 43 | description: "Mocks for Elixir functions", 44 | files: ["lib", "LICENSE", "mix.exs", "README.md", ".formatter.exs"], 45 | licenses: ["Apache-2.0"], 46 | maintainers: ["Eduardo Gurgel"], 47 | links: %{ 48 | "GitHub" => @source_url 49 | } 50 | ] 51 | end 52 | 53 | defp docs do 54 | [ 55 | extras: [ 56 | LICENSE: [title: "License"], 57 | "README.md": [title: "Overview"] 58 | ], 59 | main: "readme", 60 | source_url: @source_url, 61 | source_ref: "v#{@version}", 62 | formatters: ["html"] 63 | ] 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, 3 | "credo": {:hex, :credo, "1.7.7", "771445037228f763f9b2afd612b6aa2fd8e28432a95dbbc60d8e03ce71ba4446", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8bc87496c9aaacdc3f90f01b7b0582467b69b4bd2441fe8aae3109d843cc2f2e"}, 4 | "earmark": {:hex, :earmark, "1.4.1", "07bb382826ee8d08d575a1981f971ed41bd5d7e86b917fd012a93c51b5d28727", [:mix], [], "hexpm"}, 5 | "earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"}, 6 | "ex_doc": {:hex, :ex_doc, "0.34.1", "9751a0419bc15bc7580c73fde506b17b07f6402a1e5243be9e0f05a68c723368", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "d441f1a86a235f59088978eff870de2e815e290e44a8bd976fe5d64470a4c9d2"}, 7 | "file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"}, 8 | "ham": {:hex, :ham, "0.2.0", "e9ca2bd88bfe56866bc6d772a89731036e05c97d806384609201384182bc74d5", [:mix], [], "hexpm", "24a01df506a955ba80a1c1230a1904571ccf6cd7c689341f23baf6c93934e01c"}, 9 | "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, 10 | "makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"}, 11 | "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, 12 | "makeup_erlang": {:hex, :makeup_erlang, "1.0.0", "6f0eff9c9c489f26b69b61440bf1b238d95badae49adac77973cbacae87e3c2e", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "ea7a9307de9d1548d2a72d299058d1fd2339e3d398560a0e46c27dab4891e4d2"}, 13 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, 14 | } 15 | -------------------------------------------------------------------------------- /test/dsl_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Mimic.DSLTest do 2 | use ExUnit.Case, async: false 3 | use Mimic.DSL 4 | 5 | setup :set_mimic_private 6 | 7 | @stub 10 8 | 9 | test "basic example" do 10 | stub(Calculator.add(_x, _y), do: @stub) 11 | expect Calculator.add(x, y), do: x + y 12 | expect Calculator.mult(x, y), do: x * y 13 | 14 | assert Calculator.add(2, 3) == 5 15 | assert Calculator.mult(2, 3) == 6 16 | 17 | assert Calculator.add(2, 3) == @stub 18 | end 19 | 20 | test "guards on stub" do 21 | stub Calculator.add(x, y) when rem(x, 2) == 0 and y == 2 do 22 | x + y 23 | end 24 | 25 | assert Calculator.add(2, 2) == 4 26 | 27 | assert_raise FunctionClauseError, fn -> 28 | Calculator.add(3, 1) 29 | end 30 | end 31 | 32 | test "guards on expect" do 33 | expect Calculator.add(x, y) when rem(x, 2) == 0 and y == 2 do 34 | x + y 35 | end 36 | 37 | assert_raise FunctionClauseError, fn -> 38 | Calculator.add(3, 1) 39 | end 40 | end 41 | 42 | test "expect supports optional num_calls" do 43 | n = 2 44 | 45 | expect Calculator.add(x, y), num_calls: n do 46 | x + y 47 | end 48 | 49 | assert Calculator.add(1, 3) == 4 50 | assert Calculator.add(1, 4) == 5 51 | end 52 | 53 | test "expect supports optional num_calls with guard clause" do 54 | expect Calculator.add(x, y) when x == 1, num_calls: 2 do 55 | x + y 56 | end 57 | 58 | assert Calculator.add(1, 3) == 4 59 | 60 | assert_raise FunctionClauseError, fn -> 61 | Calculator.add(2, 4) == 6 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /test/edge_case_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Mimic.EdgeCase do 2 | use ExUnit.Case 3 | import Mimic 4 | 5 | describe "auto verification" do 6 | test "should verify_on_exit! correctly even when stub is called before (simulation test)" do 7 | set_mimic_private() 8 | parent_pid = self() 9 | 10 | spawn_link(fn -> 11 | stub(Calculator, :add, fn _, _ -> 3 end) 12 | Mimic.Server.verify_on_exit(self()) 13 | expect(Calculator, :add, fn _, _ -> 3 end) 14 | send(parent_pid, {:ok, self()}) 15 | end) 16 | 17 | assert_receive({:ok, child_pid}) 18 | 19 | assert_raise Mimic.VerificationError, fn -> 20 | verify!(child_pid) 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /test/mimic/type_check_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Mimic.TypeCheckTest do 2 | use ExUnit.Case, async: true 3 | alias Mimic.TypeCheck 4 | 5 | setup do 6 | # Force mimic to copy the module 7 | Mimic.stub(Typecheck.Calculator, :add, fn _x, _y -> 1 end) 8 | Mimic.stub(Typecheck.Counter, :inc, fn _x -> 1 end) 9 | :ok 10 | end 11 | 12 | describe "wrap/3" do 13 | test "behaviour typespec return value" do 14 | func = fn _x, _y -> :not_a_number end 15 | func = TypeCheck.wrap(Typecheck.Calculator, :add, func) 16 | 17 | assert_raise( 18 | Mimic.TypeCheckError, 19 | ~r/Returned value :not_a_number does not match type number()/, 20 | fn -> func.(1, 2) end 21 | ) 22 | end 23 | 24 | test "function typespec return value" do 25 | func = fn _x -> :not_a_number end 26 | func = TypeCheck.wrap(Typecheck.Counter, :inc, func) 27 | 28 | assert_raise( 29 | Mimic.TypeCheckError, 30 | ~r/Returned value :not_a_number does not match type number()/, 31 | fn -> func.(1) end 32 | ) 33 | end 34 | 35 | test "function typespec overriding behaviour spec return value" do 36 | func = fn _x, _y -> :not_a_number end 37 | func = TypeCheck.wrap(Typecheck.Calculator, :mult, func) 38 | 39 | assert_raise( 40 | Mimic.TypeCheckError, 41 | ~r/Returned value :not_a_number does not match type number()/, 42 | fn -> func.(1, 2) end 43 | ) 44 | end 45 | 46 | test "behaviour typespec argument" do 47 | func = fn _x, _y -> 42 end 48 | func = TypeCheck.wrap(Typecheck.Calculator, :add, func) 49 | 50 | assert_raise( 51 | Mimic.TypeCheckError, 52 | ~r/1st argument value :not_a_number does not match 1st parameter's type number()/, 53 | fn -> func.(:not_a_number, 2) end 54 | ) 55 | end 56 | 57 | test "function typespec argument" do 58 | func = fn _x -> 1 end 59 | func = TypeCheck.wrap(Typecheck.Counter, :inc, func) 60 | 61 | assert_raise( 62 | Mimic.TypeCheckError, 63 | ~r/1st argument value :not_a_number does not match 1st parameter's type number()/, 64 | fn -> func.(:not_a_number) end 65 | ) 66 | end 67 | 68 | test "function typespec overriding behaviour spec argument" do 69 | func = fn _x, _y -> 42 end 70 | func = TypeCheck.wrap(Typecheck.Calculator, :mult, func) 71 | 72 | assert_raise( 73 | Mimic.TypeCheckError, 74 | ~r/1st argument value :not_a_number does not match 1st parameter's type number()/, 75 | fn -> func.(:not_a_number, 2) end 76 | ) 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /test/mimic_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Mimic.Test do 2 | use ExUnit.Case, async: false 3 | import Mimic 4 | 5 | @expected 100 6 | @expected_1 200 7 | @expected_2 300 8 | 9 | @stubbed 400 10 | @private_stub 500 11 | @elixir_version System.version() |> Float.parse() |> elem(0) 12 | 13 | describe "no stub or expects private mode" do 14 | setup :set_mimic_private 15 | 16 | test "no stubs calls original" do 17 | assert Calculator.add(2, 2) == 4 18 | assert Calculator.mult(2, 3) == 6 19 | end 20 | end 21 | 22 | describe "no stub or expects global mode" do 23 | setup :set_mimic_global 24 | 25 | test "no stubs calls original" do 26 | assert Calculator.add(2, 2) == 4 27 | assert Calculator.mult(2, 3) == 6 28 | end 29 | end 30 | 31 | describe "default mode" do 32 | test "private mode is the default mode" do 33 | pid = 34 | spawn_link(fn -> 35 | Mimic.set_mimic_global() 36 | stub(Calculator, :add, fn _, _ -> @stubbed end) 37 | 38 | pid = 39 | spawn_link(fn -> 40 | assert Calculator.add(3, 7) == @stubbed 41 | end) 42 | 43 | Process.monitor(pid) 44 | assert_receive {:DOWN, _, _, ^pid, _} 45 | refute Process.alive?(pid) 46 | end) 47 | 48 | Process.monitor(pid) 49 | assert_receive {:DOWN, _, _, ^pid, _} 50 | refute Process.alive?(pid) 51 | 52 | :timer.sleep(1) 53 | 54 | stub(Calculator, :add, fn _, _ -> @private_stub end) 55 | assert Calculator.add(3, 7) == @private_stub 56 | end 57 | end 58 | 59 | describe "stub/1 private mode" do 60 | setup :set_mimic_private 61 | 62 | test "stubs all defined functions" do 63 | stub(Calculator) 64 | assert_raise Mimic.UnexpectedCallError, fn -> Calculator.add(3, 7) end 65 | assert_raise Mimic.UnexpectedCallError, fn -> Calculator.mult(4, 9) end 66 | 67 | parent_pid = self() 68 | 69 | spawn_link(fn -> 70 | assert Calculator.add(3, 7) == 10 71 | assert Calculator.mult(4, 9) == 36 72 | send(parent_pid, :ok) 73 | end) 74 | 75 | assert_receive :ok 76 | end 77 | 78 | test "stubbing when mock is not defined" do 79 | assert_raise ArgumentError, fn -> stub(Date) end 80 | end 81 | end 82 | 83 | describe "stub/1 global mode" do 84 | setup :set_mimic_global 85 | 86 | test "stubs all defined functions" do 87 | stub(Calculator) 88 | assert_raise Mimic.UnexpectedCallError, fn -> Calculator.add(2, 2) end 89 | assert_raise Mimic.UnexpectedCallError, fn -> Calculator.mult(2, 2) end 90 | 91 | parent_pid = self() 92 | 93 | spawn_link(fn -> 94 | assert_raise Mimic.UnexpectedCallError, fn -> Calculator.add(2, 2) end 95 | assert_raise Mimic.UnexpectedCallError, fn -> Calculator.mult(2, 2) end 96 | send(parent_pid, :ok) 97 | end) 98 | 99 | assert_receive :ok 100 | end 101 | 102 | test "raises if a different process used stub" do 103 | parent_pid = self() 104 | 105 | spawn_link(fn -> 106 | assert_raise ArgumentError, 107 | "Stub cannot be called by the current process. Only the global owner is allowed.", 108 | fn -> 109 | stub(Calculator) 110 | end 111 | 112 | send(parent_pid, :ok) 113 | end) 114 | 115 | assert_receive :ok 116 | end 117 | 118 | test "stubbing when mock is not defined" do 119 | assert_raise ArgumentError, fn -> stub(Date) end 120 | end 121 | end 122 | 123 | describe "stub_with/2 private mode" do 124 | setup :set_mimic_private 125 | 126 | test "called multiple times" do 127 | stub_with(Calculator, InverseCalculator) 128 | 129 | assert Calculator.add(2, 3) == -1 130 | assert Calculator.add(3, 2) == 1 131 | end 132 | 133 | test "stubs all functions which are not in mocking module" do 134 | stub_with(Calculator, InverseCalculator) 135 | 136 | assert_raise Mimic.UnexpectedCallError, fn -> Calculator.mult(4, 9) end 137 | end 138 | 139 | test "undefined mocking module" do 140 | assert_raise ArgumentError, 141 | "Module MissingModule not defined", 142 | fn -> 143 | stub_with(Calculator, MissingModule) 144 | end 145 | end 146 | 147 | test "undefined mocked module" do 148 | assert_raise ArgumentError, 149 | "Module MissingModule has not been copied. See docs for Mimic.copy/1", 150 | fn -> 151 | stub_with(MissingModule, InverseCalculator) 152 | end 153 | end 154 | end 155 | 156 | describe "stub/3 private mode" do 157 | setup :set_mimic_private 158 | 159 | test "called multiple times" do 160 | Calculator 161 | |> stub(:add, fn x, _y -> x + 2 end) 162 | |> stub(:mult, fn x, _y -> x * 2 end) 163 | 164 | Counter 165 | |> stub(:inc, fn x -> x + 7 end) 166 | |> stub(:add, fn counter, x -> counter + x + 7 end) 167 | 168 | assert Calculator.add(2, :undefined) == 4 169 | assert Calculator.mult(2, 3) == 4 170 | assert Counter.inc(3) == 10 171 | assert Counter.add(3, 10) == 20 172 | end 173 | 174 | test "different processes see different results" do 175 | Calculator 176 | |> stub(:add, fn x, _y -> x + 2 end) 177 | |> stub(:mult, fn x, _y -> x * 2 end) 178 | 179 | assert Calculator.add(2, :undefined) == 4 180 | assert Calculator.mult(2, 3) == 4 181 | 182 | parent_pid = self() 183 | 184 | spawn_link(fn -> 185 | Calculator 186 | |> stub(:add, fn x, _y -> x + 3 end) 187 | |> stub(:mult, fn x, _y -> x * 7 end) 188 | 189 | assert Calculator.add(2, :undefined) == 5 190 | assert Calculator.mult(2, 3) == 14 191 | send(parent_pid, :ok) 192 | end) 193 | 194 | assert_receive :ok 195 | end 196 | 197 | test "does not fail verification if not called" do 198 | stub(Calculator, :add, fn x, y -> x + y end) 199 | verify!() 200 | end 201 | 202 | test "respects calls precedence" do 203 | Calculator 204 | |> stub(:add, fn x, y -> x + y end) 205 | |> expect(:add, fn _, _ -> @expected end) 206 | 207 | assert Calculator.add(1, 1) == @expected 208 | verify!() 209 | end 210 | 211 | test "allows multiple invocations" do 212 | stub(Calculator, :add, fn x, y -> x + y end) 213 | assert Calculator.add(1, 2) == 3 214 | assert Calculator.add(3, 4) == 7 215 | end 216 | 217 | test "invokes stub after expectations are fulfilled" do 218 | Calculator 219 | |> stub(:add, fn _x, _y -> @stubbed end) 220 | |> expect(:add, fn _, _ -> @expected_1 end) 221 | |> expect(:add, fn _, _ -> @expected_2 end) 222 | 223 | assert Calculator.add(1, 1) == @expected_1 224 | assert Calculator.add(1, 1) == @expected_2 225 | assert Calculator.add(1, 1) == @stubbed 226 | verify!() 227 | end 228 | 229 | test "stub redefining overrides" do 230 | Calculator 231 | |> stub(:add, fn x, _y -> x + 2 end) 232 | |> stub(:add, fn x, _y -> x + 3 end) 233 | 234 | assert Calculator.add(2, :undefined) == 5 235 | end 236 | 237 | test "raises if a non copied module is given" do 238 | assert_raise ArgumentError, 239 | "Module NotCopiedModule has not been copied. See docs for Mimic.copy/1", 240 | fn -> 241 | stub(NotCopiedModule, :inc, fn x -> x - 1 end) 242 | end 243 | end 244 | 245 | test "raises if function is not in behaviour" do 246 | assert_raise ArgumentError, "Function oops/2 not defined for Calculator", fn -> 247 | stub(Calculator, :oops, fn x, y -> x + y end) 248 | end 249 | 250 | assert_raise ArgumentError, "Function add/3 not defined for Calculator", fn -> 251 | stub(Calculator, :add, fn x, y, z -> x + y + z end) 252 | end 253 | end 254 | end 255 | 256 | describe "stub/3 global mode" do 257 | setup :set_mimic_global 258 | 259 | test "called multiple times" do 260 | Calculator 261 | |> stub(:add, fn x, _y -> x + 2 end) 262 | |> stub(:mult, fn x, _y -> x * 2 end) 263 | 264 | Counter 265 | |> stub(:inc, fn x -> x + 7 end) 266 | |> stub(:add, fn counter, x -> counter + x + 7 end) 267 | 268 | parent_pid = self() 269 | 270 | spawn_link(fn -> 271 | assert Calculator.add(2, :undefined) == 4 272 | assert Calculator.mult(2, 3) == 4 273 | assert Counter.inc(3) == 10 274 | assert Counter.add(3, 10) == 20 275 | 276 | send(parent_pid, :ok) 277 | end) 278 | 279 | assert_receive :ok 280 | end 281 | 282 | test "respects calls precedence" do 283 | Calculator 284 | |> stub(:add, fn x, y -> x + y end) 285 | |> expect(:add, fn _, _ -> @expected end) 286 | 287 | parent_pid = self() 288 | 289 | spawn_link(fn -> 290 | assert Calculator.add(1, 1) == @expected 291 | 292 | send(parent_pid, :ok) 293 | end) 294 | 295 | assert_receive :ok 296 | verify!() 297 | end 298 | 299 | test "allows multiple invocations" do 300 | stub(Calculator, :add, fn x, y -> x + y end) 301 | 302 | parent_pid = self() 303 | 304 | spawn_link(fn -> 305 | assert Calculator.add(1, 2) == 3 306 | assert Calculator.add(3, 4) == 7 307 | send(parent_pid, :ok) 308 | end) 309 | 310 | assert_receive :ok 311 | end 312 | 313 | test "invokes stub after expectations are fulfilled" do 314 | Calculator 315 | |> stub(:add, fn _x, _y -> @stubbed end) 316 | |> expect(:add, fn _, _ -> @expected end) 317 | |> expect(:add, fn _, _ -> @expected end) 318 | 319 | parent_pid = self() 320 | 321 | spawn_link(fn -> 322 | assert Calculator.add(1, 1) == @expected 323 | assert Calculator.add(1, 1) == @expected 324 | assert Calculator.add(1, 1) == @stubbed 325 | 326 | send(parent_pid, :ok) 327 | end) 328 | 329 | assert_receive :ok 330 | verify!() 331 | end 332 | 333 | test "stub redefining overrides" do 334 | Calculator 335 | |> stub(:add, fn x, _y -> x + 2 end) 336 | |> stub(:add, fn x, _y -> x + 3 end) 337 | 338 | parent_pid = self() 339 | 340 | spawn_link(fn -> 341 | assert Calculator.add(2, :undefined) == 5 342 | 343 | send(parent_pid, :ok) 344 | end) 345 | 346 | assert_receive :ok 347 | end 348 | 349 | test "raises if a different process used stub" do 350 | parent_pid = self() 351 | 352 | spawn_link(fn -> 353 | assert_raise ArgumentError, 354 | "Stub cannot be called by the current process. Only the global owner is allowed.", 355 | fn -> 356 | stub(Calculator, :add, fn x, y -> x + y end) 357 | end 358 | 359 | send(parent_pid, :ok) 360 | end) 361 | 362 | assert_receive :ok 363 | end 364 | 365 | test "raises if a non copied module is given" do 366 | assert_raise ArgumentError, 367 | "Module NotCopiedModule has not been copied. See docs for Mimic.copy/1", 368 | fn -> 369 | stub(NotCopiedModule, :inc, fn x -> x - 1 end) 370 | end 371 | end 372 | 373 | test "raises if function is not defined" do 374 | assert_raise ArgumentError, "Function oops/2 not defined for Calculator", fn -> 375 | stub(Calculator, :oops, fn x, y -> x + y end) 376 | end 377 | 378 | assert_raise ArgumentError, "Function add/3 not defined for Calculator", fn -> 379 | stub(Calculator, :add, fn x, y, z -> x + y + z end) 380 | end 381 | end 382 | end 383 | 384 | describe "expect/4 private mode" do 385 | setup :set_mimic_private 386 | 387 | test "basic expectation" do 388 | Calculator 389 | |> expect(:add, fn x, _y -> x + 2 end) 390 | |> expect(:mult, fn x, _y -> x * 2 end) 391 | 392 | assert Calculator.add(4, :_) == 6 393 | assert Calculator.mult(5, :_) == 10 394 | end 395 | 396 | test "stacking expectations" do 397 | Calculator 398 | |> expect(:add, fn _x, _y -> @expected_1 end) 399 | |> expect(:add, fn _x, _y -> @expected_2 end) 400 | 401 | assert Calculator.add(4, 0) == @expected_1 402 | assert Calculator.add(5, 0) == @expected_2 403 | end 404 | 405 | test "expect multiple calls" do 406 | Calculator 407 | |> expect(:add, 2, fn x, y -> x + y + 1 end) 408 | 409 | assert Calculator.add(4, 3) == 4 + 3 + 1 410 | assert Calculator.add(5, 2) == 5 + 2 + 1 411 | end 412 | 413 | test "expectation not being fulfilled" do 414 | Calculator 415 | |> expect(:add, 2, fn x, _y -> x + 2 end) 416 | |> expect(:mult, fn x, _y -> x * 2 end) 417 | 418 | message = 419 | ~r"\* expected Calculator.mult/2 to be invoked 1 time\(s\) but it has been called 0 time\(s\)" 420 | 421 | assert_raise Mimic.VerificationError, message, fn -> verify!(self()) end 422 | 423 | message = 424 | ~r"\* expected Calculator.add/2 to be invoked 2 time\(s\) but it has been called 0 time\(s\)" 425 | 426 | assert_raise Mimic.VerificationError, message, fn -> verify!(self()) end 427 | 428 | Calculator.add(1, 2) 429 | Calculator.add(2, 3) 430 | Calculator.mult(4, 5) 431 | verify!(self()) 432 | end 433 | 434 | test "expecting when no expectation is defined calls original" do 435 | Calculator 436 | |> expect(:add, fn x, _y -> x + 2 end) 437 | |> expect(:mult, fn x, _y -> x * 2 end) 438 | 439 | assert Calculator.add(4, 0) == 4 + 2 440 | assert Calculator.mult(5, 0) == 5 * 2 441 | 442 | assert Calculator.mult(5, 3) == 15 443 | end 444 | 445 | test "raises if a non copied module is given" do 446 | assert_raise ArgumentError, 447 | "Module NotCopiedModule has not been copied. See docs for Mimic.copy/1", 448 | fn -> 449 | stub(NotCopiedModule, :inc, fn x -> x - 1 end) 450 | end 451 | end 452 | 453 | test "expecting when mock is not defined" do 454 | assert_raise ArgumentError, fn -> expect(Date, :add, fn x, _y -> x + 2 end) end 455 | end 456 | 457 | test "expecting 0 calls should point to reject" do 458 | message = ~r"Expecting 0 calls should be done through Mimic.reject/1" 459 | 460 | assert_raise ArgumentError, message, fn -> 461 | expect(Calculator, :add, 0, fn x, y -> x + y end) 462 | end 463 | end 464 | end 465 | 466 | describe "expect/4 global mode" do 467 | setup :set_mimic_global 468 | 469 | test "basic expectation" do 470 | Calculator 471 | |> expect(:add, fn x, _y -> x + 2 end) 472 | |> expect(:mult, fn x, _y -> x * 2 end) 473 | 474 | parent_pid = self() 475 | 476 | spawn_link(fn -> 477 | assert Calculator.add(4, :_) == 6 478 | assert Calculator.mult(5, :_) == 10 479 | 480 | send(parent_pid, :ok) 481 | end) 482 | 483 | assert_receive :ok 484 | end 485 | 486 | test "stacking expectations" do 487 | Calculator 488 | |> expect(:add, fn _x, _y -> @expected_1 end) 489 | |> expect(:add, fn _x, _y -> @expected_2 end) 490 | 491 | parent_pid = self() 492 | 493 | spawn_link(fn -> 494 | assert Calculator.add(4, 0) == @expected_1 495 | assert Calculator.add(5, 0) == @expected_2 496 | 497 | send(parent_pid, :ok) 498 | end) 499 | 500 | assert_receive :ok 501 | end 502 | 503 | test "expect multiple calls" do 504 | Calculator 505 | |> expect(:add, 2, fn x, y -> x + y + 1 end) 506 | 507 | parent_pid = self() 508 | 509 | spawn_link(fn -> 510 | assert Calculator.add(4, 3) == 4 + 3 + 1 511 | assert Calculator.add(5, 2) == 5 + 2 + 1 512 | 513 | send(parent_pid, :ok) 514 | end) 515 | 516 | assert_receive :ok 517 | end 518 | 519 | test "expectation not being fulfilled" do 520 | Calculator 521 | |> expect(:add, 2, fn x, _y -> x + 2 end) 522 | |> expect(:mult, fn x, _y -> x * 2 end) 523 | 524 | message = 525 | ~r"\* expected Calculator.mult/2 to be invoked 1 time\(s\) but it has been called 0 time\(s\)" 526 | 527 | assert_raise Mimic.VerificationError, message, fn -> verify!(self()) end 528 | 529 | message = 530 | ~r"\* expected Calculator.add/2 to be invoked 2 time\(s\) but it has been called 0 time\(s\)" 531 | 532 | assert_raise Mimic.VerificationError, message, fn -> verify!(self()) end 533 | 534 | parent_pid = self() 535 | 536 | spawn_link(fn -> 537 | Calculator.add(1, 2) 538 | Calculator.add(2, 3) 539 | Calculator.mult(4, 5) 540 | 541 | send(parent_pid, :ok) 542 | end) 543 | 544 | assert_receive :ok 545 | verify!(self()) 546 | end 547 | 548 | test "expecting when no expectation is defined calls original" do 549 | Calculator 550 | |> expect(:add, fn x, _y -> x + 2 end) 551 | |> expect(:mult, fn x, _y -> x * 2 end) 552 | 553 | parent_pid = self() 554 | 555 | spawn_link(fn -> 556 | assert Calculator.add(4, :_) == 6 557 | assert Calculator.mult(5, :_) == 10 558 | 559 | assert Calculator.mult(5, 3) == 15 560 | 561 | send(parent_pid, :ok) 562 | end) 563 | 564 | assert_receive :ok 565 | end 566 | 567 | test "raises if a different process used expect" do 568 | Task.async(fn -> 569 | assert_raise ArgumentError, 570 | "Expect cannot be called by the current process. Only the global owner is allowed.", 571 | fn -> 572 | expect(Calculator, :add, fn x, y -> x + y end) 573 | end 574 | end) 575 | |> Task.await() 576 | end 577 | 578 | test "raises if a non copied module is given" do 579 | assert_raise ArgumentError, 580 | "Module NotCopiedModule has not been copied. See docs for Mimic.copy/1", 581 | fn -> 582 | stub(NotCopiedModule, :inc, fn x -> x - 1 end) 583 | end 584 | end 585 | 586 | test "expecting when mock is not defined" do 587 | assert_raise ArgumentError, fn -> expect(Date, :add, fn x, y -> x + y end) end 588 | end 589 | end 590 | 591 | describe "reject/1 private mode" do 592 | setup :set_mimic_private 593 | 594 | test "expect no call to function" do 595 | reject(&Calculator.add/2) 596 | reject(&Calculator.mult/2) 597 | 598 | message = 599 | ~r"expected Calculator.add/2 to be called 0 time\(s\) but it has been called 1 time\(s\)" 600 | 601 | assert_raise Mimic.UnexpectedCallError, message, fn -> Calculator.add(3, 7) end 602 | 603 | message = 604 | ~r"expected Calculator.mult/2 to be called 0 time\(s\) but it has been called 1 time\(s\)" 605 | 606 | assert_raise Mimic.UnexpectedCallError, message, fn -> Calculator.mult(3, 7) end 607 | end 608 | 609 | test "expectation being fulfilled" do 610 | reject(&Calculator.add/2) 611 | reject(&Calculator.mult/2) 612 | 613 | verify!(self()) 614 | end 615 | 616 | test "raises if a non copied module is given" do 617 | assert_raise ArgumentError, 618 | "Module NotCopiedModule has not been copied. See docs for Mimic.copy/1", 619 | fn -> 620 | stub(NotCopiedModule, :inc, fn x -> x - 1 end) 621 | end 622 | end 623 | 624 | test "expecting when mock is not defined" do 625 | assert_raise ArgumentError, fn -> reject(&Date.add/2) end 626 | end 627 | end 628 | 629 | describe "reject/1 global mode" do 630 | setup :set_mimic_global 631 | 632 | test "basic expectation" do 633 | reject(&Calculator.add/2) 634 | reject(&Calculator.mult/2) 635 | 636 | parent_pid = self() 637 | 638 | spawn_link(fn -> 639 | assert_raise Mimic.UnexpectedCallError, fn -> Calculator.add(4, :_) end 640 | assert_raise Mimic.UnexpectedCallError, fn -> Calculator.mult(4, :_) end 641 | 642 | send(parent_pid, :ok) 643 | end) 644 | 645 | assert_receive :ok 646 | end 647 | 648 | test "raises if a different process used expect" do 649 | Task.async(fn -> 650 | assert_raise ArgumentError, 651 | "Reject cannot be called by the current process. Only the global owner is allowed.", 652 | fn -> 653 | reject(&Calculator.add/2) 654 | end 655 | end) 656 | |> Task.await() 657 | end 658 | 659 | test "expecting when mock is not defined" do 660 | assert_raise ArgumentError, fn -> reject(&Date.add/2) end 661 | end 662 | end 663 | 664 | describe "reject/3 private mode" do 665 | setup :set_mimic_private 666 | 667 | test "expect no call to function" do 668 | reject(Calculator, :add, 2) 669 | reject(Calculator, :mult, 2) 670 | 671 | message = 672 | ~r"expected Calculator.add/2 to be called 0 time\(s\) but it has been called 1 time\(s\)" 673 | 674 | assert_raise Mimic.UnexpectedCallError, message, fn -> Calculator.add(3, 7) end 675 | 676 | message = 677 | ~r"expected Calculator.mult/2 to be called 0 time\(s\) but it has been called 1 time\(s\)" 678 | 679 | assert_raise Mimic.UnexpectedCallError, message, fn -> Calculator.mult(3, 7) end 680 | end 681 | 682 | test "expectation being fulfilled" do 683 | reject(Calculator, :add, 2) 684 | reject(Calculator, :mult, 2) 685 | 686 | verify!(self()) 687 | end 688 | 689 | test "raises if a non copied module is given" do 690 | assert_raise ArgumentError, 691 | "Module NotCopiedModule has not been copied. See docs for Mimic.copy/1", 692 | fn -> 693 | stub(NotCopiedModule, :inc, fn x -> x - 1 end) 694 | end 695 | end 696 | 697 | test "expecting when mock is not defined" do 698 | assert_raise ArgumentError, fn -> reject(Date, :add, 2) end 699 | end 700 | end 701 | 702 | describe "reject/3 global mode" do 703 | setup :set_mimic_global 704 | 705 | test "basic expectation" do 706 | reject(Calculator, :add, 2) 707 | reject(Calculator, :mult, 2) 708 | 709 | parent_pid = self() 710 | 711 | spawn_link(fn -> 712 | assert_raise Mimic.UnexpectedCallError, fn -> Calculator.add(4, :_) end 713 | assert_raise Mimic.UnexpectedCallError, fn -> Calculator.mult(4, :_) end 714 | 715 | send(parent_pid, :ok) 716 | end) 717 | 718 | assert_receive :ok 719 | end 720 | 721 | test "raises if a different process used expect" do 722 | Task.async(fn -> 723 | assert_raise ArgumentError, 724 | "Reject cannot be called by the current process. Only the global owner is allowed.", 725 | fn -> 726 | reject(Calculator, :add, 2) 727 | end 728 | end) 729 | |> Task.await() 730 | end 731 | 732 | test "expecting when mock is not defined" do 733 | assert_raise ArgumentError, fn -> reject(Date, :add, 2) end 734 | end 735 | end 736 | 737 | describe "allow/3" do 738 | setup :set_mimic_private 739 | setup :verify_on_exit! 740 | 741 | test "uses $callers property from Task to allow" do 742 | Calculator 743 | |> expect(:add, 2, fn x, y -> x + y end) 744 | |> expect(:mult, fn x, y -> x * y end) 745 | |> expect(:add, fn _, _ -> 0 end) 746 | 747 | task = 748 | Task.async(fn -> 749 | assert Calculator.add(2, 3) == 5 750 | assert Calculator.add(3, 2) == 5 751 | end) 752 | 753 | Task.await(task) 754 | 755 | assert Calculator.add(:whatever, :whatever) == 0 756 | assert Calculator.mult(3, 2) == 6 757 | end 758 | 759 | test "nested callers are allowed as well" do 760 | Calculator 761 | |> expect(:add, 2, fn x, y -> x + y end) 762 | |> expect(:mult, fn x, y -> x * y end) 763 | |> expect(:add, fn _, _ -> 0 end) 764 | 765 | task = 766 | Task.async(fn -> 767 | assert Calculator.add(2, 3) == 5 768 | assert Calculator.add(3, 2) == 5 769 | 770 | inner_task = 771 | Task.async(fn -> 772 | assert Calculator.add(:whatever, :whatever) == 0 773 | assert Calculator.mult(3, 2) == 6 774 | end) 775 | 776 | Task.await(inner_task) 777 | end) 778 | 779 | Task.await(task) 780 | end 781 | 782 | test "allows different processes to share mocks from parent process" do 783 | parent_pid = self() 784 | 785 | child_pid = 786 | spawn_link(fn -> 787 | receive do 788 | :call_mock -> 789 | add_result = Calculator.add(1, 1) 790 | mult_result = Calculator.mult(1, 1) 791 | send(parent_pid, {:verify, add_result, mult_result}) 792 | end 793 | end) 794 | 795 | Calculator 796 | |> expect(:add, fn _, _ -> @expected end) 797 | |> stub(:mult, fn _, _ -> @stubbed end) 798 | |> allow(self(), child_pid) 799 | 800 | send(child_pid, :call_mock) 801 | 802 | assert_receive {:verify, add_result, mult_result} 803 | assert add_result == @expected 804 | assert mult_result == @stubbed 805 | end 806 | 807 | test "allows different processes to share mocks from parent process when allow is defined first" do 808 | parent_pid = self() 809 | 810 | child_pid = 811 | spawn_link(fn -> 812 | receive do 813 | :call_mock -> 814 | add_result = Calculator.add(1, 1) 815 | mult_result = Calculator.mult(1, 1) 816 | send(parent_pid, {:verify, add_result, mult_result}) 817 | end 818 | end) 819 | 820 | Calculator 821 | |> allow(self(), child_pid) 822 | |> expect(:add, fn _, _ -> @expected end) 823 | |> stub(:mult, fn _, _ -> @stubbed end) 824 | 825 | send(child_pid, :call_mock) 826 | 827 | assert_receive {:verify, add_result, mult_result} 828 | assert add_result == @expected 829 | assert mult_result == @stubbed 830 | end 831 | 832 | test "doesn't raise if no expectation defined" do 833 | child_pid = spawn_link(fn -> :ok end) 834 | 835 | Calculator 836 | |> allow(self(), child_pid) 837 | end 838 | 839 | test "allows different processes to share mocks from child process" do 840 | parent_pid = self() 841 | 842 | Calculator 843 | |> expect(:add, fn _, _ -> @expected end) 844 | |> stub(:mult, fn _, _ -> @stubbed end) 845 | 846 | spawn_link(fn -> 847 | Calculator 848 | |> allow(parent_pid, self()) 849 | 850 | assert Calculator.add(1, 1) == @expected 851 | assert Calculator.mult(1, 1) == @stubbed 852 | send(parent_pid, :ok) 853 | end) 854 | 855 | assert_receive :ok 856 | end 857 | 858 | test "allowances are transitive" do 859 | parent_pid = self() 860 | 861 | child_pid = 862 | spawn_link(fn -> 863 | receive do 864 | :call_mock -> 865 | add_result = Calculator.add(1, 1) 866 | mult_result = Calculator.mult(1, 1) 867 | send(parent_pid, {:verify, add_result, mult_result}) 868 | end 869 | end) 870 | 871 | transitive_pid = 872 | spawn_link(fn -> 873 | receive do 874 | :allow_mock -> 875 | Calculator 876 | |> allow(self(), child_pid) 877 | 878 | send(child_pid, :call_mock) 879 | end 880 | end) 881 | 882 | Calculator 883 | |> expect(:add, fn _, _ -> @expected end) 884 | |> stub(:mult, fn _, _ -> @stubbed end) 885 | |> allow(self(), transitive_pid) 886 | 887 | send(transitive_pid, :allow_mock) 888 | 889 | receive do 890 | {:verify, add_result, mult_result} -> 891 | assert add_result == @expected 892 | assert mult_result == @stubbed 893 | verify!() 894 | after 895 | 1000 -> verify!() 896 | end 897 | end 898 | 899 | test "allowances are reclaimed if the owner process dies" do 900 | parent_pid = self() 901 | 902 | pid = 903 | spawn_link(fn -> 904 | Calculator 905 | |> expect(:add, fn _, _ -> @expected end) 906 | |> stub(:mult, fn _, _ -> @stubbed end) 907 | |> allow(self(), parent_pid) 908 | end) 909 | 910 | Process.monitor(pid) 911 | assert_receive {:DOWN, _, _, ^pid, _} 912 | refute Process.alive?(pid) 913 | 914 | :timer.sleep(1) 915 | 916 | assert Calculator.add(1, 3) == 4 917 | 918 | Calculator 919 | |> expect(:add, fn x, y -> x + y + 7 end) 920 | 921 | assert Calculator.add(1, 1) == 9 922 | end 923 | 924 | test "raises if you try to allow process while in global mode" do 925 | set_mimic_global() 926 | parent_pid = self() 927 | child_pid = spawn_link(fn -> Process.sleep(:infinity) end) 928 | 929 | spawn_link(fn -> 930 | assert_raise ArgumentError, "Allow must not be called when mode is global.", fn -> 931 | Calculator 932 | |> allow(self(), child_pid) 933 | end 934 | 935 | send(parent_pid, :ok) 936 | end) 937 | 938 | assert_receive :ok 939 | end 940 | end 941 | 942 | describe "mode/0 global mode" do 943 | setup :set_mimic_global 944 | 945 | test "returns :global" do 946 | assert Mimic.mode() == :global 947 | end 948 | end 949 | 950 | describe "mode/0 private mode" do 951 | setup :set_mimic_private 952 | 953 | test "returns :private" do 954 | assert Mimic.mode() == :private 955 | end 956 | end 957 | 958 | describe "behaviours" do 959 | test "copies behaviour attributes" do 960 | behaviours = 961 | Calculator.module_info(:attributes) 962 | |> Keyword.get_values(:behaviour) 963 | |> List.flatten() 964 | 965 | assert AddAdapter in behaviours 966 | assert MultAdapter in behaviours 967 | end 968 | end 969 | 970 | describe "copy/1 with duplicates" do 971 | setup :set_mimic_private 972 | 973 | test "stubs still stub" do 974 | parent_pid = self() 975 | 976 | Mimic.copy(Calculator) 977 | Mimic.copy(Calculator) 978 | 979 | Calculator 980 | |> stub(:add, fn x, y -> 981 | send(parent_pid, {:add, x, y}) 982 | @stubbed 983 | end) 984 | 985 | Mimic.copy(Calculator) 986 | 987 | assert Calculator.add(1, 2) == @stubbed 988 | assert_receive {:add, 1, 2} 989 | end 990 | end 991 | 992 | describe "call_original/3" do 993 | setup :set_mimic_private 994 | 995 | test "calls original function even if it has been is stubbed" do 996 | stub_with(Calculator, InverseCalculator) 997 | 998 | assert call_original(Calculator, :add, [1, 2]) == 3 999 | end 1000 | 1001 | test "calls original function even if it has been rejected as a module function" do 1002 | Mimic.reject(Calculator, :add, 2) 1003 | 1004 | assert call_original(Calculator, :add, [1, 2]) == 3 1005 | end 1006 | 1007 | test "calls original function even if it has been rejected as a capture" do 1008 | Mimic.reject(&Calculator.add/2) 1009 | 1010 | assert call_original(Calculator, :add, [1, 2]) == 3 1011 | end 1012 | 1013 | test "when called on a function that has not been stubbed" do 1014 | assert call_original(Calculator, :add, [1, 2]) == 3 1015 | end 1016 | 1017 | test "when called on a module that does not exist" do 1018 | assert_raise ArgumentError, "Function add/2 not defined for NonExistentModule", fn -> 1019 | call_original(NonExistentModule, :add, [1, 2]) 1020 | end 1021 | end 1022 | 1023 | test "when called on a function that does not exist" do 1024 | assert_raise ArgumentError, "Function non_existent_call/2 not defined for Calculator", fn -> 1025 | call_original(Calculator, :non_existent_call, [1, 2]) 1026 | end 1027 | end 1028 | end 1029 | 1030 | describe "structs" do 1031 | setup :set_mimic_private 1032 | 1033 | test "copies struct fields with default values" do 1034 | Structs 1035 | |> stub(:foo, fn -> @stubbed end) 1036 | 1037 | assert Structs.__struct__() == %Structs{ 1038 | foo: nil, 1039 | bar: nil, 1040 | default: "123", 1041 | map_default: %{} 1042 | } 1043 | end 1044 | 1045 | if @elixir_version >= 1.18 do 1046 | test "copies struct fields" do 1047 | StructNoEnforceKeys 1048 | |> stub(:bar, fn -> @stubbed end) 1049 | 1050 | assert StructNoEnforceKeys.__info__(:struct) == [ 1051 | %{field: :foo, default: nil}, 1052 | %{field: :bar, default: nil} 1053 | ] 1054 | end 1055 | else 1056 | test "copies struct fields" do 1057 | StructNoEnforceKeys 1058 | |> stub(:bar, fn -> @stubbed end) 1059 | 1060 | assert StructNoEnforceKeys.__info__(:struct) == [ 1061 | %{field: :foo, required: false}, 1062 | %{field: :bar, required: false} 1063 | ] 1064 | end 1065 | end 1066 | 1067 | test "protocol still works" do 1068 | Structs 1069 | |> stub(:foo, fn -> @stubbed end) 1070 | 1071 | s = %Structs{foo: "abc", bar: "def"} 1072 | 1073 | assert to_string(s) == "{abc} - {def}" 1074 | end 1075 | end 1076 | 1077 | describe "calls/1" do 1078 | setup :set_mimic_private 1079 | 1080 | test "returns calls for stubbed functions" do 1081 | stub(Calculator, :add, fn x, y -> x + y end) 1082 | 1083 | Calculator.add(1, 2) 1084 | Calculator.add(3, 4) 1085 | 1086 | assert Mimic.calls(&Calculator.add/2) == [[1, 2], [3, 4]] 1087 | assert Mimic.calls(&Calculator.add/2) == [] 1088 | end 1089 | end 1090 | 1091 | describe "calls/3 private mode" do 1092 | setup :set_mimic_private 1093 | 1094 | test "returns calls for stubbed functions" do 1095 | stub(Calculator, :add, fn x, y -> x + y end) 1096 | 1097 | Calculator.add(1, 2) 1098 | Calculator.add(3, 4) 1099 | 1100 | assert Mimic.calls(Calculator, :add, 2) == [[1, 2], [3, 4]] 1101 | assert Mimic.calls(Calculator, :add, 2) == [] 1102 | end 1103 | 1104 | test "returns calls for expected functions" do 1105 | expect(Calculator, :add, 2, fn x, y -> x + y end) 1106 | 1107 | Calculator.add(1, 2) 1108 | Calculator.add(3, 4) 1109 | 1110 | assert Mimic.calls(Calculator, :add, 2) == [[1, 2], [3, 4]] 1111 | assert Mimic.calls(Calculator, :add, 2) == [] 1112 | end 1113 | 1114 | test "return calls from child pid as well" do 1115 | parent_pid = self() 1116 | 1117 | Calculator 1118 | |> expect(:add, fn _, _ -> @expected end) 1119 | |> stub(:mult, fn _, _ -> @stubbed end) 1120 | 1121 | spawn_link(fn -> 1122 | Calculator 1123 | |> allow(parent_pid, self()) 1124 | 1125 | assert Calculator.add(1, 2) == @expected 1126 | assert Calculator.mult(3, 4) == @stubbed 1127 | send(parent_pid, :ok) 1128 | end) 1129 | 1130 | assert_receive :ok 1131 | assert Mimic.calls(&Calculator.add/2) == [[1, 2]] 1132 | assert Mimic.calls(&Calculator.add/2) == [] 1133 | 1134 | assert Mimic.calls(&Calculator.mult/2) == [[3, 4]] 1135 | assert Mimic.calls(&Calculator.mult/2) == [] 1136 | end 1137 | 1138 | test "raises when mock is not defined" do 1139 | assert_raise ArgumentError, fn -> Mimic.calls(Date, :add, 2) end 1140 | end 1141 | 1142 | test "raises for non-existent functions" do 1143 | assert_raise ArgumentError, 1144 | "Function invalid/2 not defined for Calculator", 1145 | fn -> Mimic.calls(Calculator, :invalid, 2) end 1146 | end 1147 | 1148 | test "raises for non-existent modules" do 1149 | assert_raise ArgumentError, "Function add/2 not defined for NonExistentModule", fn -> 1150 | Mimic.calls(NonExistentModule, :add, 2) 1151 | end 1152 | end 1153 | end 1154 | 1155 | describe "calls/3 global mode" do 1156 | setup :set_mimic_global 1157 | 1158 | test "returns calls for stubbed functions" do 1159 | stub(Calculator, :add, fn x, y -> x + y end) 1160 | 1161 | parent_pid = self() 1162 | 1163 | spawn_link(fn -> 1164 | Calculator.add(1, 2) 1165 | Calculator.add(3, 4) 1166 | send(parent_pid, :ok) 1167 | end) 1168 | 1169 | assert_receive :ok 1170 | assert Mimic.calls(Calculator, :add, 2) == [[1, 2], [3, 4]] 1171 | assert Mimic.calls(Calculator, :add, 2) == [] 1172 | end 1173 | 1174 | test "returns calls for expected functions" do 1175 | expect(Calculator, :add, 2, fn x, y -> x + y end) 1176 | 1177 | parent_pid = self() 1178 | 1179 | spawn_link(fn -> 1180 | Calculator.add(1, 2) 1181 | Calculator.add(3, 4) 1182 | send(parent_pid, :ok) 1183 | end) 1184 | 1185 | assert_receive :ok 1186 | assert Mimic.calls(Calculator, :add, 2) == [[1, 2], [3, 4]] 1187 | assert Mimic.calls(Calculator, :add, 2) == [] 1188 | end 1189 | 1190 | test "raises when mock is not defined" do 1191 | assert_raise ArgumentError, fn -> Mimic.calls(Date, :add, 2) end 1192 | end 1193 | 1194 | test "raises for non-existent functions" do 1195 | assert_raise ArgumentError, 1196 | "Function invalid/2 not defined for Calculator", 1197 | fn -> Mimic.calls(Calculator, :invalid, 2) end 1198 | end 1199 | 1200 | test "raises for non-existent modules" do 1201 | assert_raise ArgumentError, "Function add/2 not defined for NonExistentModule", fn -> 1202 | Mimic.calls(NonExistentModule, :add, 2) 1203 | end 1204 | end 1205 | end 1206 | end 1207 | -------------------------------------------------------------------------------- /test/mimic_type_check_test.exs: -------------------------------------------------------------------------------- 1 | defmodule MimicTypeCheckTest do 2 | use ExUnit.Case, async: false 3 | use Mimic 4 | 5 | alias Typecheck.Calculator 6 | 7 | setup :set_mimic_private 8 | 9 | describe "stub/3" do 10 | test "does not raise with correct type" do 11 | stub(Calculator, :add, fn _x, _y -> 42 end) 12 | 13 | assert Calculator.add(2, 7) == 42 14 | end 15 | 16 | test "raises on wrong argument" do 17 | stub(Calculator, :add, fn _x, _y -> 42 end) 18 | 19 | assert_raise( 20 | Mimic.TypeCheckError, 21 | ~r/1st argument value :not_a_number does not match 1st parameter's type number()./, 22 | fn -> Calculator.add(:not_a_number, 7) end 23 | ) 24 | end 25 | 26 | test "raises on wrong return value" do 27 | stub(Calculator, :add, fn _x, _y -> :not_a_number end) 28 | 29 | assert_raise( 30 | Mimic.TypeCheckError, 31 | ~r/Returned value :not_a_number does not match type number()/, 32 | fn -> Calculator.add(77, 7) end 33 | ) 34 | end 35 | end 36 | 37 | defmodule InverseCalculator do 38 | @moduledoc false 39 | @behaviour AddAdapter 40 | def add(_x, _y), do: :not_a_number 41 | end 42 | 43 | describe "stub_with/2" do 44 | test "raises on wrong argument" do 45 | stub_with(Calculator, InverseCalculator) 46 | 47 | assert_raise( 48 | Mimic.TypeCheckError, 49 | ~r/1st argument value :not_a_number does not match 1st parameter's type number()./, 50 | fn -> Calculator.add(:not_a_number, 7) end 51 | ) 52 | end 53 | 54 | test "raises on wrong return value" do 55 | stub_with(Calculator, InverseCalculator) 56 | 57 | assert_raise( 58 | Mimic.TypeCheckError, 59 | ~r/Returned value :not_a_number does not match type number()/, 60 | fn -> Calculator.add(77, 7) end 61 | ) 62 | end 63 | end 64 | 65 | describe "expect/4" do 66 | test "does not raise with correct type" do 67 | expect(Calculator, :add, fn _x, _y -> 42 end) 68 | 69 | assert Calculator.add(13, 7) == 42 70 | end 71 | 72 | test "raises on wrong argument" do 73 | expect(Calculator, :add, fn _x, _y -> 42 end) 74 | 75 | assert_raise( 76 | Mimic.TypeCheckError, 77 | ~r/1st argument value :not_a_number does not match 1st parameter's type number()./, 78 | fn -> Calculator.add(:not_a_number, 7) end 79 | ) 80 | end 81 | 82 | test "raises on wrong return value" do 83 | expect(Calculator, :add, fn _x, _y -> :not_a_number end) 84 | 85 | assert_raise( 86 | Mimic.TypeCheckError, 87 | ~r/Returned value :not_a_number does not match type number()/, 88 | fn -> Calculator.add(77, 7) end 89 | ) 90 | end 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /test/support/test_cover.ex: -------------------------------------------------------------------------------- 1 | defmodule Mimic.TestCover do 2 | @moduledoc false 3 | 4 | @doc false 5 | def start(compile_path, _opts) do 6 | :cover.stop() 7 | :cover.start() 8 | :cover.compile_beam_directory(compile_path |> String.to_charlist()) 9 | 10 | fn -> 11 | execute() 12 | end 13 | end 14 | 15 | defp execute do 16 | {:result, results, _fail} = :cover.analyse(:calls, :function) 17 | 18 | mimic_module_cover = 19 | Enum.any?(results, fn 20 | {{Calculator.Mimic.Original.Module, _, _}, _} -> true 21 | _ -> false 22 | end) 23 | 24 | expected = 25 | {{Calculator, :add, 2}, 9} in results && 26 | {{Calculator, :mult, 2}, 5} in results && 27 | {{NoStubs, :add, 2}, 2} in results && !mimic_module_cover 28 | 29 | unless expected do 30 | IO.puts("Cover results are incorrect!") 31 | throw(:test_cover_failed) 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /test/support/test_modules.ex: -------------------------------------------------------------------------------- 1 | defmodule AddAdapter do 2 | @moduledoc false 3 | @callback add(number(), number()) :: number() 4 | end 5 | 6 | defmodule MultAdapter do 7 | @moduledoc false 8 | @callback mult(number(), number()) :: number() 9 | end 10 | 11 | defmodule Calculator do 12 | @moduledoc false 13 | @behaviour AddAdapter 14 | @behaviour MultAdapter 15 | def add(x, y), do: x + y 16 | def mult(x, y), do: x * y 17 | end 18 | 19 | defmodule InverseCalculator do 20 | @moduledoc false 21 | @behaviour AddAdapter 22 | def add(x, y), do: x - y 23 | end 24 | 25 | defmodule Counter do 26 | @moduledoc false 27 | def inc(counter), do: counter + 1 28 | def dec(counter), do: counter - 1 29 | def add(counter, x), do: counter + x 30 | end 31 | 32 | defmodule Enumerator do 33 | @moduledoc false 34 | def to_list(x, y), do: Enum.to_list(x..y) 35 | end 36 | 37 | defmodule NoStubs do 38 | @moduledoc false 39 | def add(x, y), do: x + y 40 | end 41 | 42 | defmodule NotCopiedModule do 43 | @moduledoc false 44 | def inc(counter), do: counter - 1 45 | end 46 | 47 | defmodule Structs do 48 | @moduledoc false 49 | @enforce_keys [:foo, :bar] 50 | defstruct [:foo, :bar, default: "123", map_default: %{}] 51 | def foo, do: nil 52 | end 53 | 54 | defmodule StructNoEnforceKeys do 55 | @moduledoc false 56 | defstruct [:foo, :bar] 57 | def bar, do: nil 58 | end 59 | 60 | defimpl String.Chars, for: Structs do 61 | def to_string(structs) do 62 | "{#{structs.foo}} - {#{structs.bar}}" 63 | end 64 | end 65 | 66 | defmodule Typecheck.Counter do 67 | @moduledoc false 68 | 69 | @spec inc(number) :: number 70 | def inc(counter), do: counter + 1 71 | 72 | @spec dec(number) :: number 73 | def dec(counter), do: counter - 1 74 | 75 | @spec add(number, number) :: number 76 | def add(counter, x), do: counter + x 77 | end 78 | 79 | defmodule Typecheck.Calculator do 80 | @moduledoc false 81 | @behaviour AddAdapter 82 | @behaviour MultAdapter 83 | 84 | def add(x, y), do: x + y 85 | 86 | @spec mult(integer, integer) :: integer 87 | def mult(x, y), do: x * y 88 | end 89 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | NoStubs.add(1, 2) 2 | NoStubs.add(3, 5) 3 | Calculator.add(2, 3) 4 | Mimic.copy(NoStubs) 5 | Mimic.copy(Calculator) 6 | Mimic.copy(Counter) 7 | Mimic.copy(Structs) 8 | Mimic.copy(StructNoEnforceKeys) 9 | Mimic.copy(Typecheck.Calculator, type_check: true) 10 | Mimic.copy(Typecheck.Counter, type_check: true) 11 | ExUnit.start() 12 | --------------------------------------------------------------------------------