How To Test Generic JSON-RPC Servers

While pytest-lsp is primarily focused on writing tests for LSP servers it is possible to reuse some of the machinery to test other JSON-RPC servers.

A Simple JSON-RPC Server

As an example we’ll reuse some of the pygls internals to write a simple JSON-RPC server that implements the following protocol.

  • client to server request math/add, returns the sum of two numbers a and b

  • client to server request math/sub, returns the difference of two numbers a and b

  • server to client notification log/message, allows the server to send debug messages to the client.

Note

The details of the implementation below don’t really matter as we just need something to help us illustrate how to use pytest-lsp in this way.

Remember you can write your servers in whatever language/framework you prefer!

from pygls.protocol import JsonRPCProtocol, default_converter
from pygls.server import Server

server = Server(protocol_cls=JsonRPCProtocol, converter_factory=default_converter)


@server.lsp.fm.feature("math/add")
def addition(ls: Server, params):
    a = params.a
    b = params.b

    ls.lsp.notify("log/message", dict(message=f"{a=}"))
    ls.lsp.notify("log/message", dict(message=f"{b=}"))

    return dict(total=a + b)


@server.lsp.fm.feature("math/sub")
def subtraction(ls: Server, params):
    a = params.a
    b = params.b

    ls.lsp.notify("log/message", dict(message=f"{a=}"))
    ls.lsp.notify("log/message", dict(message=f"{b=}"))

    return dict(total=b - a)


if __name__ == "__main__":
    server.start_io()

Constructing a Client

While pytest-lsp can manage the connection between client and server, it needs to be given a client that understands the protocol that the server implements. This is done with a factory function

def client_factory():
    client = JsonRPCClient()

    @client.feature("log/message")
    def _on_message(params):
        logging.info("LOG: %s", params.message)

    return client

The Client Fixture

Once you have your factory function defined you can pass it to the ClientServerConfig when defining your client fixture

@pytest_lsp.fixture(
    config=ClientServerConfig(
        client_factory=client_factory, server_command=[sys.executable, "server.py"]
    ),
)
async def client(rpc_client: JsonRPCClient):
    # Setup code here (if any)

    yield

    # Teardown code here (if any)

Writing Test Cases

With the client fixuture defined, test cases are written almost identically as they would be for your LSP servers. The only difference is that the generic send_request_async() and notify() methods are used to communicate with the server.

@pytest.mark.asyncio
async def test_add(client: JsonRPCClient):
    """Ensure that the server implements addition correctly."""

    result = await client.protocol.send_request_async(
        "math/add", params={"a": 1, "b": 2}
    )
    assert result.total == 3


@pytest.mark.asyncio
async def test_sub(client: JsonRPCClient):
    """Ensure that the server implements addition correctly."""

    result = await client.protocol.send_request_async(
        "math/sub", params={"a": 1, "b": 2}
    )
    assert result.total == -1

However, it is also possible to extend the base JsonRPCClient to provide a higher level interface to your server. See the SubprocessSphinxClient from the esbonio project for such an example.