crashbytes-testkit 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- crashbytes_testkit-1.0.0/.github/CODEOWNERS +1 -0
- crashbytes_testkit-1.0.0/.github/workflows/ci.yml +23 -0
- crashbytes_testkit-1.0.0/.github/workflows/publish.yml +19 -0
- crashbytes_testkit-1.0.0/.gitignore +11 -0
- crashbytes_testkit-1.0.0/LICENSE +21 -0
- crashbytes_testkit-1.0.0/PKG-INFO +71 -0
- crashbytes_testkit-1.0.0/README.md +43 -0
- crashbytes_testkit-1.0.0/pyproject.toml +53 -0
- crashbytes_testkit-1.0.0/src/crashbytes_testkit/__init__.py +5 -0
- crashbytes_testkit-1.0.0/src/crashbytes_testkit/_core.py +146 -0
- crashbytes_testkit-1.0.0/src/crashbytes_testkit/py.typed +0 -0
- crashbytes_testkit-1.0.0/tests/test_testkit.py +289 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
* @CrashBytes
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
- uses: actions/setup-python@v5
|
|
18
|
+
with:
|
|
19
|
+
python-version: ${{ matrix.python-version }}
|
|
20
|
+
- run: pip install -e ".[dev]"
|
|
21
|
+
- run: ruff check src/ tests/
|
|
22
|
+
- run: mypy --strict src/
|
|
23
|
+
- run: pytest --cov=crashbytes_testkit --cov-branch --cov-fail-under=90
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ["*"]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
publish:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
permissions:
|
|
11
|
+
id-token: write
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
- uses: actions/setup-python@v5
|
|
15
|
+
with:
|
|
16
|
+
python-version: "3.12"
|
|
17
|
+
- run: pip install build
|
|
18
|
+
- run: python -m build
|
|
19
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 CrashBytes
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: crashbytes-testkit
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Test data builders for Python — auto-generate dataclass instances from type hints.
|
|
5
|
+
Project-URL: Homepage, https://github.com/CrashBytes/crashbytes-testkit
|
|
6
|
+
Project-URL: Repository, https://github.com/CrashBytes/crashbytes-testkit
|
|
7
|
+
Project-URL: Issues, https://github.com/CrashBytes/crashbytes-testkit/issues
|
|
8
|
+
Author-email: CrashBytes <crashbytes@users.noreply.github.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: builder,dataclass,fixture,test-data,testing
|
|
12
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: mypy; extra == 'dev'
|
|
24
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest-cov; extra == 'dev'
|
|
26
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# crashbytes-testkit
|
|
30
|
+
|
|
31
|
+
Test data builders for Python — auto-generate dataclass instances from type hints.
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install crashbytes-testkit
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Usage
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from dataclasses import dataclass
|
|
43
|
+
from crashbytes_testkit import Fixture, Builder
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class User:
|
|
47
|
+
name: str
|
|
48
|
+
age: int
|
|
49
|
+
email: str
|
|
50
|
+
|
|
51
|
+
# Auto-generate from type hints
|
|
52
|
+
user = Fixture.create(User)
|
|
53
|
+
users = Fixture.create_many(User, 5)
|
|
54
|
+
|
|
55
|
+
# Override specific fields
|
|
56
|
+
admin = Fixture.create(User, name="Admin", age=30)
|
|
57
|
+
|
|
58
|
+
# Fluent builder
|
|
59
|
+
user = (
|
|
60
|
+
Builder(User)
|
|
61
|
+
.with_field("name", "Alice")
|
|
62
|
+
.with_field("age", 25)
|
|
63
|
+
.build()
|
|
64
|
+
)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Supports: `str`, `int`, `float`, `bool`, `bytes`, `datetime`, `date`, `UUID`, `list[T]`, `dict[K,V]`, `set[T]`, `tuple`, `Optional[T]`, nested dataclasses.
|
|
68
|
+
|
|
69
|
+
## License
|
|
70
|
+
|
|
71
|
+
MIT
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# crashbytes-testkit
|
|
2
|
+
|
|
3
|
+
Test data builders for Python — auto-generate dataclass instances from type hints.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install crashbytes-testkit
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from crashbytes_testkit import Fixture, Builder
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class User:
|
|
19
|
+
name: str
|
|
20
|
+
age: int
|
|
21
|
+
email: str
|
|
22
|
+
|
|
23
|
+
# Auto-generate from type hints
|
|
24
|
+
user = Fixture.create(User)
|
|
25
|
+
users = Fixture.create_many(User, 5)
|
|
26
|
+
|
|
27
|
+
# Override specific fields
|
|
28
|
+
admin = Fixture.create(User, name="Admin", age=30)
|
|
29
|
+
|
|
30
|
+
# Fluent builder
|
|
31
|
+
user = (
|
|
32
|
+
Builder(User)
|
|
33
|
+
.with_field("name", "Alice")
|
|
34
|
+
.with_field("age", 25)
|
|
35
|
+
.build()
|
|
36
|
+
)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Supports: `str`, `int`, `float`, `bool`, `bytes`, `datetime`, `date`, `UUID`, `list[T]`, `dict[K,V]`, `set[T]`, `tuple`, `Optional[T]`, nested dataclasses.
|
|
40
|
+
|
|
41
|
+
## License
|
|
42
|
+
|
|
43
|
+
MIT
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "crashbytes-testkit"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Test data builders for Python — auto-generate dataclass instances from type hints."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [{ name = "CrashBytes", email = "crashbytes@users.noreply.github.com" }]
|
|
13
|
+
keywords = ["testing", "fixture", "builder", "dataclass", "test-data"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 5 - Production/Stable",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
"Typing :: Typed",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.optional-dependencies]
|
|
27
|
+
dev = ["pytest", "pytest-cov", "mypy", "ruff"]
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://github.com/CrashBytes/crashbytes-testkit"
|
|
31
|
+
Repository = "https://github.com/CrashBytes/crashbytes-testkit"
|
|
32
|
+
Issues = "https://github.com/CrashBytes/crashbytes-testkit/issues"
|
|
33
|
+
|
|
34
|
+
[tool.ruff]
|
|
35
|
+
target-version = "py310"
|
|
36
|
+
line-length = 99
|
|
37
|
+
|
|
38
|
+
[tool.ruff.lint]
|
|
39
|
+
select = ["E", "F", "I", "N", "UP", "B", "SIM", "TCH"]
|
|
40
|
+
|
|
41
|
+
[tool.mypy]
|
|
42
|
+
strict = true
|
|
43
|
+
python_version = "3.10"
|
|
44
|
+
|
|
45
|
+
[tool.pytest.ini_options]
|
|
46
|
+
testpaths = ["tests"]
|
|
47
|
+
|
|
48
|
+
[tool.coverage.run]
|
|
49
|
+
branch = true
|
|
50
|
+
source = ["crashbytes_testkit"]
|
|
51
|
+
|
|
52
|
+
[tool.coverage.report]
|
|
53
|
+
fail_under = 90
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""Test data builders — auto-generate dataclass instances from type hints."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import dataclasses
|
|
6
|
+
import random
|
|
7
|
+
import string
|
|
8
|
+
import uuid
|
|
9
|
+
from datetime import date, datetime
|
|
10
|
+
from typing import Any, TypeVar, get_type_hints
|
|
11
|
+
|
|
12
|
+
T = TypeVar("T")
|
|
13
|
+
|
|
14
|
+
_counter = 0
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _next_id() -> int:
|
|
18
|
+
global _counter # noqa: PLW0603
|
|
19
|
+
_counter += 1
|
|
20
|
+
return _counter
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _random_string(length: int = 8) -> str:
|
|
24
|
+
return "".join(random.choices(string.ascii_lowercase, k=length))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _generate_value(type_hint: Any, field_name: str = "") -> Any:
|
|
28
|
+
"""Generate a plausible value for a given type hint."""
|
|
29
|
+
origin = getattr(type_hint, "__origin__", None)
|
|
30
|
+
args = getattr(type_hint, "__args__", ())
|
|
31
|
+
|
|
32
|
+
# Handle Union types (Optional[X], X | Y, etc.)
|
|
33
|
+
import types
|
|
34
|
+
import typing
|
|
35
|
+
if origin is typing.Union or isinstance(type_hint, types.UnionType):
|
|
36
|
+
type_args = args if args else getattr(type_hint, "__args__", ())
|
|
37
|
+
non_none = [a for a in type_args if a is not type(None)]
|
|
38
|
+
if non_none:
|
|
39
|
+
return _generate_value(non_none[0], field_name)
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
if type_hint is str:
|
|
43
|
+
return f"{field_name}_{_random_string()}" if field_name else _random_string()
|
|
44
|
+
if type_hint is int:
|
|
45
|
+
return _next_id()
|
|
46
|
+
if type_hint is float:
|
|
47
|
+
return round(random.uniform(0.0, 100.0), 2)
|
|
48
|
+
if type_hint is bool:
|
|
49
|
+
return random.choice([True, False])
|
|
50
|
+
if type_hint is bytes:
|
|
51
|
+
return _random_string().encode()
|
|
52
|
+
if type_hint is datetime:
|
|
53
|
+
return datetime(2024, 1, 1, 12, 0, 0)
|
|
54
|
+
if type_hint is date:
|
|
55
|
+
return date(2024, 1, 1)
|
|
56
|
+
|
|
57
|
+
# list[X]
|
|
58
|
+
if origin is list:
|
|
59
|
+
inner = args[0] if args else str
|
|
60
|
+
return [_generate_value(inner, field_name) for _ in range(2)]
|
|
61
|
+
|
|
62
|
+
# dict[K, V]
|
|
63
|
+
if origin is dict:
|
|
64
|
+
k_type = args[0] if args else str
|
|
65
|
+
v_type = args[1] if len(args) > 1 else str # type: ignore[misc]
|
|
66
|
+
return {_generate_value(k_type): _generate_value(v_type) for _ in range(2)}
|
|
67
|
+
|
|
68
|
+
# set[X]
|
|
69
|
+
if origin is set:
|
|
70
|
+
inner = args[0] if args else str
|
|
71
|
+
return {_generate_value(inner, field_name) for _ in range(2)}
|
|
72
|
+
|
|
73
|
+
# tuple[X, ...]
|
|
74
|
+
if origin is tuple:
|
|
75
|
+
if args:
|
|
76
|
+
return tuple(_generate_value(a, field_name) for a in args if a is not Ellipsis)
|
|
77
|
+
return ()
|
|
78
|
+
|
|
79
|
+
# UUID
|
|
80
|
+
if type_hint is uuid.UUID:
|
|
81
|
+
return uuid.uuid4()
|
|
82
|
+
|
|
83
|
+
# Nested dataclass
|
|
84
|
+
if dataclasses.is_dataclass(type_hint) and isinstance(type_hint, type):
|
|
85
|
+
return _create_instance(type_hint)
|
|
86
|
+
|
|
87
|
+
# Fallback
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _create_instance(cls: type[T], overrides: dict[str, Any] | None = None) -> T:
|
|
92
|
+
"""Create an instance of *cls* with auto-generated values."""
|
|
93
|
+
hints = get_type_hints(cls)
|
|
94
|
+
kwargs: dict[str, Any] = {}
|
|
95
|
+
|
|
96
|
+
for field in dataclasses.fields(cls): # type: ignore[arg-type]
|
|
97
|
+
if overrides and field.name in overrides:
|
|
98
|
+
kwargs[field.name] = overrides[field.name]
|
|
99
|
+
elif field.default is not dataclasses.MISSING:
|
|
100
|
+
kwargs[field.name] = field.default
|
|
101
|
+
elif field.default_factory is not dataclasses.MISSING:
|
|
102
|
+
kwargs[field.name] = field.default_factory()
|
|
103
|
+
else:
|
|
104
|
+
type_hint = hints.get(field.name, str)
|
|
105
|
+
kwargs[field.name] = _generate_value(type_hint, field.name)
|
|
106
|
+
|
|
107
|
+
return cls(**kwargs)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class Fixture:
|
|
111
|
+
"""Auto-generate test data from dataclass type hints."""
|
|
112
|
+
|
|
113
|
+
@staticmethod
|
|
114
|
+
def create(cls: type[T], **overrides: Any) -> T:
|
|
115
|
+
"""Create a single instance of *cls*."""
|
|
116
|
+
if not dataclasses.is_dataclass(cls):
|
|
117
|
+
raise TypeError(f"{cls.__name__} is not a dataclass")
|
|
118
|
+
return _create_instance(cls, overrides or None)
|
|
119
|
+
|
|
120
|
+
@staticmethod
|
|
121
|
+
def create_many(cls: type[T], count: int = 3, **overrides: Any) -> list[T]:
|
|
122
|
+
"""Create *count* instances of *cls*."""
|
|
123
|
+
return [Fixture.create(cls, **overrides) for _ in range(count)]
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class Builder:
|
|
127
|
+
"""Fluent builder for constructing test data."""
|
|
128
|
+
|
|
129
|
+
def __init__(self, cls: type[Any]) -> None:
|
|
130
|
+
if not dataclasses.is_dataclass(cls):
|
|
131
|
+
raise TypeError(f"{cls.__name__} is not a dataclass")
|
|
132
|
+
self._cls = cls
|
|
133
|
+
self._overrides: dict[str, Any] = {}
|
|
134
|
+
|
|
135
|
+
def with_field(self, name: str, value: Any) -> Builder:
|
|
136
|
+
"""Set a specific field value."""
|
|
137
|
+
self._overrides[name] = value
|
|
138
|
+
return self
|
|
139
|
+
|
|
140
|
+
def build(self) -> Any:
|
|
141
|
+
"""Build the instance."""
|
|
142
|
+
return _create_instance(self._cls, self._overrides)
|
|
143
|
+
|
|
144
|
+
def build_many(self, count: int = 3) -> list[Any]:
|
|
145
|
+
"""Build *count* instances."""
|
|
146
|
+
return [self.build() for _ in range(count)]
|
|
File without changes
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"""Tests for crashbytes-testkit."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import uuid
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from datetime import date, datetime
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from crashbytes_testkit import Builder, Fixture
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class SimpleUser:
|
|
16
|
+
name: str
|
|
17
|
+
age: int
|
|
18
|
+
active: bool
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class Address:
|
|
23
|
+
street: str
|
|
24
|
+
city: str
|
|
25
|
+
zip_code: str
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class UserWithAddress:
|
|
30
|
+
name: str
|
|
31
|
+
address: Address
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class UserWithDefaults:
|
|
36
|
+
name: str
|
|
37
|
+
role: str = "user"
|
|
38
|
+
tags: list[str] = field(default_factory=list)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class UserWithOptional:
|
|
43
|
+
name: str
|
|
44
|
+
nickname: str | None = None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class UserWithCollections:
|
|
49
|
+
name: str
|
|
50
|
+
scores: list[int] = field(default_factory=list)
|
|
51
|
+
metadata: dict[str, str] = field(default_factory=dict)
|
|
52
|
+
unique_ids: set[int] = field(default_factory=set)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class UserWithTypes:
|
|
57
|
+
id: uuid.UUID
|
|
58
|
+
created_at: datetime
|
|
59
|
+
birth_date: date
|
|
60
|
+
score: float
|
|
61
|
+
data: bytes
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass
|
|
65
|
+
class UserWithTuple:
|
|
66
|
+
name: str
|
|
67
|
+
coords: tuple[float, float] = (0.0, 0.0)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class TestFixtureCreate:
|
|
71
|
+
def test_creates_instance(self) -> None:
|
|
72
|
+
user = Fixture.create(SimpleUser)
|
|
73
|
+
assert isinstance(user, SimpleUser)
|
|
74
|
+
assert isinstance(user.name, str)
|
|
75
|
+
assert isinstance(user.age, int)
|
|
76
|
+
assert isinstance(user.active, bool)
|
|
77
|
+
|
|
78
|
+
def test_overrides(self) -> None:
|
|
79
|
+
user = Fixture.create(SimpleUser, name="John", age=30)
|
|
80
|
+
assert user.name == "John"
|
|
81
|
+
assert user.age == 30
|
|
82
|
+
|
|
83
|
+
def test_with_defaults(self) -> None:
|
|
84
|
+
user = Fixture.create(UserWithDefaults)
|
|
85
|
+
assert user.role == "user"
|
|
86
|
+
assert user.tags == []
|
|
87
|
+
|
|
88
|
+
def test_with_optional(self) -> None:
|
|
89
|
+
user = Fixture.create(UserWithOptional)
|
|
90
|
+
assert isinstance(user.name, str)
|
|
91
|
+
# nickname should be generated as a string (non-None branch)
|
|
92
|
+
assert user.nickname is None or isinstance(user.nickname, str)
|
|
93
|
+
|
|
94
|
+
def test_nested_dataclass(self) -> None:
|
|
95
|
+
user = Fixture.create(UserWithAddress)
|
|
96
|
+
assert isinstance(user.address, Address)
|
|
97
|
+
assert isinstance(user.address.street, str)
|
|
98
|
+
|
|
99
|
+
def test_with_types(self) -> None:
|
|
100
|
+
user = Fixture.create(UserWithTypes)
|
|
101
|
+
assert isinstance(user.id, uuid.UUID)
|
|
102
|
+
assert isinstance(user.created_at, datetime)
|
|
103
|
+
assert isinstance(user.birth_date, date)
|
|
104
|
+
assert isinstance(user.score, float)
|
|
105
|
+
assert isinstance(user.data, bytes)
|
|
106
|
+
|
|
107
|
+
def test_raises_for_non_dataclass(self) -> None:
|
|
108
|
+
with pytest.raises(TypeError, match="not a dataclass"):
|
|
109
|
+
Fixture.create(dict) # type: ignore[arg-type]
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class TestFixtureCreateMany:
|
|
113
|
+
def test_creates_multiple(self) -> None:
|
|
114
|
+
users = Fixture.create_many(SimpleUser, 5)
|
|
115
|
+
assert len(users) == 5
|
|
116
|
+
assert all(isinstance(u, SimpleUser) for u in users)
|
|
117
|
+
|
|
118
|
+
def test_default_count(self) -> None:
|
|
119
|
+
users = Fixture.create_many(SimpleUser)
|
|
120
|
+
assert len(users) == 3
|
|
121
|
+
|
|
122
|
+
def test_with_overrides(self) -> None:
|
|
123
|
+
users = Fixture.create_many(SimpleUser, 3, active=True)
|
|
124
|
+
assert all(u.active is True for u in users)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class TestBuilder:
|
|
128
|
+
def test_basic_build(self) -> None:
|
|
129
|
+
user = Builder(SimpleUser).build()
|
|
130
|
+
assert isinstance(user, SimpleUser)
|
|
131
|
+
|
|
132
|
+
def test_with_field(self) -> None:
|
|
133
|
+
user = Builder(SimpleUser).with_field("name", "Alice").with_field("age", 25).build()
|
|
134
|
+
assert user.name == "Alice"
|
|
135
|
+
assert user.age == 25
|
|
136
|
+
|
|
137
|
+
def test_build_many(self) -> None:
|
|
138
|
+
users = Builder(SimpleUser).with_field("active", True).build_many(4)
|
|
139
|
+
assert len(users) == 4
|
|
140
|
+
assert all(u.active is True for u in users)
|
|
141
|
+
|
|
142
|
+
def test_fluent_chaining(self) -> None:
|
|
143
|
+
builder = Builder(SimpleUser)
|
|
144
|
+
result = builder.with_field("name", "Bob")
|
|
145
|
+
assert result is builder # Returns self
|
|
146
|
+
|
|
147
|
+
def test_raises_for_non_dataclass(self) -> None:
|
|
148
|
+
with pytest.raises(TypeError, match="not a dataclass"):
|
|
149
|
+
Builder(list) # type: ignore[arg-type]
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class TestCollections:
|
|
153
|
+
def test_list_generation(self) -> None:
|
|
154
|
+
user = Fixture.create(UserWithCollections)
|
|
155
|
+
# Default factory will be used
|
|
156
|
+
assert isinstance(user.scores, list)
|
|
157
|
+
assert isinstance(user.metadata, dict)
|
|
158
|
+
|
|
159
|
+
def test_tuple_with_default(self) -> None:
|
|
160
|
+
user = Fixture.create(UserWithTuple)
|
|
161
|
+
assert user.coords == (0.0, 0.0)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
# Additional types for coverage
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@dataclass
|
|
168
|
+
class WithList:
|
|
169
|
+
items: list[str]
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@dataclass
|
|
173
|
+
class WithDict:
|
|
174
|
+
data: dict[str, int]
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@dataclass
|
|
178
|
+
class WithSet:
|
|
179
|
+
ids: set[int]
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@dataclass
|
|
183
|
+
class WithTuple:
|
|
184
|
+
pair: tuple[int, str]
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@dataclass
|
|
188
|
+
class WithBareList:
|
|
189
|
+
items: list[str]
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@dataclass
|
|
193
|
+
class WithOptionalStr:
|
|
194
|
+
name: str
|
|
195
|
+
bio: str | None = None
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class TestGenerateValuePaths:
|
|
199
|
+
def test_list_type(self) -> None:
|
|
200
|
+
obj = Fixture.create(WithList)
|
|
201
|
+
assert isinstance(obj.items, list)
|
|
202
|
+
assert len(obj.items) == 2
|
|
203
|
+
assert all(isinstance(i, str) for i in obj.items)
|
|
204
|
+
|
|
205
|
+
def test_dict_type(self) -> None:
|
|
206
|
+
obj = Fixture.create(WithDict)
|
|
207
|
+
assert isinstance(obj.data, dict)
|
|
208
|
+
assert len(obj.data) == 2
|
|
209
|
+
|
|
210
|
+
def test_set_type(self) -> None:
|
|
211
|
+
obj = Fixture.create(WithSet)
|
|
212
|
+
assert isinstance(obj.ids, set)
|
|
213
|
+
assert all(isinstance(i, int) for i in obj.ids)
|
|
214
|
+
|
|
215
|
+
def test_tuple_type(self) -> None:
|
|
216
|
+
obj = Fixture.create(WithTuple)
|
|
217
|
+
assert isinstance(obj.pair, tuple)
|
|
218
|
+
assert isinstance(obj.pair[0], int)
|
|
219
|
+
assert isinstance(obj.pair[1], str)
|
|
220
|
+
|
|
221
|
+
def test_optional_generates_value(self) -> None:
|
|
222
|
+
# Test the Union/Optional branch
|
|
223
|
+
obj = Fixture.create(WithOptionalStr)
|
|
224
|
+
assert isinstance(obj.name, str)
|
|
225
|
+
|
|
226
|
+
def test_bytes_type(self) -> None:
|
|
227
|
+
obj = Fixture.create(UserWithTypes)
|
|
228
|
+
assert isinstance(obj.data, bytes)
|
|
229
|
+
|
|
230
|
+
def test_float_type(self) -> None:
|
|
231
|
+
obj = Fixture.create(UserWithTypes)
|
|
232
|
+
assert isinstance(obj.score, float)
|
|
233
|
+
assert 0.0 <= obj.score <= 100.0
|
|
234
|
+
|
|
235
|
+
def test_uuid_type(self) -> None:
|
|
236
|
+
obj = Fixture.create(UserWithTypes)
|
|
237
|
+
assert isinstance(obj.id, uuid.UUID)
|
|
238
|
+
|
|
239
|
+
def test_datetime_type(self) -> None:
|
|
240
|
+
obj = Fixture.create(UserWithTypes)
|
|
241
|
+
assert isinstance(obj.created_at, datetime)
|
|
242
|
+
|
|
243
|
+
def test_date_type(self) -> None:
|
|
244
|
+
obj = Fixture.create(UserWithTypes)
|
|
245
|
+
assert isinstance(obj.birth_date, date)
|
|
246
|
+
|
|
247
|
+
def test_nested_dataclass(self) -> None:
|
|
248
|
+
obj = Fixture.create(UserWithAddress)
|
|
249
|
+
assert isinstance(obj.address, Address)
|
|
250
|
+
assert isinstance(obj.address.city, str)
|
|
251
|
+
|
|
252
|
+
def test_field_name_in_string(self) -> None:
|
|
253
|
+
user = Fixture.create(SimpleUser)
|
|
254
|
+
assert "name" in user.name
|
|
255
|
+
|
|
256
|
+
def test_create_many_unique(self) -> None:
|
|
257
|
+
users = Fixture.create_many(SimpleUser, 5)
|
|
258
|
+
names = [u.name for u in users]
|
|
259
|
+
assert len(set(names)) == 5 # All unique
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
@dataclass
|
|
263
|
+
class WithBareTuple:
|
|
264
|
+
data: tuple[()]
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
@dataclass
|
|
268
|
+
class WithUnknownType:
|
|
269
|
+
value: object
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
@dataclass
|
|
273
|
+
class WithOptionalTyping:
|
|
274
|
+
name: str | None
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
class TestEdgeCases:
|
|
278
|
+
def test_empty_tuple(self) -> None:
|
|
279
|
+
obj = Fixture.create(WithBareTuple)
|
|
280
|
+
assert isinstance(obj.data, tuple)
|
|
281
|
+
|
|
282
|
+
def test_unknown_type_returns_none(self) -> None:
|
|
283
|
+
obj = Fixture.create(WithUnknownType)
|
|
284
|
+
assert obj.value is None
|
|
285
|
+
|
|
286
|
+
def test_union_type_syntax(self) -> None:
|
|
287
|
+
obj = Fixture.create(WithOptionalTyping)
|
|
288
|
+
# Should generate a string (non-None branch of str | None)
|
|
289
|
+
assert obj.name is None or isinstance(obj.name, str)
|