eproxus / meck

A mocking library for Erlang
http://eproxus.github.io/meck
Apache License 2.0
813 stars 231 forks source link

Add an option to create modules named after their PID #136

Closed kwrooijen closed 9 years ago

kwrooijen commented 9 years ago

First of all: This feature feels like it strays from the purpose of Meck. Therefore if you don't agree with adding this feature I'll understand and will build my own library and re-implement the Meck specific characteristics that I require.

This feature adds the ability to create a new module named after its process id. Why would you want this? The PID is a unique identifier, meaning you can't spawn two of the same "new" modules. This is nice because you can create object-like variable with their own functions.

This is already possible by simply doing:

MyObject = fun(one) -> 1; (two) -> 2 end,
MyObject(one),  %=> 1
MyObject(two).  %=> 2

However it's very limited,

  1. You only have X amount of arguments (can't pattern match different arity)
  2. You can't bind actual functions to them
  3. There is no sense of state

With this change you can create new modules named after their Pid (as an atom) and bind it to a variable. In the test there is an example of this usage.

Dog = meck:new(just_a_dog, [pid_module]),
Cat = meck:new(just_a_cat, [pid_module]),
meck:expect(Dog, stare_at, fun(Target) -> meck_proc:send(Target, "*Stare*") end),
meck:expect(Dog, bark_at,  fun(Target) -> meck_proc:send(Target, "Woof!") end),
meck:expect(Dog, sleep, fun() -> zzz end),
meck:expect(Cat, react, fun () ->
    case meck_proc:get(Cat) of
        "Woof!" -> run_away;
        _       -> just_being_a_cat
    end
end),

Dog:stare_at(Cat), %=> *send message to Cat*
Cat:react(),       %=> just_being_a_cat
Dog:bark_at(Cat),  %=> *send message to Cat*
Cat:react(),       %=> run_away,
Dog:sleep().       %=> zzz,

I don't feel that this really belongs in Meck, but Meck provides everything I need for this feature. And I can't use Meck as a dependency because I need to adjust Meck's implementation to make this work. So if you think this should not be in Meck I'll create my own library for it.

eproxus commented 9 years ago

I have to admit I feel that this is a bit of an edge case, so I'm reluctant to include this in the Meck core itself. I have done similar things in the past using processes started in the test with not much extra code. An alternative could be:

test() ->
    Dog = start(just_a_dog),
    Cat = start(just_a_cat),
    expect(Dog, stare_at, fun(Target) -> send(Target, "*Stare*") end),
    expect(Dog, bark_at,  fun(Target) -> send(Target, "Woof!") end),
    expect(Dog, sleep, fun() -> zzz end),
    expect(Cat, react, fun () ->
        case get(Cat) of
            "Woof!" -> run_away;
            _       -> just_being_a_cat
        end
    end),

    Dog:stare_at(Cat), %=> *send message to Cat*
    Cat:react(),       %=> just_being_a_cat
    Dog:bark_at(Cat),  %=> *send message to Cat*
    Cat:react(),       %=> run_away,
    Dog:sleep().       %=> zzz.

start(Name) ->
    meck:new(just_a_dog, []),
    {Name, spawn_link(fun() -> proc(undefined) end)}.

proc(Data) ->
    receive
         {msg, Msg}  -> proc(Msg);
         {get, From} -> From ! Data, proc(Data)
    end.

expect({Mod, }, Func, Exp) -> meck:expect(Mod, Func, Exp).

send({_Mod, Pid}, Msg) -> Pid ! {msg, Msg}.

get({_Mod, Pid}) ->
    Pid ! {get, self()},
    receive Data -> Data end.

(Not compiled or tested, but should illustrate that it is a quite simple wrapper)

Since it is basically "abusing" Meck to hold test specific data, I think I'd rather keep this outside of Meck. However, if you create something like a meck_state wrapper module (like my example above) I'd be happy to list it in the Meck README!

kwrooijen commented 9 years ago

Your solution looks interesting and I'd much rather have it that way, but one problem is that it registers dog as the atom just_a_dog. But instead I want it to register it as '<0.16.0>' (it's pid). Of course we can unregister just_a_dog and afterwards register it, but that could cause race conditions. Also another thing about this implementation is that you now have 2 processes per mock instead of 1. (one for the mock and one for the state). I honestly can't think of a clean solution for this without implementing it directly in Meck.

So in short: issue 1: no (instant) unique identifier. issue 2: second process for holding state.

I might just close this PR all together as I can't think of a way right now to solve these issues. But I'll think about it over the weekend. Thanks for the reply!

eproxus commented 9 years ago

I think I see it more as separation of concerns. In my opinion, Meck should keep track of the mock only, not act as a middle man passing data back and forth between your test and your program.

I'm not quite following the uniqueness problem of names. Creating a mock module with a certain name (just_a_dog) will and should have only one Meck process tied to it. The module is in a global namespace, so you can only have one mock with the same name anyway.

It might be annoying that there's two processes instead of one, but if that proves to be a bottle neck in your tests, you probably have bigger problems. :smile:

eproxus commented 9 years ago

Closing because of inactivity. Re-open if needed.