Skip to main content

Pro Test Fixture Provider

Project description

Protestr: Pro Test Fixture Provider

PyPI - Version PyPI - Python Version


Protestr is a simple, powerful Python library that generates versatile fixtures based on your rules. It's designed to maximize focus on acts and assertions by handling the complexities of fixture management. Its intuitive API lets you:

  • Re-run tests
    Provide dynamic fixtures using dependency injection, repeating a test for different scenarios instead of duplicating it with hardly any change.

  • Ensure teardown
    Have your defined cleanup logic run consistently after every test run.

  • Use anywhere
    Integrate seamlessly with all popular Python testing frameworks, such as unittest, pytest, and nose2, facing zero disruption to your existing testing practices.

The examples in this doc have been carefully crafted to help you master its concepts and get the best out of it.

[!NOTE] Protestr was tested with Protestr.

Next Up

Quick Demo

The test below won't run because the test runner can't provide users and mongo.

import unittest
from unittest.mock import patch as mock


class TestWithMongo(unittest.TestCase):
    @mock("examples.lib.os")
    def test_add_to_users_db(
        self,   #  ✅  Provided by `unittest`
        os,     #  ✅  Provided by `mock()`
        users,  #  ❌  Unexpected param `users`
        mongo,  #  ❌  Unexpected param `mongo`
    ):
        os.environ.__getitem__.return_value = "localhost"

        add_to_users_db(users)

        added = mongo.client.users_db.users.count_documents({})
        self.assertEqual(added, len(users) if users else 0)

Protestr allows you to provide fixtures to generate these parameters elegantly. You can also chain multiple fixtures to repeat the test for different test cases.

...
from protestr import provide
from examples.specs import User, MongoDB


class TestWithMongo(unittest.TestCase):
    @provide(              #  ▶️  Fixture Ⅰ
        users=[User] * 3,  #  ✨  Generate 3 test users. Spin up a MongoDB container.
        mongo=MongoDB,     #  🔌  After each test, disconnect and remove the container.
    )
    @provide(users=[])     #  ▶️  Fixture Ⅱ: Patch the above fixture to generate 0 users
    @provide(users=None)   #  ▶️  Fixture Ⅲ
    @mock("examples.lib.os")
    def test_add_to_users_db(self, os, users, mongo):
        os.environ.__getitem__.return_value = "localhost"

        add_to_users_db(users)

        added = mongo.client.users_db.users.count_documents({})
        self.assertEqual(added, len(users) if users else 0)

[!TIP] The top-most provide() call must declare all specs. Subsequent ones should specify patches (what changed) from the previous test case.

In the example above, User and MongoDB are user-defined specs—the blueprints for generating test data.

Protestr offers some great specs in protestr.specs and makes it incredibly easy to define new ones (explained in "Creating Specs").

from protestr.specs import between


@provide(id=between(1, 99), name=str, password=str)
class User:
    def __init__(self, id, name, password):
        self.id = id
        self.name = name
        self.password = password


class MongoDB:
    def __init__(self):
        self.container = docker.from_env().containers.run(
            "mongo", detach=True, ports={27017: 27017}
        )
        self.client = pymongo.MongoClient("localhost", 27017)

    def __teardown__(self):      #  ♻️  After each test:
        self.client.close()      #  🔌  Disconnect the container
        self.container.stop()    #  🛑  Stop the container
        self.container.remove()  #  🧹  Remove the container

Find the complete example in examples/.

Getting Started

Installation

Install protestr from PyPI:

pip install protestr

Specs and Fixtures

Specs are blueprints for generating test data. A fixture is a combination of specs provided to a class/function—usually a test method—using provide().

Specs are resolved by Protestr to generate usable data. There are three types of specs:

  1. Python primitives: int, float, complex, bool, or str.

  2. Classes and functions that are callable without args.
    If a constructor or a function contains required parameters, it can be transformed into a spec by auto-providing those parameters using provide() (explained in "Creating Specs").

  3. Tuples, lists, sets, or dictionaries of specs in any configuration, such as a list of lists of specs.

Specs are resolved in two ways:

  1. By resolving

    >>> from protestr import resolve
    >>> from protestr.specs import choice
    >>> bits = [choice(0, 1)] * 8
    >>> resolve(bits)
    [1, 0, 0, 1, 1, 0, 1, 0]
    
  2. By calling/resolving a spec-provided class/function

    >>> from protestr import provide
    >>> @provide(where=choice("home", "work", "vacation"))
    ... def test(where):
    ...     return where
    ...
    >>> test()
    'vacation'
    >>> resolve(test)
    'home'
    

The resolution of specs is recursive. If a spec produces another spec, Protestr will resolve that spec, and so on.

from protestr import provide


@provide(x=int, y=int)
def point(x, y):
    return x, y


def triangle():
    return [point] * 3


print(resolve(triangle))
# [(971, 704), (268, 581), (484, 548)]

[!TIP] A spec-provided class/function itself becomes a spec and can be resolved recursively.

>>> @provide(n=int)
... def f(n):
...     def g():
...         return n
...     return g
...
>>> resolve(f)
784

Protestr simplifies spec creation so that you can create custom specs effortlessly for your testing requirements.

Creating Specs

Creating a spec usually takes two steps:

  1. Write a class/function

    class GeoCoordinate:
        def __init__(self, latitude, longitude, altitude):
            self.latitude = latitude
            self.longitude = longitude
            self.altitude = altitude
    
    
    # def geo_coordinate(latitude, longitude, altitude):
    #     return latitude, longitude, altitude
    
  2. Provide specs for required parameters if any

    @provide(
        latitude=between(-90.0, 90.0),
        longitude=between(-180.0, 180.0),
        altitude=float,
    )
    class GeoCoordinate:
        def __init__(self, latitude, longitude, altitude):
            self.latitude = latitude
            self.longitude = longitude
            self.altitude = altitude
    
    
    # @provide(
    #     latitude=between(-90.0, 90.0),
    #     longitude=between(-180.0, 180.0),
    #     altitude=float,
    # )
    # def geo_coordinate(latitude, longitude, altitude):
    #     return latitude, longitude, altitude
    

Thus, our new spec is prime and ready!

>>> resolve(GeoCoordinate).altitude
247.70713408051304
>>> GeoCoordinate().altitude
826.6117116092906
>>> GeoCoordinate(altitude=int).altitude
299
import unittest
from protestr import provide


class TestLocations(unittest.TestCase):

    @provide(locs=[GeoCoordinate] * 100)

    def test_locations(self, locs):

        self.assertEqual(100, len(locs))

        for loc in locs:
            self.assertTrue(hasattr(loc, "latitude"))
            self.assertTrue(hasattr(loc, "longitude"))
            self.assertTrue(hasattr(loc, "altitude"))


if __name__ == "__main__":
    unittest.main()

Find more sophisticated usages in the Documentation.

Ensuring Teardown

Good fixture design demands remembering to dispose of resources at the end of tests. Protestr takes care of it out of the box with the __teardown__ function. Whenever a provide()-applied function returns or terminates abnormally, it looks for __teardown__ in each (resolved) object it provided and invokes it on the object if found. So, all you need to do is define __teardown__ once in a class, and it will be called every time you provide one.

class MongoDB:
    def __init__(self):
        self.container = docker.from_env().containers.run(
            "mongo", detach=True, ports={27017: 27017}
        )
        self.client = pymongo.MongoClient("localhost", 27017)

    def __teardown__(self):
        self.client.close()
        self.container.stop()
        self.container.remove()

Documentation

protestr

$\large\textcolor{gray}{@protestr.}\textbf{provide(**specs)}$

Transform a class/function to auto-supply args when invoked.

@provide(
    uppercase=choice(ascii_uppercase),
    lowercase=choice(ascii_lowercase),
    digit=choice(digits),
    chars=choices(str, k=between(5, 100))
)
def password(uppercase, lowercase, digit, chars):
    return "".join((uppercase, lowercase, digit, chars))

@provide(
    password=password,
    username=choices(ascii_lowercase, k=between(4, 12))
)
def credentials(username, password):
    return username, password

The specs are provided implicitly from keyword args in provide() to the matching parameters of the function when called with those args omitted.

>>> credentials()
('cgbqkmsehf', 'Pr8LOipCBKCBkAxbbKykppKkALxykKLOiKpiy')

When specified as keyword args, they override the original specs.

>>> credentials(username="johndoe")
('johndoe', 'En2HivppppimmFaFHpEeEEEExEamp')

If provide() is applied multiple times, the function runs as many times (when invoked), and teardowns are performed at the end of each invocation (see Ensuring Teardown). This trait can be leveraged to re-run tests for different test cases.

class TestFactorial(unittest.TestCase):
    @provide(n=0, expected=1)
    @provide(n=1)
    @provide(n=5, expected=120)
    def test_factorial_valid_number(self, n, expected):
        self.assertEqual(expected, factorial(n))

    @provide(n=float, expected="n must be a whole number")
    @provide(n=between(-1000, -1), expected="n must be >= 0")
    def test_factorial_invalid_number(self, n, expected):
        try:
            factorial(n)
        except Exception as e:
            (message,) = e.args

        self.assertEqual(expected, message)

$\large\textcolor{gray}{protestr.}\textbf{resolve(spec)}$

Resolve a spec.

Specs can be any of the following types:

  1. Python primitives: int, float, complex, bool, or str.

  2. Classes and functions that are callable without args.
    If a constructor or a function contains required parameters, it can be transformed into a spec by auto-providing those parameters using provide().

  3. Tuples, lists, sets, or dictionaries of specs in any configuration, such as a list of lists of specs.

>>> resolve(str)
'jKKbbyNgzj'
>>> resolve([bool] * 3)
[False, False, True]
>>> resolve({"name": str})
{'name': 'raaqSzSdfCIYxbIhuTGdxi'}
>>> class Foo:
...     def __init__(self):
...         self.who = "I'm Foo"
...
>>> resolve(Foo).who
"I'm Foo"

protestr.specs

$\large\textcolor{gray}{protestr.specs.}\textbf{between(x, y)}$

Return a spec representing a number between x and y.

x and y must be specs that evaluate to numbers. If both x and y evaluate to integers, the resulting number is also an integer.

>>> resolve(between(10, -10))
3
>>> resolve(between(-10, 10.0))
-4.475185425413375
>>> resolve(between(int, int))
452

$\large\textcolor{gray}{protestr.specs.}\textbf{choice(*elems)}$

Return a spec representing a member of elems.

>>> colors = ["red", "green", "blue"]
>>> resolve(choice(colors))
'green'
>>> resolve(choice(str)) # a char from a generated str
'T'
>>> resolve(choice(str, str, str)) # an str from three generated str objects
'NOBuybxrf'

$\large\textcolor{gray}{protestr.specs.}\textbf{choices(*elems, k)}$

Return a spec representing k members chosen from elems with replacement.

k must be a spec that evaluates to some natural number.

>>> resolve(choices(["red", "green", "blue"], k=5))
['blue', 'red', 'green', 'blue', 'green']
>>> resolve(choices("red", "green", "blue", k=5))
('red', 'blue', 'red', 'blue', 'green')
>>> resolve(choices(ascii_letters, k=10))
'OLDpaXOGGj'

$\large\textcolor{gray}{protestr.specs.}\textbf{sample(*elems, k)}$

Return a spec representing k members chosen from elems without replacement.

k must be a spec that evaluates to some natural number.

>>> colors = ["red", "green", "blue"]
>>> resolve(sample(colors, k=2))
['blue', 'green']
>>> resolve(sample("red", "green", "blue", k=3))
('red', 'blue', 'green')
>>> resolve(sample(ascii_letters, k=10))
'tkExshCbTi'
>>> resolve(sample([int] * 3, k=between(2, 3))) # generate 3, pick 2, for demo only
[497, 246]

$\large\textcolor{gray}{protestr.specs.}\textbf{recipe(*specs, then)}$

Return a spec representing the result of calling a given function with some given specs resolved.

then must be callable with a collection containing the resolved specs.

>>> from string import ascii_letters, digits
>>> resolve(
...     recipe(
...         sample(ascii_letters, k=5),
...         sample(digits, k=5),
...         then="-".join,
...     )
... )
'JzRYQ-51428'

License

protestr is distributed under the terms of the MIT license.

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

protestr-4.0.1.tar.gz (13.0 kB view details)

Uploaded Source

Built Distribution

protestr-4.0.1-py3-none-any.whl (9.0 kB view details)

Uploaded Python 3

File details

Details for the file protestr-4.0.1.tar.gz.

File metadata

  • Download URL: protestr-4.0.1.tar.gz
  • Upload date:
  • Size: 13.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/5.1.1 CPython/3.10.11

File hashes

Hashes for protestr-4.0.1.tar.gz
Algorithm Hash digest
SHA256 0f574b5cfa61267c4fb829156e4f75fd1cd3a2bc782f25b014f2278905c7a0f0
MD5 74659b87535a242d995d3e7952d011ba
BLAKE2b-256 d01ecda721f7533ebbe5eed6fbd77b3e5cb1c8440a7c2434ec34b0c90921c43d

See more details on using hashes here.

File details

Details for the file protestr-4.0.1-py3-none-any.whl.

File metadata

  • Download URL: protestr-4.0.1-py3-none-any.whl
  • Upload date:
  • Size: 9.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/5.1.1 CPython/3.10.11

File hashes

Hashes for protestr-4.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 35c4e2b9a1605bd43a7c4e58dd1150f33f125426de39baa377915d9eb8d5cf8d
MD5 dc854444b50a877e88198e40ec9d3503
BLAKE2b-256 6336c8ed9284f83ab168d8cb926392b239c9e95b8ab39bf7f6f3f7dda2e202db

See more details on using hashes here.

Supported by

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