In Erlang's EUnit, assert that a function was called with an anonymous function

374 views Asked by At

I have some code that looks like this:

-module(ca_data).
-export([delete_ca_data/1]).

% ...

delete_ca_data(N) when N < 1 -> ?NEGATIVE_ID_ERROR;
delete_ca_data(N) -> util_db_generic:delete_object(ca_data, N, fun(_) -> ok end).

% ...

And I have some test code that looks like this:

wrap_meck(MockMod, MockArgs, F) ->
    meck:new(MockMod, MockArgs),
    try
        F()
    catch Ex ->
        throw(Ex)
    after
        meck:unload(MockMod)
    end.

delete_ca_data_test() ->
    F = fun() ->
        meck:expect(util_db_generic, delete_object, fun (_, _, _) -> ok end),

        ?assertEqual(?NEGATIVE_ID_ERROR, ca_data:delete_ca_data(-1)),

        ?assertEqual([], meck:history(util_db_generic)),

        meck:expect(util_db_generic, delete_object, fun(_, _, _) -> ok end),
        ?assertEqual(ok, ca_data:delete_ca_data(1)),

        ?assertEqual(
            [{self(), {util_db_generic, delete_object, [ca_data, 1, fun(_) -> ok end]}, ok}], % Uh-oh
            meck:history(util_db_generic)
        )
    end,
    wrap_meck(util_db_generic, [], F).

Unfortunately, the util_db_generic:delete_object function is called with a function created in the module under test.

This provides some issues when trying to assert the history of the function calls (for example, on the line marked "Uh-oh"). Since this function is created in-situ (and the memory pointer is effectively random), it's challenging to assert what this function should look like. This code does not result in any compilation error. However, the asserts will never return as valid, since there are two different functions being asserted against each other (one created in the ca_data module, and one in the testcase). I tried using ?assertMatch and changing the fun(_) -> ok end's out for _s, but I am getting this compilation error on the assert line:

illegal pattern

How can I match these results?

2

There are 2 answers

1
distortedsignal On

While the Erlang documentation is usually good, this is one point where it lets the user down. The assertMatch documentation states:

Evaluates Expr and matches the result against GuardedPattern, if testing is enabled. ... GuardedPattern can be anything that you can write on the left hand side of the -> symbol in a case-clause, except that it cannot contain comma-separated guard tests.

And the documentation for self() states:

Allowed in guard tests.

However, the self() function cannot be used this the assertMatch macro. I am unclear on why, but experiment has proved this out.


EDIT 1:

The plot thickens! When I edit the test code to look like this:

delete_ca_data_test() ->
    F = fun() ->
        meck:expect(util_db_generic, delete_object, fun (_, _, _) -> ok end),

        ?assertEqual(?NEGATIVE_ID_ERROR, ca_data:delete_ca_data(-1)),

        ?assertEqual([], meck:history(util_db_generic)),

        meck:expect(util_db_generic, delete_object, fun(_, _, _) -> ok end),
        ?assertEqual(ok, ca_data:delete_ca_data(1)),

        Self = self(),

        ?assertEqual(
            [{Self, {util_db_generic, delete_object, [ca_data, 1, _]}, ok}], % Uh-oh
            meck:history(util_db_generic)
        )
    end,
    wrap_meck(util_db_generic, [], F).

the code compiles and runs correctly! This is, to me, a surprising result! The function (that should be able to be used in guards) is not allowed, but the return from the function is! Interesting!

0
Adam Lindberg On

The assertion you want might look something like this:

?assertMatch(
    [{Self, {util_db_generic, delete_object, [ca_data, 1, Fun]}, ok}] when Self == self() andalso is_function(Fun, 1), % Yay?
    meck:history(util_db_generic)
)

This matches the ?assertMatch documentation, in that you would write the same in a case statement like this:

case meck:history(util_db_generic) of
    [{Self, {util_db_generic, delete_object, [ca_data, 1, Fun]}, ok}] when Self == self() andalso is_function(Fun, 1) ->
        true;
    _ ->
        false
end

(Note, the case statement could have been shorter with a , in-between the guards but that does not work in the macro so here we have to use andalso instead)

The reason you have to match the pid to Self first and then check it in a guard is that no function calls are allowed in match expressions:

1> [self()] = [self()].
* 1:2: illegal pattern

Therefore, you have to bind it first and then check the value in a guard. If course, what you wrote above, binding Self outside of the expression also works. It is just a matter of taste and/or readability (in fact, I think binding Self first is clearer).