It's hard to argue without having seen any code. From your description it sounds like you have one class that does all the things. This may already be a reason that makes testing harder than it has to be.
When it comes to unit tests, I like to stick with Michael Feathers definition:
A test is not a unit test if:
- it talks to the database.
- it communicates across the network.
- it touches the file system.
- it can't run at the same time as any of your other unit tests.
- you have to do special things to your environment (such as editing config files) to run it.
Mocking the Ratchet and RabbitMQ connections may seem like a way to make a test fit the above definition, but there is also this other phrase: "Don't mock what you don't own". Although it's not a hard rule, I think it's a pretty good guideline. There are a lot of articles on that topic. This should give you a good overview
Ratchet is a bit special with the server and event loop part, though. But it's well tested, so you don't have to do it. My suggestion: Keep the integration with Ratchet so thin, that there isn't much left to test anyway and just skip the test.
final class MyRatchetApp implements MessageComponentInterface
{
private MessageProcessor $processor;
public function __construct(MessageProcessor $processor)
{
$this->processor = $processor;
}
public function onMessage(ConnectionInterface $from, $msg)
{
$this->processor->handle($msg);
}
// ...
}
This leaves you with a MessageProcessor that you can test in isolation. All of its dependencies are types you own, so you can use mocks, stubs or fake implementations to test their interaction. This implementation is overly simplified and misses stuff like error handling that you certainly want to do and of course want to test.
final class MessageProcessor
{
private MessageParser $parser;
private MessageBroadcaster $broadcaster;
public function __construct(MessageParser $parser, MessageBroadcaster $broadcaster)
{
$this->parser = $parser;
$this->broadcaster = $broadcaster;
}
public function handle(string $rawMessage): void
{
$this->broadcaster->send($this->parser->parse($rawMessage));
}
}
interface MessageParser
{
/**
* @throws BadMessageException
*/
public function parse(string $message): Message;
}
interface MessageBroadcaster
{
/**
* @throws UnsupportedMessageException
* @throws UnroutableMessageException
*/
public function send(Message $message): void;
}
Creating an implementation of the MessageParser should be straightforward and easily unit testable. The RabbitMQ implementation of the MessageBroadcaster would be a perfect candidate for an integration test.
Finally, plug all the concrete implementations together for the real application.
$server = IoServer::factory(
new MyRatchetApp(
new MessageProcessor(
new CommandMessageParser(),
new RabbitMqMessageBroadcaster()
)
),
8080
);
To make sure all parts work together, you can create some end-to-end tests that perform full roundtrips and verify the results. Creating these tests first, allows you to do Double Loop TDD