Skip to main content

Partial support library for structured testing

Project description

speclike

A helper library for pytest designed to make test code clearer and more structured.

The following is reproduced from __init__.py.

"""

A helper library for pytest designed to make test code clearer and more structured.

It divides tests into two conceptual types:
    - Externally defined tests  (intended for **test logic reuse** across multiple cases)
    - Individual tests

Externally defined tests are composed of two function definitions that together form a single test:
    Dispatcher:
        Describes the Arrange and Assert phases of AAA testing.  
        Serves as a kind of test logic template.
    Actor:
        Describes the Act phase.  
        It is called from within the dispatcher template and contains the code that exercises the target behavior.

The placement of these functions is as follows:
    Dispatcher:
        Defined either at the top level or inside a class inheriting from `ExSpec`.  
        Must declare the arguments given to the actor by using `@case.actsig(...)`.
    Actor:
        Defined inside a class inheriting from `Spec`.  
        Uses `@case.ex(...)` to specify its corresponding dispatcher.  
        The method name must be `"_"`.
    Individual tests:
        Defined inside a class inheriting from `Spec`.

All of these definitions use decorators provided by the `Case` class,
which also handles labeling tests (for example: `@case.feature`, `@case.edge`, `@case.error`, etc.).

Two notable helpers are skipping and parametrization.  
Parametrization is provided through `.follows`, which automatically combines
the given parameters with the function’s argument information to generate a
corresponding `pytest.mark.parametrize`.

The decorator order and interaction with unrelated decorators are not yet finalized.  
In particular, when labeling or marking a dispatcher, place `@case.actsig` **inside** other decorators.

Example:
    ```python
    @case.edge_pass.follows(-1, 0, 1)
    @case.actsig(value=int)
    def check_near_zero(act, value):
        act(value)  # expect success, no exception.
    ```

Currently, test generation only occurs for classes inheriting from `Spec`.  
The system is still under development and may be unstable, though the API is mostly settled.


AI generated sample code:

# ------------------------------------------------------------
# Test target (user_service.py)
# ------------------------------------------------------------

class UserAlreadyExistsError(Exception):
    pass

class UserService:
    '''A simple service that registers users in memory.'''

    def __init__(self):
        self._users = {}

    def register(self, username: str, email: str):
        if username in self._users:
            raise UserAlreadyExistsError(f'{username} already exists')
        if '@' not in email:
            raise ValueError('Invalid email')
        self._users[username] = email
        return {'username': username, 'email': email}

    def get_user(self, username: str):
        return self._users.get(username)

    
# ------------------------------------------------------------
# Test module
# ------------------------------------------------------------
import pytest
from speclike import Case, Spec, ExSpec
from user_service import UserService, UserAlreadyExistsError

# ============================================================
# Case instance (decorator entry point)
# ============================================================
case = Case(as_pytestmark=True)

# ============================================================
# Dispatcher definitions (Arrange + Assert)
# ============================================================
@case.feature
@case.actsig(username=str, email=str)
def register_success(act, username, email):
    '''Dispatcher for successful user registration.'''
    # Arrange
    svc = UserService()
    # Act
    result = act(svc, username, email)
    # Assert
    assert result['username'] == username
    assert result['email'] == email
    assert svc.get_user(username) == email

@case.error
@case.actsig(username=str, email=str)
def register_failures(act, username, email):
    '''Dispatcher for expected registration failures.'''
    svc = UserService()
    # Act & Assert
    with pytest.raises(Exception) as e:
        act(svc, username, email)
    # Verify the raised error type
    assert isinstance(e.value, (ValueError, UserAlreadyExistsError))


# ============================================================
# Grouping dispatchers in an ExSpec class
# ============================================================
class UserDispatchers(ExSpec):

    @case.critical
    @case.actsig(username=str)
    def duplicate_registration(self, act, username):
        '''Dispatcher to check duplicate registration scenario.'''
        svc = UserService()
        svc.register(username, 'first@example.com')
        with pytest.raises(UserAlreadyExistsError):
            act(svc, username)


# ============================================================
# Spec: defines concrete Actors (Act phase)
# ============================================================
class UserRegistration(Spec):

    @case.ex(register_success)
    def _(self, svc: UserService, username: str, email: str):
        '''Actor for successful registration.'''
        return svc.register(username, email)

    @case.ex(register_failures)
    def _(self, svc: UserService, username: str, email: str):
        '''Actor for invalid or duplicate registration.'''
        return svc.register(username, email)

    @case.ex(UserDispatchers.duplicate_registration)
    def _(self, svc: UserService, username: str):
        '''Actor for duplicate registration test.'''
        svc.register(username, 'second@example.com')

    @case.edge.follows((['foo'],), (['bar', 'baz'],))
    def test_user_list_invariant(self, usernames):
        '''Individual test to ensure invariant of user list.'''
        svc = UserService()
        for name in usernames:
            svc.register(name, f'{name}@example.com')
        assert all('@' in v for v in svc._users.values())


$ pytest -v
=========================== test session starts ===========================
collected 5 items

test_user_service.py::TestUserRegistration::test_register_success PASSED
test_user_service.py::TestUserRegistration::test_register_failures PASSED
test_user_service.py::TestUserRegistration::test_duplicate_registration PASSED
test_user_service.py::TestUserRegistration::test_user_list_invariant[foo] PASSED
test_user_service.py::TestUserRegistration::test_user_list_invariant[bar,baz] PASSED
============================ 5 passed in 0.05s ============================

"""

from speclike.speclike import Spec, ExSpec, Case, CaseBase

__all__ = [
    "Spec", "ExSpec", "CaseBase", "Case"
]

Installation

pip install speclike

Status

This project is in very early development (alpha stage).
APIs and behavior may change without notice.


License

MIT License © 2025 minoru_jp

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

speclike-0.0.0.24.tar.gz (13.4 kB view details)

Uploaded Source

Built Distribution

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

speclike-0.0.0.24-py3-none-any.whl (13.1 kB view details)

Uploaded Python 3

File details

Details for the file speclike-0.0.0.24.tar.gz.

File metadata

  • Download URL: speclike-0.0.0.24.tar.gz
  • Upload date:
  • Size: 13.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.8

File hashes

Hashes for speclike-0.0.0.24.tar.gz
Algorithm Hash digest
SHA256 c134b85a273440df3c51608ad6ae0e75ab029e142f822f0169b74b6f08391a12
MD5 a75aeffe17863044f7e733fb33221de1
BLAKE2b-256 0c9e8b3ed054a6b0cdd81b47c0c889e0ae12effcea469e7ea1a04bddb18d8786

See more details on using hashes here.

File details

Details for the file speclike-0.0.0.24-py3-none-any.whl.

File metadata

  • Download URL: speclike-0.0.0.24-py3-none-any.whl
  • Upload date:
  • Size: 13.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.8

File hashes

Hashes for speclike-0.0.0.24-py3-none-any.whl
Algorithm Hash digest
SHA256 96f98bf7cd43320f059fc8b4bf63806bbcce00b74b56cffb614ff1e6d8a2b567
MD5 b88159760e97313e15e01b86e7e69f63
BLAKE2b-256 491f724ff8d81548b136a30be04fc6a7ef3f34afe71a083dbbf8be2531d5838e

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