Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
/mockPublic

Mocking library for Elixir language

License

NotificationsYou must be signed in to change notification settings

jjh42/mock

Repository files navigation

Build Status

Mock

A mocking library for the Elixir language.

We use the Erlangmeck library to providemodule mocking functionality for Elixir. It uses macros in Elixir to expose thefunctionality in a convenient manner for integrating in Elixir tests.

See the fullreference documentation.

Table of Contents

Installation

First, add mock to yourmix.exs dependencies:

defdepsdo[{:mock,"~> 0.3.0",only::test}]end

and run$ mix deps.get.

with_mock - Mocking a single module

The Mock library provides thewith_mock macro for running tests withmocks.

For a simple example, if you wanted to test some code which callsHTTPotion.get to get a webpage but without actually fetching thewebpage you could do something like this:

defmoduleMyTestdouseExUnit.Case,async:falseimportMocktest"test_name"dowith_mockHTTPotion,[get:fn(_url)->"<html></html>"end]doassert"<html></html>"==HTTPotion.get("http://example.com")endendend

Thewith_mock macro creates a mock module. The keyword list provides a setof mock implementation for functions we want to provide in the mock (inthis case justget). Insidewith_mock we exercise the test codeand we can check that the call was made as we expected usingcalled andproviding the example of the call we expected.

with_mocks - Mocking multiple modules

You can mock up multiple modules withwith_mocks.

defmoduleMyTestdouseExUnit.Case,async:falseimportMocktest"multiple mocks"dowith_mocks([{Map,[],[get:fn(%{},"http://example.com")->"<html></html>"end]},{String,[],[reverse:fn(x)->2*xend,length:fn(_x)->:okend]}])doassertMap.get(%{},"http://example.com")=="<html></html>"assertString.reverse(3)==6assertString.length(3)==:okendendend

The second parameter of each tuple isopts - a list of optional argumentspassed to meck.

test_with_mock - with_mock helper

An additional convenience macrotest_with_mock is supplied which internallydelegates towith_mock. Allowing the above test to be written as follows:

defmoduleMyTestdouseExUnit.Case,async:falseimportMocktest_with_mock"test_name",HTTPotion,[get:fn(_url)->"<html></html>"end]doHTTPotion.get("http://example.com")assert_calledHTTPotion.get("http://example.com")endend

Thetest_with_mock macro can also be passed a context argumentallowing the sharing of information between callbacks and the test

defmoduleMyTestdouseExUnit.Case,async:falseimportMocksetupdodoc="<html></html>"{:ok,doc:doc}endtest_with_mock"test_with_mock with context",%{doc:doc},HTTPotion,[],[get:fn(_url,_headers)->docend]doHTTPotion.get("http://example.com",[foo::bar])assert_calledHTTPotion.get("http://example.com",:_)endend

setup_with_mocks - Configure all tests to have the same mocks

Thesetup_with_mocks mocks up multiple modules prior to every single testalong while calling the provided setup block. It is simply an integration of thewith_mocks macro available in this module along with thesetupmacro defined in elixir'sExUnit.

defmoduleMyTestdouseExUnit.Case,async:falseimportMocksetup_with_mocks([{Map,[],[get:fn(%{},"http://example.com")->"<html></html>"end]}])dofoo="bar"{:ok,foo:foo}endtest"setup_with_mocks"doassertMap.get(%{},"http://example.com")=="<html></html>"endend

The behaviour of a mocked module within the setup call can be overridden using anyof the methods above in the scope of a specific test. Providing this functionalitybysetup_all is more difficult, and as such,setup_all_with_mocks is not currentlysupported.

Currently, mocking modules cannot be done asynchronously, so make sure that youare not usingasync: true in any module where you are testing.

Also, because of the way mock overrides the module, it must be defined in aseparate file from the test file.

Mocking input dependent output

If you have a function that should return different values depending on what theinput is, you can do as follows:

defmoduleMyTestdouseExUnit.Case,async:falseimportMocktest"mock functions with multiple returns"dowith_mock(Map,[get:fn(%{},"http://example.com")->"<html>Hello from example.com</html>"(%{},"http://example.org")->"<html>example.org says hi</html>"(%{},url)->conditionally_mocked(url)end])doassertMap.get(%{},"http://example.com")=="<html>Hello from example.com</html>"assertMap.get(%{},"http://example.org")=="<html>example.org says hi</html>"assertMap.get(%{},"http://example.xyz")=="<html>Hello from example.xyz</html>"assertMap.get(%{},"http://example.tech")=="<html>example.tech says hi</html>"endenddefconditionally_mocked(url)doconddoString.contains?(url,".xyz")->"<html>Hello from example.xyz</html>"String.contains?(url,".tech")->"<html>example.tech says hi</html>"endendend

Mocking functions with different arities

You can mock functions in the same module with different arity:

defmoduleMyTestdouseExUnit.Case,async:falseimportMocktest"mock functions with different arity"dowith_mockString,[slice:fn(string,range)->stringend,slice:fn(string,range,len)->stringend]doassertString.slice("test",1..3)=="test"assertString.slice("test",1,3)=="test"endendend

Mock repeated calls

You can mock repeated calls to the same functionand arguments to returndifferent results in a series using thein_series call with static values.This does not currently supportfunctions.

Caution: This is only useful in rare instances where the underlying businesslogic is likely to be stateful. If you can avoid it by using different functionarguments, or refactor the function to be stateful, consider that approach first.

defmoduleMyTestdouseExUnit.Case,async:falseimportMocktest"mock repeated calls with in_series"dowith_mockString,[slice:[in_series(["test",1..3],["string1","string2","string3"])]]doassertString.slice("test",1..3)=="string1"assertString.slice("test",1..3)=="string2"assertString.slice("test",1..3)=="string3"endendend

passthrough - partial mocking of a module

By default, only the functions being mocked can be accessed from within the test.Trying to call a non-mocked function from a mocked Module will result in an error.This can be circumvented by passing the:passthrough option like so:

defmoduleMyTestdouseExUnit.Case,async:falseimportMocktest_with_mock"test_name",IO,[:passthrough],[]doIO.puts"hello"assert_calledIO.puts"hello"endend

Assert called - assert a specific function was called

You can check whether or not your mocked module was called.

Assert called - specific value

It is possible to assert that the mocked module was called with a specific input.

defmoduleMyTestdouseExUnit.Case,async:falseimportMocktest"test_name"dowith_mockHTTPotion,[get:fn(_url)->"<html></html>"end]doHTTPotion.get("http://example.com")assert_calledHTTPotion.get("http://example.com")endendend

Assert called - wildcard

It is also possible to assert that the mocked module was called with any valueby passing the:_ wildcard.

defmoduleMyTestdouseExUnit.Case,async:falseimportMocktest"test_name"dowith_mockHTTPotion,[get:fn(_url)->"<html></html>"end]doHTTPotion.get("http://example.com")assert_calledHTTPotion.get(:_)endendend

Assert called - pattern matching

assert_called will check argument equality using== semantics, not pattern matching.For structs, you must provide every property present on the argument as it was called orit will fail. To use pattern matching (useful when you only care about a few properties onthe argument or need to perform advanced matching like regex matching), provide customargument matcher(s) using:meck.is/1.

defmoduleUserdodefstruct[:id,:name,:email]enddefmoduleNetworkdodefupdate(%User{}=user),do:# ...enddefmoduleMyTestdouseExUnit.Case,async:falseimportMocktest"test_name"dowith_mockNetwork,[update:fn(_user)->:okend]douser=%User{id:1,name:"Jane Doe",email:"jane.doe@gmail.com"}Network.update(user)assert_calledNetwork.update(:meck.is(fnuser->assertuser.__struct__==Userassertuser.id==1# matcher must return true when the match succeedstrueend))endendend

Assert not called - assert a specific function was not called

assert_not_called will assert that a mocked function was not called.

defmoduleMyTestdouseExUnit.Case,async:falseimportMocktest"test_name"dowith_mockHTTPotion,[get:fn(_url)->"<html></html>"end]do# Using Wildcardassert_not_calledHTTPotion.get(:_)HTTPotion.get("http://example.com")# Using Specific Valueassert_not_calledHTTPotion.get("http://another-example.com")endendend

Assert called exactly - assert a specific function was called exactly x times

assert_called_exactly will assert that a mocked function was called exactly the expected number of times.

defmoduleMyTestdouseExUnit.Case,async:falseimportMocktest"test_name"dowith_mockHTTPotion,[get:fn(_url)->"<html></html>"end]doHTTPotion.get("http://example.com")HTTPotion.get("http://example.com")# Using Wildcardassert_called_exactlyHTTPotion.get(:_),2# Using Specific Valueassert_called_exactlyHTTPotion.get("http://example.com"),2endendend

Assert called at least - assert a specific function was called at least x times

assert_called_at_least will assert that a mocked function was called at least the expected number of times.

defmoduleMyTestdouseExUnit.Case,async:falseimportMocktest"test_name"dowith_mockHTTPotion,[get:fn(_url)->"<html></html>"end]doHTTPotion.get("http://example.com")HTTPotion.get("http://example.com")HTTPotion.get("http://example.com")# Using Wildcardassert_called_at_leastHTTPotion.get(:_),2# Using Specific Valueassert_called_at_leastHTTPotion.get("http://example.com"),2endendend

Assert call order

call_history will return themeck.history(Module) allowing you assert on the order of the function invocation:

defmoduleMyTestdouseExUnit.Case,async:falseimportMocktest"test_name"dowith_mockHTTPotion,[get:fn(_url)->"<html></html>"end]doHTTPotion.get("http://example.com")assertcall_history(HTTPotion)==[{pid,{HTTPotion,:get,["http://example.com"]},"<html></html>"}]endendend

You can use any valid Elixir pattern matching/multiple function heads to accomplishthis more succinctly, but remember that the matcher will be executed forall functioncalls, so be sure to include a fallback case that returnsfalse. For mocked functionswith multiple arguments, you must include a matcher/pattern for each argument.

defmoduleNetwork.V2dodefupdate(%User{}=user,changes),do:# ...defupdate(id,changes)whenis_integer(id),do:# ...defupdate(_,_),do:# ...enddefmoduleMyTestdouseExUnit.Case,async:falseimportMocktest"test_name"dowith_mockNetwork.V2,[update:fn(_user,_changes)->:okend]doNetwork.V2.update(%User{id:456,name:"Jane Doe"},%{name:"John Doe"})Network.V2.update(123,%{name:"John Doe",email:"john.doe@gmail.com"})Network.V2.update(nil,%{})# assert that `update` was called with user id 456assert_calledNetwork.V2.update(:meck.is(fn%User{id:456}->true_->falseend),:_)# assert that `update` was called with an email changeassert_calledNetwork.V2.update(:_,:meck.is(fn%{email:"john.doe@gmail.com"}->true_->falseend))endendend

NOT SUPPORTED

Mocking internal function calls

A common issue a lot of developers run into is Mock's lack of support for mockinginternal functions. Mock will behave as follows:

defmoduleMyApp.IndirectModdodefvaluedo1enddefindirect_valuedovalue()enddefindirect_value_2doMyApp.IndirectMod.value()endend
defmoduleMyTestdouseExUnit.Case,async:falseimportMocktest"indirect mock"dowith_mocks([{MyApp.IndirectMod,[:passthrough],[value:fn->2end]},])do# The following assert succeedsassertMyApp.IndirectMod.indirect_value_2()==2# The following assert also succeedsassertMyApp.IndirectMod.indirect_value()==1endendend

It is important to understand that only fully qualified function calls get mocked.The reason for this is because of the way Meck is structured. Meck creates a thin wrapper module with the name of the mocked module (and passes through any calls to the originalModule in case passthrough is used). The original module is renamed, but otherwise unmodified. Once the call enters the original module, the local function call jumps stay in the module.

Big thanks to @eproxus (author of Meck) who helped explain this to me. We're lookinginto some alternatives to help solve this, but it is something to be aware of in the meantime. The issue is being tracked inIssue 71.

In order to workaround this issue, theindirect_value can be rewritten like so:

defindirect_valuedo__MODULE__.value()end

Or, like so:

defindirect_valuedoMyApp.IndirectMod.value()end

Mocking macros

Currently mocking macros is not supported. For example this will not work becauseLogger.error/1 is a macro:

with_mockLogger,[error:fn(_)->42end]doassertLogger.error("msg")==42end

This code will give you this error:Erlang error: {:undefined_function, {Logger, :error, 1}}

As a workaround, you may define a wrapper function for the macro you need to invoke:

defmoduleMyModuledodeflog_error(arg)doLogger.error(arg)endend

Then in your test you can mock that wrapper function:

with_mockMyModule,[log_error:fn(_)->42end]doassertMyModule.log_error("msg")==42end

Tips

The use of mocking can be somewhat controversial. I personally think that itworks well for certain types of tests. Certainly, you should not overuse it. Itis best to write as much as possible of your code as pure functions which don'trequire mocking to test. However, when interacting with the real world (or webservices, users etc.) sometimes side-effects are necessary. In these cases,mocking is one useful approach for testing this functionality.

Also, note that Mock has a global effect so if you are using Mocks in multipletests setasync: false so that only one test runs at a time.

Help

Open an issue.

Publishing New Package Versions

For library maintainers, the following is an example of how to publish new versions of the package. Run the following commands assuming you incremented the version in themix.exs file from 0.3.4 to 0.3.5:

git commit -am "Increase version from 0.3.4 to 0.3.5"git tag -a v0.3.5 -m "Git tag 0.3.5"git push origin --tagsmix hex.publish

Suggestions

I'd welcome suggestions for improvements or bugfixes. Just open an issue.


[8]ページ先頭

©2009-2025 Movatter.jp