Testing users of the Nublado client#
The MockJupyter class can be used to write unit tests of users of the Nublado Python client without needing a running Phalanx environment.
It simulates the subset of the JupyterHub and JupyterLab API used by the Nublado client and simulates Python code execution inside the notebook with exec.
Warning
Code for which no result has been registered via
MockJupyter.register_python_result will be executed via exec. This
mock therefore supports arbitrary code execution via its handlers and
must never be exposed to untrusted messages.
Creating the mock in a test fixture#
MockJupyter requires RESPX in addition to rubin.nublado.client.
Add respx to your project’s development dependencies.
Then, add a fixture (usually to tests/conftest.py) that calls register_mock_jupyter and yields the MockJupyter object.
from collections.abc import AsyncGenerator
import pytest
import respx
from nublado.rubin.client import MockJupyter, register_mock_jupyter
@pytest.fixture
async def mock_jupyter(
respx_mock: respx.Router,
) -> AsyncGenerator[MockJupyter]:
async with register_mock_jupyter(respx_mock) as mock:
yield mock
Warning
register_mock_jupyter will globally patch the websockets.connect function to mock the JuypterLab web socket.
If your application uses websockets.connect outside of the Nublado client, you cannot use this Jupyter mock and will have to find some other way to test.
register_mock_jupyter uses service discovery to determine what Nublado URLs to mock.
You therefore must set up the service discovery mock before setting up the Jupyter mock (such as by declaring it auto-use).
See the Repertoire documentation for more information.
By default, register_mock_jupyter sets up a mock of a Nublado instance configured with per-user subdomains.
If you want to emulate hosting JupyterHub and JupyterLab on the same hostname instead, pass use_subdomains=False as an argument to register_mock_jupyter.
This should be invisible to your application; the Nublado client should transparently handle both configurations.
Writing tests#
Any test you write that uses the Nublado client should depend on the mock_jupyter fixture defined above, directly or indirectly, so that the mock will be in place.
Alternately, you can mark the fixture as auto-use.
When using this mock, you must use a token created with MockJupyter.create_mock_token to authenticate.
The result of this static method should be passed in as the token constructor parameter to NubladoClient.
For example:
from rubin.nublado.client import MockJupyter, NubladoClient
def test_something(mock_jupyter: MockJupyter) -> None:
token = mock_jupyter.create_mock_token("some-user")
client = NubladoClient("some-user", token)
# More tests go here
...
Mocking payloads#
By default, MockJupyter runs the code provided to JupyterLabSession.run_python using eval.
To change this behavior, you can call MockJupyter.register_python_result, passing it a code string and a result.
Any subsequent attempt to execute that code string will return the registered result rather than executing the code.
The MockJupyter.register_notebook_result method provides similar functionality for NubladoClient.run_notebook.
It takes the notebook contents (as a JSON string) and a corresponding NotebookExecutionResult.
Any subsequent execution of a notebook matching that string will return the registered notebook execution result.
If MockJupyter.register_notebook_result has not been called with a matching notebook string value, the MockJupyter replacement for full notebook execution will return the input notebook.
The mock will never attempt to run nbconvert in the way that the Nublado JupyterLab extension would do.
Injecting delays#
There are two ways to inject delays into the mock to simulate how long it takes Nublado operations to take on a real cluster:
MockJupyter.set_delete_delayWait this long before deleting the lab. The lab will be fully delated if at least this long has passed and the client makes a call to the API endpoint listing running labs (called by
NubladoClient.is_lab_stopped).MockJupyter.set_spawn_delayPause for this long before returning success from the spawn progress route.
Testing Jupyter errors#
Any Jupyter operation performed by the client can be configured to fail for a given user by calling MockJupyter.fail_on and passing in the user and the operation or list of operations that should fail.
The operation should be chosen from MockJupyterAction.
There is one other error behavior that can be enabled in the mock:
set_redirect_loopIf the parameter
enabledisTrue, tells the mock to return redirect loops from the endpoints for getting the JupyterHub top-level page, the JupyterLab top-level page, and the spawn progress server-sent events API.
Inspecting client behavior#
MockJupyter provides a few methods that can be used to inspect the internal state of the mock.
This is useful for testing the Nublado client itself, and may be useful when testing software that uses the Nublado client internally to see if it left Jupyter in the expected state.
get_last_spawn_formReturns the contents, as a dictionary, of the last spawn form submitted to the mock. Intended primarily to test the client
spawn_labmethod.get_sessionReturns the current JupyterLab session for a given user, or
Noneif there is no current session. The session is returned as aMockJupyterLabSessionobject, which contains information about the parameters sent by the client to create the session.