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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c134b85a273440df3c51608ad6ae0e75ab029e142f822f0169b74b6f08391a12
|
|
| MD5 |
a75aeffe17863044f7e733fb33221de1
|
|
| BLAKE2b-256 |
0c9e8b3ed054a6b0cdd81b47c0c889e0ae12effcea469e7ea1a04bddb18d8786
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
96f98bf7cd43320f059fc8b4bf63806bbcce00b74b56cffb614ff1e6d8a2b567
|
|
| MD5 |
b88159760e97313e15e01b86e7e69f63
|
|
| BLAKE2b-256 |
491f724ff8d81548b136a30be04fc6a7ef3f34afe71a083dbbf8be2531d5838e
|