Skip to main content

go-go test: make Python unittest go fast

Project description

ggt is a unittest runner for Python that runs tests in parallel with the ability to run and share test setup. ggt also provides a pytest-like CLI to run tests with some useful test selection and running features.

There is no runtime bloat or magic, it’s standard unittest underneath, which makes running tests with ggt fast.

Quick Start

Basic Usage

Run tests from the default tests/ directory:

ggt

Run tests in specific files or directories:

ggt tests/test_models.py tests/integration/

Run tests sequentially:

ggt -j1

Verbose output with detailed test descriptions:

ggt -v

Filter Tests

Run only tests matching a regular expression:

ggt -k "test_user.*create"

Exclude tests matching a pattern:

ggt -e "test_slow.*"

Run tests in shards (useful for CI):

# Run shard 2 out of 4 total shards
ggt -s 2/4

Command Line Option Reference

Core Options

-v, --verbose

Increase verbosity level. Shows detailed test descriptions and results.

-q, --quiet

Decrease verbosity level. Minimal output.

-j, --jobs INTEGER

Number of parallel worker processes. Default is 0 (auto-detect based on the number of CPU cores).

-s, --shard TEXT

Run tests in shards using format current/total (e.g., 2/4).

Test Selection

-k, --include REGEXP

Only run tests matching the regular expression. Can be specified multiple times.

-e, --exclude REGEXP

Skip tests matching the regular expression. Can be specified multiple times.

-x, --failfast

Stop execution after the first test failure or error.

--shuffle

Randomize the order in which tests are executed.

--repeat INTEGER

Repeat the test suite N times or until the first failure.

Output and Reporting

--output-format [auto|simple|stacked|verbose|silent]

Control test progress output style:

  • auto: Automatically choose based on terminal capabilities

  • simple: Simple dot notation (like standard unittest)

  • stacked: Rich progress display with module grouping

  • verbose: Detailed output for each test

  • silent: Suppress progress output while still printing the final result summary

--warnings/--no-warnings

Enable or disable warning capture and reporting (enabled by default).

--result-log FILEPATH

Write test results to a JSON log file. Use %TIMESTAMP% for automatic timestamping. Result logs include outcome details and timing fields such as setup_time_taken and tests_time_taken.

--running-times-log FILEPATH

Maintain a CSV file tracking test execution times for performance analysis and shard balancing.

-X, --option KEY=VALUE

Test suite specific options in key-value format. Can be specified multiple times to pass configuration options to test fixtures and test cases.

Examples:

# Enable database caching
ggt -X test-db-cache=on

# Specify custom data directory
ggt -X data-dir=/custom/path

# Multiple options
ggt -X backend-dsn=postgresql://... -X use-ssl=true

Advanced Options

--debug

Output internal debug logs for troubleshooting.

--list

List all discovered tests and exit without running them.

--include-unsuccessful

Include tests that failed in the previous run (requires --result-log).

--cov PACKAGE

Enable code coverage reporting for the specified package. Can be used multiple times. PACKAGE must be an importable package name, not a file path. Enable coverage support with uv add --dev ggt[coverage] or python -m pip install 'ggt[coverage]'.

Test Decorators and Fixtures

Test Decorators

@async_timeout(seconds)

Set a timeout for async test methods. The test will fail if it takes longer than the specified time.

@xfail(reason, *, unless=False)

Mark a test as expected to fail. The test will be reported as “expected failure” if it fails, or “unexpected success” if it passes.

@xerror(reason, *, unless=False)

Like @xfail but expects an error (exception) rather than just a failure (assertion).

@not_implemented(reason)

Mark a test as not implemented. Similar to @xfail but semantically indicates missing functionality.

@skip(reason)

Skip a test entirely (from standard unittest).

Fixture System

ggt provides a fixture system for managing test prerequisites at the session level. Fixtures are declared as class attributes and automatically handle setup and teardown across the entire test session.

Basic Fixture Example:

class DatabaseFixture:
    def __init__(self):
        self._instance = None

    async def set_up(self, ui):
        """Called once during session setup"""
        self._instance = await create_database()

    async def tear_down(self, ui):
        """Called once during session teardown"""
        if self._instance:
            await self._instance.close()

    def __get__(self, obj, cls):
        """Descriptor protocol - returns the fixture value"""
        return self._instance

    def set_options(self, options):
        """Configure fixture from command-line options"""
        if 'database-url' in options:
            self.database_url = options['database-url']

    def get_shared_data(self):
        """Return JSON-serializable data for worker processes"""
        return None

    def set_shared_data(self, data):
        """Receive shared data in worker processes"""
        pass

    async def post_session_set_up(self, cases, *, ui):
        """Called after test class setup is complete"""
        pass

class MyTestCase(unittest.TestCase):
    database = DatabaseFixture()

    def test_something(self):
        # self.database is automatically available
        result = self.database.query("SELECT 1")
        self.assertEqual(result, 1)

Fixtures support:

  • Automatic lifecycle management: set_up() and tear_down() are called automatically

  • Option integration: set_options() receives command-line options passed via -X

  • Shared data: Fixtures can share data across processes using get_shared_data() and set_shared_data()

  • Post-session setup: post_session_set_up() is called after all class setup is complete

Test Case Protocol

For advanced test cases that need session-level setup and configuration, implement the GGTProto protocol:

class MyAdvancedTestCase(unittest.TestCase):
    @classmethod
    def set_options(cls, options):
        """Receive command-line options passed via -X"""
        cls.database_url = options.get('database-url')
        cls.enable_cache = options.get('cache') == 'on'

    @classmethod
    async def set_up_class_once(cls, ui):
        """Called once per test class during session setup"""
        if cls.database_url:
            cls.connection = await connect(cls.database_url)

    @classmethod
    async def tear_down_class_once(cls, ui):
        """Called once per test class during session teardown"""
        if hasattr(cls, 'connection'):
            await cls.connection.close()

    @classmethod
    def get_shared_data(cls):
        """Return JSON-serializable data to share with worker processes"""
        return {}

    @classmethod
    def update_shared_data(cls, **data):
        """Receive data exported during session setup"""
        pass

The protocol methods are:

  • set_options(options): Receives command-line options from -X flags

  • set_up_class_once(ui): Async setup called once per test class

  • tear_down_class_once(ui): Async teardown called once per test class

  • get_shared_data(): Returns JSON-serializable data for worker processes

  • update_shared_data(**data): Receives shared data in worker processes

Output Formats

Simple Format

Classic unittest-style output with dots, F’s, and E’s:

....F..E...s.......................................

Stacked Format

Rich progress display showing test progress by module:

tests/test_models.py    ....F....................
tests/test_queries.py   ..................s......
tests/test_auth.py      .........................

First few failed: test_user_creation, test_login
Running: (3) test_complex_query, test_batch_insert, test_migration
Progress: 45/120 tests.

Verbose Format

Detailed output for each individual test:

test_user_creation (tests.test_models.TestUser): OK
test_user_validation (tests.test_models.TestUser): FAILED: Validation
failed
test_async_operation (tests.test_queries.TestQueries): OK

Silent Format

Suppress progress output and print only the final summary, failures, errors, and warnings.

Performance and Optimization

Parallel Execution

ggt automatically detects a worker count based on your CPU cores. You can override this:

# Use specific number of workers
ggt -j 8

# Use single-threaded execution
ggt -j 1

Fixture Options

Pass configuration to your fixtures using the -X option:

# Enable caching in your fixtures
ggt -X test-cache=on

# Pass database configuration
ggt -X database-url=postgresql://localhost/testdb

# Multiple configuration options
ggt -X cache=on -X timeout=30 -X verbose=true

Your fixtures receive these options in their set_options() method and can use them to customize behavior.

Test Sharding

Distribute tests across multiple CI jobs using sharding:

# Job 1 of 4
ggt -s 1/4

# Job 2 of 4
ggt -s 2/4

ggt uses test and setup timing data from --running-times-log when available to balance load across shards.

Integration

Continuous Integration

Example GitHub Actions configuration:

name: Tests
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        shard: [1, 2, 3, 4]
    steps:
      - uses: actions/checkout@v5
      - uses: actions/setup-python@v5
        with:
          python-version: '3.11'
      - run: pip install ggt
      - run: |
          ggt -s ${{ matrix.shard }}/4 \
            --result-log results-${{ matrix.shard }}.json \
            -X test-cache=on -X timeout=300
      - uses: actions/upload-artifact@v4
        with:
          name: test-results
          path: results-*.json

Coverage Integration

Generate coverage reports alongside your tests:

ggt --cov myproject --cov myproject.submodule

This integrates with the coverage package to provide detailed code coverage analysis. Enable coverage support with uv add --dev ggt[coverage] or python -m pip install 'ggt[coverage]' to use this option.

The coverage report is written to the console and a .coverage data file is left in the working directory for follow-up coverage commands.

Option Integration

Test cases can receive and use options passed via -X:

class MyTestCase(unittest.TestCase):
    @classmethod
    def set_options(cls, options):
        cls.enable_debug = options.get('debug') == 'on'
        cls.database_url = options.get('database-url')

    @classmethod
    async def set_up_class_once(cls, ui):
        if cls.database_url:
            cls.db = await connect(cls.database_url)

    @classmethod
    async def tear_down_class_once(cls, ui):
        if hasattr(cls, 'db'):
            await cls.db.close()

    @classmethod
    def get_shared_data(cls):
        return {}

    @classmethod
    def update_shared_data(cls, **data):
        pass

Run with custom options:

ggt -X debug=on -X database-url=postgresql://localhost/test

Requirements

  • Python 3.11+

  • typing-extensions >= 4.14.0

Optional dependencies:

  • coverage >= 7.4

Git Hooks

Install the repository hooks with:

scripts/install-hooks.sh

This sets core.hooksPath to .githooks.

The pre-commit hook checks the staged index, not unstaged working-tree files. It creates a temporary checkout from the Git index and runs:

ruff format --check src tests
ruff check src/ggt pyproject.toml
ggt tests --output-format simple

The pre-push hook runs broader checks against the working tree:

uv run --dev --no-sync ruff format --check src tests
uv run --dev --no-sync ruff check src/ggt pyproject.toml
uv run --dev --no-sync mypy
uv run --dev --no-sync ty check
uv run --dev --no-sync ggt tests --output-format simple

License

ggt is licensed under the Apache License, Version 2.0. See the LICENSE file for details.

Contributing

Contributions are welcome. Please include tests and keep the type-checking suite passing.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

ggt-1.0.0.tar.gz (73.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

ggt-1.0.0-py3-none-any.whl (43.6 kB view details)

Uploaded Python 3

File details

Details for the file ggt-1.0.0.tar.gz.

File metadata

  • Download URL: ggt-1.0.0.tar.gz
  • Upload date:
  • Size: 73.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.24 {"installer":{"name":"uv","version":"0.11.24","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for ggt-1.0.0.tar.gz
Algorithm Hash digest
SHA256 12b6d384e1fef4a7b3d5f353f3b0031a282d0bbb5e4036599f5e795aacfefc6e
MD5 b708d8c547dc62f940406f33f1efd0ed
BLAKE2b-256 4f99f0cb02379e6b183d0530306cba6be114bf00cd5e4b9e9fca8d075460ab36

See more details on using hashes here.

File details

Details for the file ggt-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: ggt-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 43.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.24 {"installer":{"name":"uv","version":"0.11.24","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for ggt-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 2f894345d2942fc56f04f10b4aec3a77ca7dc3a2208717a7f40df56dc5b38eb7
MD5 b312644852d9c7dca87eb600733c7a95
BLAKE2b-256 efd828dc4481f96ceaa7c2a7e41448fef3ecde59f541a991281e40e5efaea3b5

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page