Testing the CLI


Table of contents
  1. Introduction
  2. Running the tests
  3. Testing pure logic
  4. Testing a command that writes files
  5. Testing a command that runs processes
  6. Using the shared fixtures
    1. What every test gets
  7. Asserting on a result
  8. Adding a command

Introduction

CLI commands read environment variables, touch the filesystem and call docker and git. You test them with Click’s CliRunner, pytest’s tmp_path and unittest.mock.patch. Every command reads its context through the service layer, so pointing WORKING_DIR at a temporary folder redirects a whole command there, with no Docker.

cd /workspace/splent_cli
pytest tests/ -v

Running the tests

pytest tests/unit/commands/product/test_product_status.py -v
pytest tests/unit/commands/env/test_env_list.py::TestFilter::test_filter_by_prefix -v
pytest tests/ --cov=splent_cli --cov-report=term-missing

Tests live under tests/unit/ (services/, utils/, commands/<group>/) and tests/integration/, with the shared fixtures in tests/conftest.py.


Testing pure logic

from splent_cli.services.compose import project_name

def test_project_name_replaces_special_chars():
    assert project_name("splent_io/notes@v1.0", "prod") == "splent_io_notes_v1_0_prod"

Testing a command that writes files

from click.testing import CliRunner
from splent_cli.commands.cache.cache_status import cache_status

def test_cache_shows_versioned_feature(tmp_path, monkeypatch):
    monkeypatch.setenv("WORKING_DIR", str(tmp_path))
    cache = tmp_path / ".splent_cache" / "features" / "splent_io" / "splent_feature_notes@v1.0.0"
    cache.mkdir(parents=True)

    result = CliRunner().invoke(cache_status, [])

    assert result.exit_code == 0
    assert "v1.0.0" in result.output

Testing a command that runs processes

Mock subprocess.run with a MagicMock of the right shape.

import json
from unittest.mock import patch, MagicMock
from click.testing import CliRunner
from splent_cli.commands.product.product_containers import product_docker

def test_shows_running_containers(product_workspace):
    out = json.dumps({"Service": "web", "State": "running", "Publishers": []})
    with patch("subprocess.run", return_value=MagicMock(returncode=0, stdout=out, stderr="")):
        result = CliRunner().invoke(product_docker, ["--dev"])

    assert result.exit_code == 0
    assert "running" in result.output

Using the shared fixtures

Fixture Gives
runner A CliRunner(mix_stderr=False), so stderr is in result.stderr
workspace A tmp_path set as WORKING_DIR, with SPLENT_APP and SPLENT_ENV cleared
product_workspace A workspace with a test_app product selected in dev, its pyproject.toml and compose files

tests/conftest.py also has helpers such as make_env_file, make_cache_entry, make_spl_working_copy, make_spl_cache_entry and write_product_spl_pin.

make_cache_entry(workspace, "splent_io", "splent_feature_notes", "v1.0.0")

What every test gets

Autouse fixtures keep the developer’s machine out of every test.

Fixture Does
forge_env_is_clean Removes every SPLENT_FORGE_* variable, then sets SPLENT_FORGE_HOST=github.com, whose URL shapes the suite asserts
splent_env_is_not_inherited Removes SPLENT_ENV
pypi_is_not_asked Answers “could not ask” for every PyPI check
no_builtin_spl_pins Empties the CLI’s built-in SPL pins
contract_maps_are_not_shared Clears the memoised contract maps

GITHUB_TOKEN and GITHUB_USER are not removed. Unset them in your shell before you run the suite.


Asserting on a result

result = runner.invoke(my_command, ["--flag"])
assert result.exit_code == 0, result.output

Assert exit_code first. result.output holds stdout, and result.exception the exception when a command crashed.

Test the error paths too, such as a missing SPLENT_APP, flags that exclude each other, or a missing file.

def test_requires_splent_app(runner, workspace):
    result = runner.invoke(product_up, ["--dev"])
    assert result.exit_code == 1
    assert "SPLENT_APP" in result.output

When you assert that a name does not appear, pick names that are not substrings of each other. "SET_VAR" is inside "UNSET_VAR".


Adding a command

  1. Create tests/unit/commands/<group>/test_<command>.py, and import the command function directly.
  2. Use workspace, product_workspace or tmp_path, and mock any external tool.
  3. Test flag validation, the happy path and at least one error path.
  4. Write its reference page under splent_cli/ in the docs repository, titled with the command itself (title: feature:foo). It then appears in the Command index. A page with a prose title names its commands in the front matter, as in command: doctor or command: [locust, "locust:stop"].

The docs test tests/test_command_index.py fails when a command has no page, or a page documents a command the CLI lacks.


Back to top

splent. Distributed by an LGPL license v3. Contact us: drorganvidez@us.es