Tests with multiple async messages

I’m writing a MUD. I can move most of the “straight-up logic” into modules so I can TDD it simply… But when I’m doing more end-to-end tests (say, “a mob dies, the process goes away, a new item process for the corpse arrives in the location”), what is a reasonable approach to such a test that has a series of async messages that need to happen?

The documentation for Process.sleep/1 suggests sending a message back to the calling process, but I’m not sure this makes a lot of sense for me in production because … I think the messages would be sent back to the timer process that sends the tick message at a regular interval?
So… I could have my functions take an optional argument that’d be a pid that would then send a message back to that pid… But I’m not sure that’s a good long-term practice.

Thoughts?

Could you simply query the state of whichever processes need to be in whichever state?

I could do that; do you suggest using :sys.get_state/1 ? I’ve been wondering whether this kind of reaching into the internals was acceptable for tests or not…

No, I’d just make the processes themselves queryable in some fashion. It won’t add any overhead, as far as I’m concerned, but will let you check what the current situation is.

I’m actually also curious: Will items have their own life or are they always tied to monsters? If the latter is the case, it might make sense to make it a state machine that transitions into loot, so that you can simply keep the process. This would allow you to know (still) where the process resides without any registry.

Otherwise, to make sure that you know which item to refer to, might be tricky, I suppose. The easiest way to enable looking up items might be a registry where items spawned from monsters are tied to some monster identifier that is unique. That way you know which item (not just type, but actual item) should spawn, and so you can just fetch the PID (that is registered in the item’s init) and query it if you need to.

It’s awkward to me to add functions only for testing purposes, it makes me feel like I’m failing to design a testable system.

I’d probably even prefer going a step further and using more mocks, much as described in this article: http://blog.plataformatec.com.br/2015/10/mocks-and-explicit-contracts/

Items have their own life; no reason for them to be tied to a single monster. I might eventually implement a kind of chain of inheritance, but that’s for further behavior… :slight_smile:

Process.sleep is perfectly safe to use, the process will just suspend for that time. It is most likely implemented with a receive do after time -> :ok end and as their are no messages to receive the receive will just sit and wait until the time is up when it will be rescheduled and can run.

The documentation is basically saying that you should not just sit and wait then check for a message but check with a timeout so you will get the message immediately it arrives. If that makes sense.

But if you want to wait for specific time the Process.sleep is fine.

1 Like

Well… I’m not really waiting “for a specific time”, I’m just waiting “for all messages to have been delivered”.

Ah. If you know which messages you are expecting can’t you just go into receive loop with the right messages and loop over the right number?

I honestly don’t see the problem with adding test functions as this is probably the only way if you want to test specific sequences of events.

I appreciate that it is.

There’s nothing about processes answering calls that indicates an untestable system.

It’s interesting that you don’t want to add calls to a few gen_servers, because that would supposedly indicate that you’ve failed, but adding mocks somehow doesn’t.

To each his own. Do what’s simple for you. I’d have to actually know the point of your tests and why you can’t add calls to understand why you find this to be such a problem.

This is where my newness to Elixir will show. (ping @gon782).

I have a location (GenServer).
I have a mob (GenServer) with a lifespan of 1 in its state.
I have a reaper (GenServer) process.
I have an item supervisor that can create item (GenServer) processes.

The mob is “in the location” (they each have a reference to each other’s id)
When I send the :tick message to the mob, its lifespan will go down to 0. This will trigger a message to the reaper process, which will send a message to the mob to die (stop the genserver, stop related processes) and send a message to the item supervisor to create an item in the location where the dwarf was.

All of this is asynchronous.
… Which, you know, maybe it should be synchronous, I’m just trying to limit the number of synchronous things that happen.

In any case – I don’t know how to “go into the receive loop with the right messages” given that all of this is abstracted under handle_cast and such.

Well, the documentation for :sys in Erlang says that get_state/1 should be used for debugging only. I’m not sure writing tests count as debugging or acceptable use.

I think if I write my own extraction function, I’m basically replacing :sys.get_state/1 and so … Maybe I’m doing something wrong?

I’m not sure that mocks are right, either.

I am looking for perspectives around this topic because I can’t really find any online otherwise.

Yes, I wouldn’t imagine anything other than gen_server:call to be necessary. The location will hold a reference to the item, I assume? In this case, couldn’t you just check for the item? Once again, we end up in a situation where you might want to check for a “correct item”, but this can be sorted out.

@gon782 checking for the correct item is what I’m doing now ( https://github.com/Trevoke/dwarlixir/blob/36c6c43d7e2d44d90c8e2863e41850a744e6c94c/apps/mobs/test/bird_test.exs#L19 ) although I do need the Process.sleep/1 to hopefully wait for all messages to have been delivered everywhere.

One idea if you don’t like the sleep solution is to have an optional parameter to one of the processes, perhaps the reaper, where you can register a process to receive a message when an item is spawned. That way, you could just do a receive for the message and have an after section in the receive that makes sure you never wait too long, but you’ll react in a more robust way to variance in the timing of everything.