apitestgen 1.0.1__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.
Files changed (42) hide show
  1. apitestgen-1.0.1/PKG-INFO +71 -0
  2. apitestgen-1.0.1/README.md +46 -0
  3. apitestgen-1.0.1/apitestgen/__init__.py +0 -0
  4. apitestgen-1.0.1/apitestgen/core/__init__.py +0 -0
  5. apitestgen-1.0.1/apitestgen/core/base_generator.py +76 -0
  6. apitestgen-1.0.1/apitestgen/core/config.py +79 -0
  7. apitestgen-1.0.1/apitestgen/core/format_generator.py +108 -0
  8. apitestgen-1.0.1/apitestgen/core/generators/__init__.py +0 -0
  9. apitestgen-1.0.1/apitestgen/core/generators/array_generator.py +144 -0
  10. apitestgen-1.0.1/apitestgen/core/generators/boolean_generator.py +46 -0
  11. apitestgen-1.0.1/apitestgen/core/generators/integer_generator.py +127 -0
  12. apitestgen-1.0.1/apitestgen/core/generators/null_generator.py +33 -0
  13. apitestgen-1.0.1/apitestgen/core/generators/number_generator.py +111 -0
  14. apitestgen-1.0.1/apitestgen/core/generators/object_generator.py +60 -0
  15. apitestgen-1.0.1/apitestgen/core/generators/string_generator.py +194 -0
  16. apitestgen-1.0.1/apitestgen/core/ir.py +55 -0
  17. apitestgen-1.0.1/apitestgen/core/meta.py +37 -0
  18. apitestgen-1.0.1/apitestgen/formats/__init__.py +95 -0
  19. apitestgen-1.0.1/apitestgen/formats/json_schema/__init__.py +0 -0
  20. apitestgen-1.0.1/apitestgen/formats/json_schema/adapter.py +114 -0
  21. apitestgen-1.0.1/apitestgen/formats/json_schema/generator.py +25 -0
  22. apitestgen-1.0.1/apitestgen/formats/json_schema/logic.py +225 -0
  23. apitestgen-1.0.1/apitestgen/formats/openapi/__init__.py +0 -0
  24. apitestgen-1.0.1/apitestgen/formats/openapi/adapter.py +64 -0
  25. apitestgen-1.0.1/apitestgen/formats/openapi/generator.py +8 -0
  26. apitestgen-1.0.1/apitestgen/formats/openapi/logic.py +64 -0
  27. apitestgen-1.0.1/apitestgen/negative/__init__.py +0 -0
  28. apitestgen-1.0.1/apitestgen/negative/modifier.py +186 -0
  29. apitestgen-1.0.1/apitestgen/negative/result.py +167 -0
  30. apitestgen-1.0.1/apitestgen/negative/tag.py +70 -0
  31. apitestgen-1.0.1/apitestgen/orchestration/__init__.py +0 -0
  32. apitestgen-1.0.1/apitestgen/orchestration/multi_scene.py +77 -0
  33. apitestgen-1.0.1/apitestgen/orchestration/negative_runner.py +143 -0
  34. apitestgen-1.0.1/apitestgen/orchestration/validator.py +76 -0
  35. apitestgen-1.0.1/apitestgen/orchestration/variant_generator.py +256 -0
  36. apitestgen-1.0.1/apitestgen.egg-info/PKG-INFO +71 -0
  37. apitestgen-1.0.1/apitestgen.egg-info/SOURCES.txt +40 -0
  38. apitestgen-1.0.1/apitestgen.egg-info/dependency_links.txt +1 -0
  39. apitestgen-1.0.1/apitestgen.egg-info/requires.txt +2 -0
  40. apitestgen-1.0.1/apitestgen.egg-info/top_level.txt +1 -0
  41. apitestgen-1.0.1/setup.cfg +4 -0
  42. apitestgen-1.0.1/setup.py +25 -0
@@ -0,0 +1,71 @@
1
+ Metadata-Version: 2.4
2
+ Name: apitestgen
3
+ Version: 1.0.1
4
+ Summary: 智能测试数据生成器 - 支持 JSON Schema / OpenAPI 的正向和负向测试用例生成
5
+ Home-page: https://github.com/your/apitestgen
6
+ Author: 盼盼
7
+ Author-email: 2063769412@qq.com
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Topic :: Software Development :: Testing
12
+ Requires-Python: >=3.8
13
+ Description-Content-Type: text/markdown
14
+ Requires-Dist: jsonschema>=4.0.0
15
+ Requires-Dist: rstr>=3.0.0
16
+ Dynamic: author
17
+ Dynamic: author-email
18
+ Dynamic: classifier
19
+ Dynamic: description
20
+ Dynamic: description-content-type
21
+ Dynamic: home-page
22
+ Dynamic: requires-dist
23
+ Dynamic: requires-python
24
+ Dynamic: summary
25
+
26
+ # apitestgen
27
+
28
+ 智能 API 测试数据生成器,支持 JSON Schema / OpenAPI 的正向和负向测试用例生成。
29
+
30
+ ## 安装
31
+
32
+ ```bash
33
+ pip install apitestgen
34
+ ```
35
+
36
+ ##快速开始
37
+
38
+ from apitestgen.formats import create_generator
39
+
40
+ schema = {
41
+ "type": "object",
42
+ "required": ["name", "age"],
43
+ "properties": {
44
+ "name": {"type": "string", "minLength": 2, "maxLength": 10},
45
+ "age": {"type": "integer", "minimum": 0, "maximum": 150}
46
+ }
47
+ }
48
+
49
+ gen = create_generator(schema)
50
+
51
+ # 正向生成
52
+ print(gen.generate())
53
+
54
+ # 多场景
55
+ for v in gen.generates():
56
+ print(v["label"], v["data"])
57
+
58
+ # 负向测试
59
+ for case in gen.generate_negative():
60
+ print(case.sub_case_id, case.data)
61
+
62
+ ## 特性
63
+ 正向单次/多场景数据生成
64
+
65
+ 负向测试用例生成(边界值、类型错误、枚举违反等)
66
+
67
+ Schema 校验
68
+
69
+ 组合策略(single/pairwise/combinatorial)
70
+
71
+ 支持 JSON Schema 和 OpenAPI
@@ -0,0 +1,46 @@
1
+ # apitestgen
2
+
3
+ 智能 API 测试数据生成器,支持 JSON Schema / OpenAPI 的正向和负向测试用例生成。
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ pip install apitestgen
9
+ ```
10
+
11
+ ##快速开始
12
+
13
+ from apitestgen.formats import create_generator
14
+
15
+ schema = {
16
+ "type": "object",
17
+ "required": ["name", "age"],
18
+ "properties": {
19
+ "name": {"type": "string", "minLength": 2, "maxLength": 10},
20
+ "age": {"type": "integer", "minimum": 0, "maximum": 150}
21
+ }
22
+ }
23
+
24
+ gen = create_generator(schema)
25
+
26
+ # 正向生成
27
+ print(gen.generate())
28
+
29
+ # 多场景
30
+ for v in gen.generates():
31
+ print(v["label"], v["data"])
32
+
33
+ # 负向测试
34
+ for case in gen.generate_negative():
35
+ print(case.sub_case_id, case.data)
36
+
37
+ ## 特性
38
+ 正向单次/多场景数据生成
39
+
40
+ 负向测试用例生成(边界值、类型错误、枚举违反等)
41
+
42
+ Schema 校验
43
+
44
+ 组合策略(single/pairwise/combinatorial)
45
+
46
+ 支持 JSON Schema 和 OpenAPI
File without changes
File without changes
@@ -0,0 +1,76 @@
1
+ # api/core/generator.py
2
+ """基础生成器 - 工具方法"""
3
+
4
+ import random
5
+ import logging
6
+ from typing import Any, Optional, Tuple, List
7
+
8
+ from .meta import GeneratorMeta
9
+ from .config import GeneratorConfig
10
+ from apitestgen.negative.result import FieldNegativeValue
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class BaseGenerator(metaclass=GeneratorMeta):
15
+ """基础生成器:工具方法"""
16
+
17
+ def __init__(self, field, config: Optional[GeneratorConfig] = None, depth: int = 0, **kwargs):
18
+ self.field = field
19
+ self.config = config or GeneratorConfig
20
+ self.depth = depth
21
+
22
+ def generate(self) -> Any:
23
+ if self.depth > self.config.MAX_DEPTH:
24
+ return None
25
+ return self._default_value()
26
+
27
+ def _default_value(self) -> Any:
28
+ return self.config.TYPE_DEFAULTS.get(self.field.type, None)
29
+
30
+ def _should_generate_optional(self, rate: float) -> bool:
31
+ return random.random() < rate
32
+
33
+ def _make(self, value) -> Any:
34
+ return FieldNegativeValue(value=value)
35
+
36
+ def parse_numeric_bounds(self) -> Tuple[Optional[float], Optional[float], bool, bool]:
37
+ return (
38
+ self.field.minimum,
39
+ self.field.maximum,
40
+ bool(self.field.exclusive_minimum),
41
+ bool(self.field.exclusive_maximum),
42
+ )
43
+
44
+ def generate_field_negative(self, tags: Optional[List[str]] = None) -> List:
45
+ """自动发现 @negative_case 方法"""
46
+ if not self.config.enable_negative:
47
+ return []
48
+
49
+ from apitestgen.negative.tag import NegativeTag
50
+
51
+ cases = []
52
+ tag_set = set(tags) if tags else None
53
+
54
+ for attr_name in dir(self):
55
+ method = getattr(self, attr_name, None)
56
+ if not callable(method):
57
+ continue
58
+ if not getattr(method, '_is_negative_method', False):
59
+ continue
60
+
61
+ tag = method._negative_tag
62
+ if tag_set and tag not in tag_set:
63
+ continue
64
+
65
+ try:
66
+ result = method()
67
+ if result and isinstance(result, list):
68
+ for fv in result:
69
+ fv.sub_case_id = tag
70
+ fv.description = getattr(method, '_negative_description', tag)
71
+ fv.expected_error_type = NegativeTag.error(tag)
72
+ cases.extend(result)
73
+ except Exception as e:
74
+ logger.warning(f"{self.__class__.__name__}.{attr_name} 失败: {e}")
75
+
76
+ return cases
@@ -0,0 +1,79 @@
1
+ # api/core/config.py
2
+ """生成器通用配置"""
3
+
4
+ from typing import Dict, Any
5
+
6
+
7
+ class GeneratorConfig:
8
+ """生成器通用配置"""
9
+
10
+ MAX_DEPTH: int = 10
11
+ OPTIONAL_RATE: float = 1.0 # 1.0=所有字段, 0.0=仅必填
12
+
13
+ GENERATE_MODE: str = "single" # single | combinatorial
14
+ EXPAND_BOOLEAN: bool = True # 展开布尔 True/False
15
+ EXPAND_ENUM: bool = True # 展开枚举所有值
16
+ EXPAND_TUPLE: bool = True # 展开元组数组
17
+ EXPAND_NESTED_REQUIRED: bool = True # 展开嵌套必填
18
+ MAX_COMBINATIONS: int = 100 # 最大组合数
19
+ INCLUDE_EMPTY: bool = True # 输出空对象/空数组
20
+ # ========== 组合策略 ==========
21
+ COMBINE_MODE: str = "single" # single | pairwise | combinatorial
22
+
23
+ # ========== 校验 ==========
24
+ ENABLE_VALIDATION: bool = False # 是否启用 Schema 校验
25
+
26
+
27
+ # 字符串
28
+ DEFAULT_MIN_LENGTH: int = 0
29
+ DEFAULT_MAX_LENGTH: int = 100
30
+ STRING_CHARSET: str = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
31
+ DEFAULT_STRING_MAX_LEN: int = 50
32
+ DEFAULT_PATTERN_STRING_MAX_LEN: int = 256
33
+ MAX_STRING_LENGTH_INCREMENT: int = 20
34
+ AX_PATTERN_ATTEMPTS: int = 100
35
+ INVALID_CHARSET: str = "!@#$%^&*()_+-=[]{}|;:',.<>?/~`"
36
+
37
+
38
+ # 数组
39
+ DEFAULT_MIN_ITEMS: int = 0
40
+ DEFAULT_MAX_ITEMS: int = 10
41
+ EXPAND_BOOLEAN: bool = True
42
+ EXPAND_TUPLE_ARRAY: bool = True
43
+ EXPAND_NESTED_REQUIRED: bool = True # 嵌套必填
44
+ EXPAND_IF_THEN_ELSE: bool = True # if-then-else 分支
45
+ EXPAND_ONEOF: bool = True # oneOf 分支
46
+
47
+ # 数值
48
+ DEFAULT_INT_MIN: int = -10000
49
+ DEFAULT_INT_MAX: int = 10000
50
+ DEFAULT_NUM_MIN: float = -1000000.0
51
+ DEFAULT_NUM_MAX: float = 1000000.0
52
+ DEFAULT_PRECISION: int = 10
53
+
54
+ # 精度
55
+ EPSILON: float = 1e-9
56
+
57
+ # 负向测试
58
+ enable_negative: bool = True
59
+ max_cases_per_tag: int = 1
60
+ boundary_offset_integer: int = 1
61
+ boundary_offset_number: float = 0.1
62
+ boundary_offset_string: int = 1
63
+ boundary_offset_array: int = 1
64
+
65
+ # 类型默认值
66
+ TYPE_DEFAULTS: Dict[str, Any] = {
67
+ 'string': '', 'integer': 0, 'number': 0.0,
68
+ 'boolean': False, 'array': [], 'object': {}, 'null': None,
69
+ }
70
+
71
+ WRONG_TYPE_MAP: Dict[str, list] = {
72
+ 'string': [123, 1.5, True, [], {}, None],
73
+ 'integer': ["not_int", 1.5, True, [], {}, None],
74
+ 'number': ["not_num", True, [], {}, None],
75
+ 'boolean': ["not_bool", 0, 1, [], {}, None],
76
+ 'array': ["not_array", 123, 1.5, True, {}, None],
77
+ 'object': ["not_obj", 123, 1.5, True, [], None],
78
+ 'null': ["not_null", 0, 1.5, True, [], {}],
79
+ }
@@ -0,0 +1,108 @@
1
+ # api/formats/json_schema/format_generator.py
2
+ """格式生成器 - 基于 Faker 生成 format 数据"""
3
+
4
+ import random
5
+ from typing import Any, Optional
6
+
7
+
8
+ try:
9
+ from faker import Faker
10
+ FAKER_AVAILABLE = True
11
+ except ImportError:
12
+ FAKER_AVAILABLE = False
13
+
14
+
15
+ class FormatGenerator:
16
+ """格式生成器"""
17
+
18
+ def __init__(self):
19
+ self._faker = None
20
+
21
+ @property
22
+ def faker(self):
23
+ if self._faker is None and FAKER_AVAILABLE:
24
+ self._faker = Faker()
25
+ return self._faker
26
+
27
+ def generate(self, format_name: str) -> Optional[str]:
28
+ handlers = {
29
+ 'email': self._email,
30
+ 'uri': self._uri,
31
+ 'url': self._url,
32
+ 'date': self._date,
33
+ 'date-time': self._date_time,
34
+ 'time': self._time,
35
+ 'ipv4': self._ipv4,
36
+ 'ipv6': self._ipv6,
37
+ 'uuid': self._uuid,
38
+ 'hostname': self._hostname,
39
+ 'phone': self._phone,
40
+ }
41
+
42
+ handler = handlers.get(format_name)
43
+ if handler:
44
+ return handler()
45
+ return self._fallback()
46
+
47
+ def _email(self):
48
+ if self.faker:
49
+ return self.faker.email()
50
+ return f"user{random.randint(1,9999)}@example.com"
51
+
52
+ def _uri(self):
53
+ if self.faker:
54
+ return self.faker.uri()
55
+ return f"https://www.example{random.randint(1,99)}.com/path"
56
+
57
+ def _url(self):
58
+ return self._uri()
59
+
60
+ def _date(self):
61
+ if self.faker:
62
+ return self.faker.date()
63
+ return "2024-01-15"
64
+
65
+ def _date_time(self):
66
+ if self.faker:
67
+ return self.faker.date_time().isoformat()
68
+ return "2024-01-15T10:30:00"
69
+
70
+ def _time(self):
71
+ if self.faker:
72
+ return self.faker.time()
73
+ return "10:30:00"
74
+
75
+ def _ipv4(self):
76
+ if self.faker:
77
+ return self.faker.ipv4()
78
+ return "192.168.1.1"
79
+
80
+ def _ipv6(self):
81
+ if self.faker:
82
+ return self.faker.ipv6()
83
+ return "::1"
84
+
85
+ def _uuid(self):
86
+ import uuid
87
+ return str(uuid.uuid4())
88
+
89
+ def _hostname(self):
90
+ if self.faker:
91
+ return self.faker.hostname()
92
+ return "host.example.com"
93
+
94
+ def _phone(self):
95
+ if self.faker:
96
+ return self.faker.phone_number()
97
+ return f"1{random.randint(3000000000, 9999999999)}"
98
+
99
+ def _fallback(self):
100
+ return f"format_{random.randint(1000, 9999)}"
101
+
102
+
103
+ # 全局实例
104
+ _format_generator = FormatGenerator()
105
+
106
+
107
+ def get_format_generator() -> FormatGenerator:
108
+ return _format_generator
@@ -0,0 +1,144 @@
1
+ # api/core/generators/array_generator.py
2
+ """数组生成器:正负向"""
3
+
4
+ import random
5
+ import logging
6
+ from typing import List
7
+
8
+ from ..base_generator import BaseGenerator
9
+ from ..meta import GeneratorMeta
10
+ from apitestgen.negative.tag import negative_case
11
+ from apitestgen.negative.result import FieldNegativeValue
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ @GeneratorMeta.register('array', alias=['list', 'tuple'])
17
+ class ArrayGenerator(BaseGenerator):
18
+ """数组:minItems / maxItems / uniqueItems"""
19
+
20
+ # ================================================================
21
+ # 正向
22
+ # ================================================================
23
+
24
+ def generate(self) -> list:
25
+ if self.field.const is not None:
26
+ return self.field.const
27
+ if self.field.enum:
28
+ return random.choice(self.field.enum)
29
+ if not self.field.items:
30
+ return []
31
+
32
+ lo = self.field.min_items if self.field.min_items is not None else self.config.DEFAULT_MIN_ITEMS
33
+ hi = self.field.max_items if self.field.max_items is not None else self.config.DEFAULT_MAX_ITEMS
34
+
35
+ if lo == 0 and hi > 0:
36
+ lo = 1
37
+
38
+ result = self._generate_items(lo, hi)
39
+
40
+ if self.field.unique_items and result:
41
+ seen = set()
42
+ unique = []
43
+ for item in result:
44
+ key = str(item)
45
+ if key not in seen:
46
+ seen.add(key)
47
+ unique.append(item)
48
+ result = unique
49
+
50
+ return result
51
+
52
+ # ================================================================
53
+ # 通用生成方法
54
+ # ================================================================
55
+
56
+ def _generate_items(self, min_items: int, max_items: int) -> list:
57
+ """根据 min/max 生成数组元素"""
58
+ if min_items > max_items:
59
+ min_items = max_items
60
+
61
+ if len(self.field.items) > 1:
62
+ # 元组类型:所有位置都生成
63
+ result = []
64
+ for item in self.field.items:
65
+ gen = BaseGenerator(item, config=self.config, depth=self.depth + 1)
66
+ result.append(gen.generate())
67
+
68
+ # 少于 min_items:随机补充
69
+ while len(result) < min_items:
70
+ extra_item = random.choice(self.field.items)
71
+ gen = BaseGenerator(extra_item, config=self.config, depth=self.depth + 1)
72
+ result.append(gen.generate())
73
+
74
+ # 超过 max_items:随机截取
75
+ if len(result) > max_items:
76
+ result = random.sample(result, max_items)
77
+ else:
78
+ # 列表类型:随机长度
79
+ length = random.randint(min_items, max_items)
80
+ item_schema = self.field.items[0]
81
+ result = [
82
+ BaseGenerator(item_schema, config=self.config, depth=self.depth + 1).generate()
83
+ for _ in range(length)
84
+ ]
85
+
86
+ return result
87
+
88
+ # ================================================================
89
+ # 负向
90
+ # ================================================================
91
+
92
+ @negative_case("F2_3", "数组元素过少")
93
+ def _too_few(self) -> List[FieldNegativeValue]:
94
+ if self.field.type != 'array':
95
+ return []
96
+ lo = self.field.min_items or 0
97
+ if lo <= 0:
98
+ return []
99
+ if not self.field.items:
100
+ return []
101
+
102
+ few = self._generate_items(0, max(0, lo - 1))
103
+ return [self._make(few)]
104
+
105
+ @negative_case("F2_4", "数组元素过多")
106
+ def _too_many(self) -> List[FieldNegativeValue]:
107
+ if self.field.type != 'array':
108
+ return []
109
+ hi = self.field.max_items
110
+ if hi is None:
111
+ return []
112
+ if not self.field.items:
113
+ return []
114
+
115
+ over = hi + self.config.boundary_offset_array
116
+ many = self._generate_items(over, over)
117
+ return [self._make(many)]
118
+
119
+ @negative_case("F4_1", "类型错误")
120
+ def _wrong_type(self) -> List[FieldNegativeValue]:
121
+ vals = self.config.WRONG_TYPE_MAP.get('array', [])
122
+ return [self._make(v) for v in vals[:self.config.max_cases_per_tag]]
123
+
124
+ @negative_case("F6_1", "枚举值违反")
125
+ def _enum_violation(self) -> List[FieldNegativeValue]:
126
+ if not self.field.enum:
127
+ return []
128
+ cases = []
129
+ for _ in range(self.config.max_cases_per_tag):
130
+ valid = random.choice(self.field.enum)
131
+ cases.append(self._make(valid + ["__INVALID__"]))
132
+ return cases
133
+
134
+ @negative_case("F7_1", "数组元素不唯一")
135
+ def _unique_violation(self) -> List[FieldNegativeValue]:
136
+ if not self.field.unique_items:
137
+ return []
138
+ if not self.field.items:
139
+ return []
140
+
141
+ item = random.choice(self.field.items)
142
+ gen = BaseGenerator(item, config=self.config)
143
+ dup = gen.generate()
144
+ return [self._make([dup, dup])]
@@ -0,0 +1,46 @@
1
+ # api/core/generators/boolean_generator.py
2
+ """布尔生成器:正负向"""
3
+
4
+ import random
5
+ from typing import List
6
+
7
+ from ..base_generator import BaseGenerator
8
+ from ..meta import GeneratorMeta
9
+ from apitestgen.negative.tag import negative_case
10
+ from apitestgen.negative.result import FieldNegativeValue
11
+
12
+
13
+ @GeneratorMeta.register('boolean', alias=['bool'])
14
+ class BooleanGenerator(BaseGenerator):
15
+ """布尔"""
16
+
17
+ def generate(self) -> bool:
18
+ if self.field.const is not None:
19
+ return self.field.const
20
+ if self.field.enum:
21
+ return random.choice(self.field.enum)
22
+ return random.choice([True, False])
23
+
24
+ @negative_case("F4_1", "类型错误")
25
+ def _wrong_type(self) -> List[FieldNegativeValue]:
26
+ vals = self.config.WRONG_TYPE_MAP.get('boolean', [])
27
+ return [self._make(v) for v in vals[:self.config.max_cases_per_tag]]
28
+
29
+ @negative_case("F6_1", "枚举值违反")
30
+ def _enum_violation(self) -> List[FieldNegativeValue]:
31
+ """不在枚举值范围内"""
32
+ if not self.field.enum:
33
+ return []
34
+
35
+ cases = []
36
+ for _ in range(self.config.max_cases_per_tag):
37
+ # 布尔枚举特殊处理:取一个不在枚举中的值
38
+ if True not in self.field.enum:
39
+ cases.append(self._make(True))
40
+ elif False not in self.field.enum:
41
+ cases.append(self._make(False))
42
+ else:
43
+ # 两个都在,用非布尔值
44
+ cases.append(self._make("NOT_BOOL"))
45
+
46
+ return cases