Overview

The module pyTooling.Testing offers enhanced classes for writing unit tests with Python’s unittest framework, which is also what pytest runs.

Testcase

Testcase is the class to derive a testcase from. It is a unittest.TestCase - everything that class offers is available unchanged - and adds what pyTooling’s test suites otherwise write themselves.

from pyTooling.Testing import Testcase


class Slots(Testcase):
  def test_SlotsAreDerived(self) -> None:
    self.assertHasAttr(MyClass, "__slots__")

Deriving from it rather than from unittest.TestCase costs nothing and means a test suite picks up what is added later without changing every class again.

Assertions

assertHasAttr() and assertNotHasAttr() check whether an object has an attribute. They were added to unittest.TestCase in Python 3.14, so a test suite running on 3.11 to 3.13 cannot use them - Testcase provides them there.

On Python 3.14 and newer the class defines nothing of its own, so the standard library’s implementations and messages are used and the two behave identically on every supported interpreter.

Application Testing

A unit test imports the code it tests. An application test starts the installed program the way a user does, so the chain under test includes what importing cannot reach: the console_scripts entry point, the argument parsing and the exit code.

ApplicationTestcase derives from Testcase. It resolves the console script once per test class and offers two ways to start the program:

  • RunEntrypoint() runs the installed console script - the path a user takes, and therefore the one covering the entry-point wiring.

  • RunModule() runs python -m <module>. When this passes while the entry point fails, the packaging is at fault rather than the code.

Both capture stdout and stderr as text and take a timeout, so a hanging program fails the test instead of the test suite.

from pyTooling.Testing import ApplicationTestcase


class Commands(ApplicationTestcase):
  _consoleScript =  "myprogram"
  _runnableModule = "myPackage.CLI"

  def test_Version(self) -> None:
    result = self.RunEntrypoint("--version")

    self.assertExitCode(result)
    self.assertIn("myprogram", result.stdout)

Both class variables are mandatory: a test class naming neither cannot run anything, so setUpClass() raises a TestingException instead of letting every testcase in the class fail with a less obvious error. A console script that is not installed is reported the same way.

Assertions

assertExitCode() compares the exit code and, when they differ, reports the command line together with what the program printed. That output is what explains the failure, and it is gone once the test has finished, so it belongs in the assertion message rather than in the console.

Helpers

stripANSIColorCodes() removes ANSI escape sequences from a text. A program writing to a terminal colors its output while the same program in a pipe usually does not - comparing the stripped text is more robust than encoding a rule about when the codes appear.