Skip to main content

A modernized unit-test framework for Python.

Project description

pUnit is a modernized unit-testing framework for Python, inspired by xUnit.

This README is only a high-level introduction to pUnit. For more detailed documentation, please view the official docs at https://pUnit.readthedocs.io.

Command-Line Usage

Running pUnit with no arguments will perform test auto-discovery and execution:

python3 -m punit

By default it will look for tests in the tests/ directory. You can override this behavior by providing a --test-package argument:

python3 -m punit --test-package elsewhere

In the above example, test discovery will instead occur in an elsewhere/ directory.

Report Generation

pUnit can generate "Test Results" reports. Currently supporting HTML and jUnit formats:

python3 -m punit --report html

Reports can also be output to a file where they can be lifted as part of a CI/CD pipeline and stored as an artifact or used for other purposes:

python3 -m punit --report junit --output results.xml

Syntax Help

There are more options available, passing a --help argument will print help text:

python3 -m punit --help

Outputs:

Usage: python3 -m punit [-h|--help]
                        [-q|--quiet] [-v|--verbose]
                        [-z|--failfast]
                        [-p|--test-package NAME]
                        [-i|--include PATTERN]
                        [-e|--exclude PATTERN]
                        [-f|--filter PATTERN|@FILEPATH]
                        [-t|--trait [!]NAME[=VALUE]]
                        [-w|--working-directory PATH]
                        [-n|--no-default-patterns]
                        [--no-exitcode]
                        [--no-pathfix]
                        [-r|--report {junit|json}]
                        [-o|--output FILENAME]

Options:
    -h, --help           Show this help text and exit
    -q, --quiet          Quiet output
    -v, --verbose        Verbose output
    -z, --failfast       Stop on first failure or error
    -p, --test-package NAME
        Use NAME as the test package, all tests should
        be locatable as modules in the named package.
        Default: 'tests'
    -i, --include PATTERN
        Include test files matching PATTERN
        Default: '*.py'
    -e, --exclude PATTERN
        Exclude test files matching PATTERN, overriding --include
        Default: '__*__' (dunder files), '/.*/' (dot-directories)
    -f, --filter PATTERN|@FILEPATH
        Only execute tests matching PATTERN
        Default: '*'
        To specify a file containing a list of filters, prefix
        with '@' and provide the path to the file.
    -t, --trait [!]NAME[=VALUE]
        Execute tests with the specified trait, negated by prefixing with '!'.
        If VALUE is specified, matches tests with the trait having specified value.
        If VALUE is not specified, matches any test with the trait having any value.
        Default: No filtering based on traits.        
    -w, --working-directory PATH
        Working directory (defaults to start directory)
    -n, --no-default-patterns
        Do not apply any default include/exclude patterns.
    --no-exitcode
        Do not exit with an error code on unit test failure.
    --no-pathfix
        Do not apply path fixes, rely on PYTHONPATH only.
    -r, --report {html|junit}
        Generate a report to stdout using either an "html"
        or "junit" format. When generating a report to stdout
        all other output is suppressed, unless `--output`
        is also specified.
    -o, --output FILENAME
        If `--report` is used, instead of writing to stdout
        write to FILENAME. In this case `--report` does not
        suppress any program output.

Writing Tests

You can write tests as functions, class methods, instance methods, or static methods with all forms offering identical functionality. You can also utilize async/await syntax without any additional overhead.

pUnit is based upon the fundamental concepts of Facts and Theories. These are codified using decorators, aptly named @fact and @theory. Consider these examples:

from punit import fact, theory, inlinedata

@fact
async def MyLibrary_WhenInitialized_TouchMustReturnTrue:
    mylib = MyLibrary()
    mylib.initialize()
    await asyncio.sleep(1)
    assert mylib.touch(), "Expecting touch() to return true, because initialize() was called."

class MyTestFixture:

    def calc(number:int|None = None):
        if number == None:
            raise Exception('Invalid value "None".')
        return number * number

    @theory
    @inlinedata(0, 0)
    @inlinedata(1, 1)
    @inlinedata(2, 4)
    @inlinedata(3, 9)
    @inlinedata(5, 25)
    @inlinedata(8, 64)
    def verifyCalcAssumptions(self, number:int, expected:int) -> None:
        assert expected == self.calc(number)

    @fact
    def verifyCalcErrorCondition(self) -> None:
        from punit.exceptions import raises
        # assert errors are raised, or not
        def calc_None() -> None:
            self.calc(None)
        def calc_1() -> None:
            self.calc(1)
        assert raises[Exception](calc_None)
        assert not raises[Exception](calc_1)

Because pUnit is a decorative framework you are afforded the utmost freedom in how you structure and implement your tests.

Unlike other testing frameworks, the names you use for functions, classes, and methods is not relevant. There is no requirement to inherit classes from specific abstract/base classes for any particular functionality. There is no requirement that your tests be organized into modules with __init__.py files.

You will want to take particular note of the --exclude command-line parameter which allows you to restrict what pUnit will consider to be a valid test file. While the default behavior will fit 99% of use-cases, you can exercise more control over the discovery process.

Vision, Future, and LTS

The long-term vision is to provide both imperative and declarative syntaxes for testing while keeping pUnit as simple as possible in its implementation.

pUnit is a Python 3.11+ package and there are no plans to backport it to earlier versions of Python, however, user requests (with proposed changes) will be accepted when it makes sense (for example, pUnit currently supports Python 3.11, but originally only supported Python 3.12.)

As Python progresses so will pUnit and SEMVER rules will be respected to provide developers with assurance that a major version of pUnit is fit for a particular purpose, thus, if there is ever a breaking change in Python that requires a breaking change in pUnit you can expect pUnit versioning to reflect this.

In any situation where an undocumented feature may be used maintainers will actively keep watch on deprecation notices and removals, will clearly identify these dependencies in the docs, and most importantly will provide a LTS alternative to any undocumented feature or such features will not be included in pUnit.

With respect to long-term support, we will commit to maintaining major versions for 3 years from the date they were superceded by a new major version. This should align well with Python Core Development.

Contact

You can reach me on Discord or open an Issue on Github.

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

punit-1.4.9.tar.gz (21.4 kB view details)

Uploaded Source

Built Distribution

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

punit-1.4.9-py3-none-any.whl (31.4 kB view details)

Uploaded Python 3

File details

Details for the file punit-1.4.9.tar.gz.

File metadata

  • Download URL: punit-1.4.9.tar.gz
  • Upload date:
  • Size: 21.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.15+

File hashes

Hashes for punit-1.4.9.tar.gz
Algorithm Hash digest
SHA256 0c213fa3ecbb50ebb7a361160ff644eb0cf879d7ecc515234b4e15a45ac06219
MD5 93ebb91869624f804aa866cf90d59f56
BLAKE2b-256 feb02b7a223b2753b3935a8103643ee7ec41a2e61631c3f917b2ab75a1f5490a

See more details on using hashes here.

File details

Details for the file punit-1.4.9-py3-none-any.whl.

File metadata

  • Download URL: punit-1.4.9-py3-none-any.whl
  • Upload date:
  • Size: 31.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.15+

File hashes

Hashes for punit-1.4.9-py3-none-any.whl
Algorithm Hash digest
SHA256 37239ab2f76e367c061c0a583cc05df2564e84d6eeda1a8b9501ed7839467779
MD5 e3ad874e18aee1ed23e030ac71592fde
BLAKE2b-256 e7035ceb2e014cca18739b3df74c22af5226caf003c7683bbdeed61112006cef

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