Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

eunit: How to test a simple process?

I'm currently writing a test for a module that runs in a simple process started with spawn_link(?MODULE, init, [self()]).

In my eunit tests, I have a setup and teardown function defined and a set of test generators.

all_tests_test_() ->
    {inorder, {
        foreach,
        fun setup/0,
        fun teardown/1,
        [
            fun my_test/1
        ]}
    }.

The setup fun creates the process-under-test:

setup() ->
    {ok, Pid} = protocol:start_link(),
    process_flag(trap_exit,true),
    error_logger:info_msg("[~p] Setting up process ~p~n", [self(), Pid]),
    Pid.

The test looks like this:

my_test(Pid) ->
    [ fun() ->
            error_logger:info_msg("[~p] Sending to ~p~n", [self(), Pid]),
            Pid ! something,
            receive
                Msg -> ?assertMatch(expected_result, Msg)
            after
                500 -> ?assert(false)
            end
        end ].

Most of my modules are gen_server but for this I figured it'll be easier without all gen_server boilerplate code...

The output from the test looks like this:

=INFO REPORT==== 31-Mar-2014::21:20:12 ===
[<0.117.0>] Setting up process <0.122.0>

=INFO REPORT==== 31-Mar-2014::21:20:12 ===
[<0.124.0>] Sending to <0.122.0>

=INFO REPORT==== 31-Mar-2014::21:20:12 ===
[<0.122.0>] Sending expected_result to <0.117.0>
protocol_test: my_test...*failed*
in function protocol_test:'-my_test/1-fun-0-'/0 (test/protocol_test.erl, line 37)
**error:{assertion_failed,[{module,protocol_test},
                   {line,37},
                   {expression,"false"},
                   {expected,true},
                   {value,false}]}

From the Pids you can see that whatever process was running setup (117) was not the same that was running the test case (124). The process under test however is the same (122). This results in a failing test case because the receive never gets the message und runs into the timeout.

Is that the expected behaviour that a new process gets spawned by eunit to run the test case?

An generally, is there a better way to test a process or other asynchronous behaviour (like casts)? Or would you suggest to always use gen_server to have a synchronous interface?

Thanks!

[EDIT]

To clarify, how protocol knows about the process, this is the start_link/0 fun:

start_link() ->
    Pid = spawn_link(?MODULE, init, [self()]),
    {ok, Pid}.

The protocol ist tightly linked to the caller. If the either of them crashes I want the other one to die as well. I know I could use gen_server and supervisors and actually it did that in parts of the application, but for this module, I thought it was a bit over the top.

like image 928
jaw Avatar asked Mar 31 '14 20:03

jaw


People also ask

How do you test Erlang?

Testing is performed by running test suites (sets of test cases) or individual test cases. A test suite is implemented as an Erlang module named <suite_name>_SUITE. erl which contains a number of test cases. A test case is an Erlang function that tests one or more things.

What is Epsilon testing?

The Epsilon Unit Testing Framework (EUnit) EUnit is an unit testing framework specifically designed to test model management tasks, based on EOL and the Ant workflow tasks. It provides assertions for comparing models, files and directories.


2 Answers

did you try:

all_tests_test_() ->
    {inorder, {
        foreach,
        local,
        fun setup/0,
        fun teardown/1,
        [
            fun my_test/1
        ]}
    }.

From the doc, it seems to be what you need.

like image 168
Pascal Avatar answered Nov 12 '22 12:11

Pascal


simple solution

Just like in Pascal answer, adding the local flag to test description might solve some your problem, but it will probably cause you some additional problems in future, especially when you link yourself to created process.

testing processes

General practice in Erlang is that while process abstraction is crucial for writing (designing and thinking about) programs, it is not something that you would expose to user of your code (even if it is you). Instead expecting someone to send you message with proper data, you wrap it in function call

get_me_some_expected_result(Pid) ->
    Pid ! something,
    receive
         Msg -> 
              Msg
    after 500
         timeouted             
    end

and then test this function rather than receiving something "by hand".

To distinguish real timeout from received timeouted atom, one can use some pattern matching, and let it fail in case of error

get_me_some_expected_result(Pid) ->
    Pid ! something,
    receive
         Msg -> 
              {ok, Msg}
    after 500
         timeouted             
    end


in_my_test() ->
    {ok, ValueToBeTested} = get_me_some_expected_result().

In addition, since your process could receive many different messages in meantime, you can make sure that you receive what you think you receive with little pattern-matching and local reference

get_me_some_expected_result(Pid) ->
    Ref = make_ref(),
    Pid ! {something, Ref},
    receive
         {Ref, Msg} -> 
              {ok, Msg}
    after 500
         timeouted             
    end

And now receive will ignore (leave for leter) all messages that will not have same Reg that you send to your process.

major concern

One thing that I do not really understand, is how does process you are testing know where to send back received message? Only logical solution would be getting pid of it's creator during initialization (call to self/0 inside protocol:start_link/0 function). But then our new process can communicate only with it's creator, which might not be something you expect, and which is not how tests are run.

So simplest solution would be sending "return address" with each call; which again could be done in our wrapping function.

get_me_some_expected_result(Pid) ->
    Ref = make_ref(),
    Pid ! {something, Ref, self()},
    receive
         {Ref, Msg} -> 
              {ok, Msg}
    after 500
         timeouted             
    end

Again, anyone who will use this get_me_some_expected_result/1 function will not have to worry about message passing, and testing such functions makes thing extremely easier.

Hope this helps at least a little.

like image 44
mpm Avatar answered Nov 12 '22 13:11

mpm