Skip to content

Integration Test for websocket connection service #80

@fulstadev

Description

@fulstadev

First of all, thanks for this package!

Second, I was trying to setup a basic and global integration test directly as a PHPUnit test which tests the following:

  • Client can connect as expected with a specific websocket server
  • Client can communicate as expected with the connected websocket server
  • Client receives responses (if any response is delivered immediately) as expected from a message sent to the websocket server

So I initially tried something like this:

<?php

declare(strict_types=1);

use PHPUnit\Framework\TestCase;
use WebSocket\Client;
use WebSocket\Connection;
use WebSocket\Middleware\CloseHandler;
use WebSocket\Middleware\PingResponder;

final class WebsocketsIntegrationTest extends TestCase
{

    public function test_websocket_connection()
    {

        $client = new Client('wss://echo.websocket.org/?auth=123xyz');

        $client
            // Add standard middlewares
            ->addMiddleware(new CloseHandler())
            ->addMiddleware(new PingResponder())
            // Listen to incoming Text messages
            ->onText(
                function (
                    Client     $client,
                    Connection $connection,
                               $message
                ) {
                    // Assert incoming message
                    $this->assertSame(
                        expected: 'Hello, world!',
                        actual  : $message->getContent()
                    );
                    $client->close();
                }
            )
            ->start();

    }

}

But, as the start method never returns, this will cause a never concluding test execution. So I have been digging for a lot of time for a suitable solution for an integration test that is ideally executable using PHP, and ideally within PHPUnit. So the only feasible and easy solution I could find is using wscat but integrating its execution and test assertions within PHP,as follows. On my mac, it perfectly works:

<?php
declare(strict_types=1);

use PHPUnit\Framework\TestCase;

final class WebsocketsIntegrationTest extends TestCase
{

    public function test_websocket_connection()
    {

        $descriptorspec = [
            // stdin
            0 => [
                "pipe",
                "r"
            ],
            // stdout
            1 => [
                "pipe",
                "w"
            ],
            // stderr
            2 => [
                "pipe",
                "w"
            ]
        ];


        $process = proc_open(
            "wscat -c wss://echo.websocket.org/?auth=123xyz",
            $descriptorspec,
            $pipes
        );

        if (is_resource($process)) {

            // Allow some time for the connection to be established (max timeout of 3 seconds)
            sleep(3);

            /**
             * Next, to test that your websocket integration works as expected, send the messages you want to, and also
             * provide the response you expected to be delivered in response to it, to verify that the websockets
             * server responds as expected.
             */
            $messages = [
                /**
                 * Send an empty message, to verify that the websocket service responds as intended to it.
                 */
                [
                    'request_body'           => '',
                    'expected_response_body' => "> {\"status\" : \"connected\"}",
                ]
            ];

            foreach ($messages as $message) {

                fwrite(
                    $pipes[0],
                    /**
                     * The newline character is crucial as it corresponds to hitting enter in the CLI
                     * (submit the message)
                     */
                    "{$message['request_body']}\n"
                );

                /**
                 * The websocket server is expected to respond immediately, hence waiting one second should be enough
                 */
                sleep(1);

            }

            // Close the stdin pipe, as no more input will be provided
            fclose($pipes[0]);

            /**
             * Terminate the process to simulate CTRL + C.
             * Note that this command is immediately executed, which is why the sleep calls above are needed.
             */
            proc_terminate($process);

            // Get the output of the stdout and stderr pipes
            $output = stream_get_contents($pipes[1]);
            $errors = stream_get_contents($pipes[2]);

            // Close the stdout and stderr pipes
            fclose($pipes[1]);
            fclose($pipes[2]);

            // Close the process and get the termination status
            $return_value = proc_close($process);

            /**
             * Order will be conserved with explode, so assert the message responses in the order of their request, as
             * any pipe is a FIFO structure (https://www.sitepoint.com/proc-open-communicate-with-the-outside-world/).
             *
             * Every response delivered by the websocket server that was currently setup responds with an ending newline
             * character in the CLI. To thus get all responses, explode the obtained stdout string via the newline
             * characters as the separator, and then kick off the last element to disregard the trailing newline
             * characters (which would otherwise lead to an additional empty string as expected response at index n + 1)
             * You may have to adapt this according to your response logic of your websockets server
             */
            $responses = explode(
                separator: "\n",
                string   : $output
            );
            array_pop($responses);

            foreach ($responses as $index => $response) {

                /**
                 * Assert that the websocket server responded as expected for every submitted message
                 */
                $this->assertSame(
                    $messages[$index]['expected_response_body'],
                    $response,
                    "An unexpected response body was received from the websocket server; $response instead of the expected {$messages[$index]['expected_response_body']}."
                );

            }

            /**
             * No errors should have been encountered
             */
            $this->assertEmpty(
                $errors,
                "Errors encountered: $errors"
            );

            /**
             * proc_terminate() by defaults sends 15 as the signal to the running process; so that's the expected
             * integer output here
             */
            $this->assertEquals(
                15,
                $return_value,
                "Process exited with code $return_value"
            );

        } else {
            $this->fail("The command used to start the websocket server failed to execute");
        }

    }

}

Of course, for this solution, you need wscat, but for me it's totally worth it for the testing.

So I just wanted to share it here to provide a concrete real-life integration test solution in PHP with websocket servers, as your testing relies on mocking only. Just in case anyone using your library as me is looking for the same thing, thanks again for the package!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions