robotframework-schemathesislibrary 2.3.3__tar.gz → 2.5.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.
Files changed (14) hide show
  1. {robotframework_schemathesislibrary-2.3.3/src/robotframework_schemathesislibrary.egg-info → robotframework_schemathesislibrary-2.5.0}/PKG-INFO +26 -3
  2. {robotframework_schemathesislibrary-2.3.3 → robotframework_schemathesislibrary-2.5.0}/README.md +23 -0
  3. {robotframework_schemathesislibrary-2.3.3 → robotframework_schemathesislibrary-2.5.0}/pyproject.toml +12 -11
  4. robotframework_schemathesislibrary-2.5.0/src/SchemathesisLibrary/__init__.py +432 -0
  5. {robotframework_schemathesislibrary-2.3.3 → robotframework_schemathesislibrary-2.5.0/src/robotframework_schemathesislibrary.egg-info}/PKG-INFO +26 -3
  6. robotframework_schemathesislibrary-2.5.0/src/robotframework_schemathesislibrary.egg-info/requires.txt +4 -0
  7. robotframework_schemathesislibrary-2.3.3/src/SchemathesisLibrary/__init__.py +0 -370
  8. robotframework_schemathesislibrary-2.3.3/src/robotframework_schemathesislibrary.egg-info/requires.txt +0 -4
  9. {robotframework_schemathesislibrary-2.3.3 → robotframework_schemathesislibrary-2.5.0}/LICENSE +0 -0
  10. {robotframework_schemathesislibrary-2.3.3 → robotframework_schemathesislibrary-2.5.0}/setup.cfg +0 -0
  11. {robotframework_schemathesislibrary-2.3.3 → robotframework_schemathesislibrary-2.5.0}/src/SchemathesisLibrary/schemathesisreader.py +0 -0
  12. {robotframework_schemathesislibrary-2.3.3 → robotframework_schemathesislibrary-2.5.0}/src/robotframework_schemathesislibrary.egg-info/SOURCES.txt +0 -0
  13. {robotframework_schemathesislibrary-2.3.3 → robotframework_schemathesislibrary-2.5.0}/src/robotframework_schemathesislibrary.egg-info/dependency_links.txt +0 -0
  14. {robotframework_schemathesislibrary-2.3.3 → robotframework_schemathesislibrary-2.5.0}/src/robotframework_schemathesislibrary.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: robotframework-schemathesislibrary
3
- Version: 2.3.3
3
+ Version: 2.5.0
4
4
  Summary: Robot Framework SchemathesisLibrary to automatically create test cases from API specifications.
5
5
  Author-email: Tatu Aalto <aalto.tatu@gmail.com>
6
6
  License-Expression: Apache-2.0
@@ -10,8 +10,8 @@ Description-Content-Type: text/markdown
10
10
  License-File: LICENSE
11
11
  Requires-Dist: robotframework>=7.2.2
12
12
  Requires-Dist: robotframework-datadriver>=1.11.2
13
- Requires-Dist: robotframework-pythonlibcore>=4.5.0
14
- Requires-Dist: schemathesis>=4.18.0
13
+ Requires-Dist: robotframework-pythonlibcore>=4.6.0
14
+ Requires-Dist: schemathesis>=4.24.3
15
15
  Dynamic: license-file
16
16
 
17
17
  # Robot Framework SchemathesisLibrary
@@ -175,3 +175,26 @@ def filter_query(ctx, query) -> bool:
175
175
  return True
176
176
  ```
177
177
  Then only two test with `PUT` request are generated.
178
+
179
+ ## Pabot Support
180
+
181
+ [Pabot](https://pabot.org/) is a parallel executor for Robot Framework tests, and
182
+ `SchemathesisLibrary` supports it.
183
+
184
+ However, there's a crucial point to consider. Because `SchemathesisLibrary` uses
185
+ [DataDriver](https://github.com/Snooz82/robotframework-datadriver) to generate
186
+ tests dynamically, you cannot use Schemathesis's built-in parallel execution
187
+ feature (`--workers` option).
188
+
189
+ When using Pabot, you are responsible for organizing your test suites to avoid
190
+ generating the same tests in multiple parallel processes. A good strategy might be to
191
+ split your test into different suites, where each suite targets a specific scenario.
192
+
193
+ For example, you could create separate suites for:
194
+ - A user that is not found in the system.
195
+ - A user that is found and has valid credentials.
196
+ - A user that is found but has invalid credentials.
197
+
198
+ This approach ensures that each Pabot process works on a unique set of tests. Since
199
+ every API and system is different, a universal rule for structuring suites is not
200
+ possible, but this scenario-based division might be good starting point.
@@ -159,3 +159,26 @@ def filter_query(ctx, query) -> bool:
159
159
  return True
160
160
  ```
161
161
  Then only two test with `PUT` request are generated.
162
+
163
+ ## Pabot Support
164
+
165
+ [Pabot](https://pabot.org/) is a parallel executor for Robot Framework tests, and
166
+ `SchemathesisLibrary` supports it.
167
+
168
+ However, there's a crucial point to consider. Because `SchemathesisLibrary` uses
169
+ [DataDriver](https://github.com/Snooz82/robotframework-datadriver) to generate
170
+ tests dynamically, you cannot use Schemathesis's built-in parallel execution
171
+ feature (`--workers` option).
172
+
173
+ When using Pabot, you are responsible for organizing your test suites to avoid
174
+ generating the same tests in multiple parallel processes. A good strategy might be to
175
+ split your test into different suites, where each suite targets a specific scenario.
176
+
177
+ For example, you could create separate suites for:
178
+ - A user that is not found in the system.
179
+ - A user that is found and has valid credentials.
180
+ - A user that is found but has invalid credentials.
181
+
182
+ This approach ensures that each Pabot process works on a unique set of tests. Since
183
+ every API and system is different, a universal rule for structuring suites is not
184
+ possible, but this scenario-based division might be good starting point.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "robotframework-schemathesislibrary"
3
- version = "2.3.3"
3
+ version = "2.5.0"
4
4
  description = "Robot Framework SchemathesisLibrary to automatically create test cases from API specifications."
5
5
  authors = [
6
6
  { name="Tatu Aalto", email="aalto.tatu@gmail.com" },
@@ -12,22 +12,23 @@ keywords = ["Robot Framework", "Schemathesis", "API Testing", "Test Automation"]
12
12
  dependencies = [
13
13
  "robotframework>=7.2.2",
14
14
  "robotframework-datadriver>=1.11.2",
15
- "robotframework-pythonlibcore>=4.5.0",
16
- "schemathesis>=4.18.0",
15
+ "robotframework-pythonlibcore>=4.6.0",
16
+ "schemathesis>=4.24.3",
17
17
  ]
18
18
 
19
19
  [dependency-groups]
20
20
  dev = [
21
- "coverage>=7.13.5",
22
- "fastapi[standard]>=0.136.1",
21
+ "coverage>=7.15.2",
22
+ "fastapi[standard]>=0.140.0",
23
23
  "invoke>=3.0.3",
24
- "mypy>=1.20.2",
25
- "pytest>=9.0.3",
26
- "requests>=2.33.1",
24
+ "mypy>=2.1.0",
25
+ "pytest>=9.1.1",
26
+ "requests>=2.34.2",
27
+ "robotframework-pabot>=5.2.2",
27
28
  "robotframework-requests>=0.9.7",
28
- "robotframework-robocop>=8.2.7",
29
- "ruff==0.15.12",
30
- "types-requests>=2.33.0.20260503",
29
+ "robotframework-robocop>=8.3.2",
30
+ "ruff==0.16.0",
31
+ "markdown>=3.10.2",
31
32
  ]
32
33
 
33
34
  [tool.semantic_release.settings]
@@ -0,0 +1,432 @@
1
+ # Copyright 2025- Tatu Aalto
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ from typing import TYPE_CHECKING, Any
15
+
16
+ from DataDriver import DataDriver # type: ignore
17
+ from requests.sessions import Session
18
+ from robot.api import logger
19
+ from robot.api.deco import keyword
20
+ from robot.result.model import TestCase as ResultTestCase # type: ignore
21
+ from robot.result.model import TestSuite as ResultTestSuite # type: ignore
22
+ from robot.running.model import TestCase, TestSuite # type: ignore
23
+ from robot.utils.dotdict import DotDict # type: ignore
24
+ from robotlibcore import DynamicCore # type: ignore
25
+ from schemathesis import Case
26
+ from schemathesis.core import NotSet
27
+ from schemathesis.core.transport import Response
28
+
29
+ from .schemathesisreader import Options, SchemathesisReader
30
+
31
+ if TYPE_CHECKING:
32
+ from pathlib import Path
33
+
34
+ __version__ = "2.5.0"
35
+
36
+
37
+ class SchemathesisLibrary(DynamicCore):
38
+ """SchemathesisLibrary is a library for validating API schema using Schemathesis tool.
39
+
40
+ Example usage of the library and the [Call And Validate] keyword
41
+
42
+ Library must be imported with the ``url`` or ``path`` argument to specify the
43
+ OpenAPI schema. The library uses
44
+ [DataDriver](https://github.com/Snooz82/robotframework-datadriver) to generate
45
+ test cases from the OpenAPI schema by using
46
+ [Schemathesis](https://github.com/schemathesis/schemathesis). The library
47
+ creates test cases that takes one argument, ``${case}``, which is a
48
+ Schemathesis
49
+ [Case](https://schemathesis.readthedocs.io/en/stable/reference/python/#schemathesis.Case)
50
+ object. The `Call And Validate` keyword can be used to call and validate
51
+ the case. The keyword will log the request and response details.
52
+
53
+ Example:
54
+ ```robotframework
55
+ *** Settings ***
56
+ Library SchemathesisLibrary url=http://127.0.0.1/openapi.json
57
+
58
+ Test Template Wrapper
59
+
60
+ *** Test Cases ***
61
+ All Tests # This test is deleted by DataDriver
62
+ Wrapper test_case_1
63
+
64
+ *** Keywords ***
65
+ Wrapper
66
+ [Arguments] ${case}
67
+ Call And Validate ${case}
68
+ ```
69
+ """
70
+
71
+ ROBOT_LIBRARY_VERSION = __version__
72
+ ROBOT_LISTENER_API_VERSION = 3
73
+ ROBOT_LIBRARY_SCOPE = "TEST SUITE"
74
+ ROBOT_LIBRARY_DOC_FORMAT = "Markdown"
75
+
76
+ def __init__( # noqa: PLR0913
77
+ self,
78
+ *,
79
+ headers: "dict[str, Any]|None" = None,
80
+ max_examples: int = 100,
81
+ path: "Path|None" = None,
82
+ url: "str|None" = None,
83
+ auth: str | None = None,
84
+ hook: str | None = None,
85
+ ) -> None:
86
+ """
87
+ Arguments:
88
+ headers:
89
+ Optional HTTP headers to be used when schema is downloaded from ``url``.
90
+ Argument is only needed when the schema is downloaded from a URL and there is need to pass example
91
+ authentication headers to the endpoint. The ``headers`` is not used in the API calls are made during
92
+ test execution.
93
+ max_examples:
94
+ Maximum number of examples to generate for each operation. Default is 5.
95
+ path:
96
+ Path to the OpenAPI schema file. Using either ``path`` or ``url`` is mandatory.
97
+ url:
98
+ URL where the OpenAPI schema can be downloaded.
99
+ auth:
100
+ Optional authentication class to be used passed for Schemathesis authentication when test cases are executed.
101
+ Argument is used to create Schemathesis
102
+ [dynamic token](https://schemathesis.readthedocs.io/en/stable/guides/auth/#dynamic-token-authentication)
103
+ authentication for the test cases. The dynamic token generation class should follow the Schemathesis
104
+ documentation. The only addition is the import and importing the class must follow the Robot Framework library
105
+ [import rules](https://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#specifying-library-to-import).
106
+ Example if importing with filename, filename much match to the class name. See example from
107
+ [README.md](https://github.com/aaltat/robotframework-schemathesis?tab=readme-ov-file##dynamic-token-authentication)
108
+ file
109
+ hook:
110
+ Optional Schemathesis [hook](https://schemathesis.readthedocs.io/en/stable/guides/extending/). The
111
+ argument can be used to specify Schemathesis hooks to modify the generated test cases. The hooks
112
+ implementation must follow the Schemathesis documentation:
113
+ [Hooks](https://schemathesis.readthedocs.io/en/stable/guides/extending/) importing follows same
114
+ rules as importing
115
+ [test libraries](https://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#specifying-library-to-import)
116
+ in Robot Framework. Multiple hooks can be specified by separating them with semicolon (;).
117
+
118
+
119
+ ``path`` and ``url`` are mutually exclusive, only one of them should be used to specify the OpenAPI schema location.
120
+
121
+ # Configuration File
122
+
123
+ SchemathesisLibrary automatically discovers and loads configuration from a ``schemathesis.toml`` file if present
124
+ in the project directory. The configuration file allows you to customize test data generation and other Schemathesis
125
+ settings without modifying the library initialization.
126
+
127
+ The library uses Schemathesis's
128
+ [configuration file discovery](https://schemathesis.readthedocs.io/en/stable/reference/configuration/)
129
+ mechanism to locate ``schemathesis.toml`` in the current directory or parent directories (stopping at .git folder
130
+ or filesystem root).
131
+
132
+ Configuration options that are automatically applied:
133
+
134
+ - ``max-examples``: If specified in ``[project.generation]``, overrides the ``max_examples`` parameter passed to the library
135
+ - ``mode``: Generation mode (``positive``, ``negative``, or ``all``) controls whether valid or invalid test data is generated
136
+
137
+ Example schemathesis.toml:
138
+ ```toml
139
+ [[project]]
140
+ title = "My API"
141
+
142
+ [project.generation]
143
+ mode = "positive" # Generate only valid test cases
144
+ max-examples = 5 # Generate 5 test cases per operation
145
+ ```
146
+
147
+ For complete configuration options, see schemathesis
148
+ [configuration](https://schemathesis.readthedocs.io/en/stable/reference/configuration/)
149
+ documentation.
150
+
151
+ If no configuration file is found, the library uses default values (POSITIVE mode and max_examples from
152
+ library initialization.
153
+ """
154
+ self.ROBOT_LIBRARY_LISTENER = self
155
+ SchemathesisReader.options = Options(
156
+ headers=headers,
157
+ max_examples=max_examples,
158
+ path=path,
159
+ url=url,
160
+ auth=auth,
161
+ hook=hook,
162
+ )
163
+ self.data_driver = DataDriver(reader_class=SchemathesisReader)
164
+ DynamicCore.__init__(self, [])
165
+
166
+ def _start_suite(self, data: TestSuite, result: ResultTestSuite) -> None:
167
+ self.data_driver._start_suite(data, result)
168
+
169
+ def _start_test(self, data: TestCase, result: ResultTestCase) -> None:
170
+ self.data_driver._start_test(data, result)
171
+
172
+ @keyword
173
+ def call_and_validate(
174
+ self,
175
+ case: Case,
176
+ *,
177
+ base_url: str | None = None,
178
+ headers: dict[str, Any] | None = None,
179
+ auth: tuple[str, str] | Any | None = None,
180
+ session: Session | None = None,
181
+ ) -> Response:
182
+ """Call and validate a Schemathesis case.
183
+
184
+ Example:
185
+ ```robotframework
186
+ *** Settings ***
187
+ Library SchemathesisLibrary
188
+ ... url=http://127.0.0.1/openapi.json
189
+ Test Template Wrapper
190
+
191
+ *** Test Cases ***
192
+ All Tests
193
+ Wrapper test_case_1 # Test is replaced by DataDriver with generated test cases
194
+
195
+ *** Keywords ***
196
+ Wrapper
197
+ ${response} = Call And Validate ${case}
198
+ ```
199
+
200
+ Arguments:
201
+ case:
202
+ The Schemathesis
203
+ [Case](https://schemathesis.readthedocs.io/en/stable/reference/python/#schemathesis.Case)
204
+ to be called and validated.
205
+ base_url:
206
+ Optional base URL to override the one defined in the schema. Need mostly when schema is read from
207
+ a file.
208
+ headers:
209
+ Optional HTTP headers to be added when calling the API endpoint.
210
+ auth:
211
+ Optional authentication to be used when calling the case. Argument can be any authentication
212
+ supported by [httpx authentication](https://www.python-httpx.org/advanced/authentication/).
213
+ Example a tuple containing username and password for basic authentication or an instance of
214
+ [Digest authentication](https://www.python-httpx.org/advanced/authentication/#digest-authentication)
215
+ session:
216
+ Optional request session to be used when calling the case. Argument can be used to pass
217
+ a pre-configured HTTP session and it must be an instance of
218
+ [requests.session.Session](https://requests.readthedocs.io/en/latest/user/advanced/#session-objects)
219
+ . If, example, session is created by using `RequestsLibrary`, then current session can be obtained
220
+ by following way from python code:
221
+ ```python
222
+ from robot.libraries.BuiltIn import BuiltIn
223
+
224
+ def get_request_session():
225
+ request_library = BuiltIn().get_library_instance("RequestsLibrary")
226
+ return request_library._cache.current
227
+ ```
228
+
229
+ Then this can be used in test case like:
230
+ ```robotframework
231
+ *** Settings ***
232
+ Library RequestsLibrary
233
+ Library SchemathesisLibrary
234
+ ... url=http://127.0.0.1/openapi.json
235
+ Library ${CURDIR}/request_session.py
236
+ Test Template Wrapper
237
+
238
+ *** Test Cases ***
239
+ All Tests
240
+ Wrapper test_case_1
241
+
242
+ *** Keywords ***
243
+ Wrapper
244
+ [Arguments] ${case}
245
+ Create Session
246
+ ... session http://127.0.0.1/
247
+ ${session} = Get Request Session
248
+ Call And Validate
249
+ ... ${case}
250
+ ... headers=&{BASIC_AUTH_HEADERS}
251
+ ... session=${session}
252
+ ```
253
+
254
+ Returns:
255
+ [Schemathesis Response](https://schemathesis.readthedocs.io/en/stable/reference/python/#schemathesis.Response)
256
+ object. Can be used to further validate the response, example check specific headers or response
257
+ body.
258
+ """
259
+ logger.info(f"auth: {auth} {type(auth)}")
260
+ headers = self._dot_dict_to_dict(headers) if headers else None
261
+ self.info(f"Case: {case.path} | {case.method} | {case.path_parameters}")
262
+ self._log_case(case, headers)
263
+ if session:
264
+ self.info("Using provided session for the request.")
265
+ response = case.call_and_validate(base_url=base_url, headers=headers, auth=auth, session=session)
266
+ self._log_request(response)
267
+ self.debug(f"Response: {response.headers} | {response.status_code} | {response.text}")
268
+ return response
269
+
270
+ @keyword
271
+ def call(
272
+ self,
273
+ case: Case,
274
+ *,
275
+ base_url: str | None = None,
276
+ headers: dict[str, Any] | None = None,
277
+ auth: tuple[str, str] | Any | None = None,
278
+ session: Session | None = None,
279
+ ) -> Response:
280
+ """Call a Schemathesis case without validation.
281
+
282
+ The `Call` and `Validate Response` keywords can be used together
283
+ to call a case and validate the response.
284
+
285
+ ```robotframework
286
+ *** Settings ***
287
+ Library SchemathesisLibrary
288
+ ... url=http://127.0.0.1/openapi.json
289
+ Test Template Wrapper
290
+
291
+ *** Test Cases ***
292
+ All Tests
293
+ Wrapper test_case_1 # Test is replaced by DataDriver with generated test cases
294
+
295
+ *** Keywords ***
296
+ Wrapper
297
+ ${response} = Call ${case}
298
+ Validate Response
299
+ ... ${case} ${response}
300
+ ```
301
+
302
+ Arguments:
303
+ case:
304
+ The Schemathesis
305
+ [Case](https://schemathesis.readthedocs.io/en/stable/reference/python/#schemathesis.Case|Case)
306
+ to be called and validated.
307
+ base_url:
308
+ Optional base URL to override the one defined in the schema. Need mostly when schema is read
309
+ from a file.
310
+ headers:
311
+ Optional HTTP headers to be added when calling the API endpoint.
312
+ auth:
313
+ Optional authentication to be used when calling the case. Argument can be any authentication
314
+ supported by [httpx authentication](https://www.python-httpx.org/advanced/authentication/).
315
+ Example a tuple containing username and password for basic authentication or an instance of
316
+ [Digest authentication](https://www.python-httpx.org/advanced/authentication/#digest-authentication)
317
+ session:
318
+ Optional request session to be used when calling the case. See [Call And Validate] keyword
319
+ for example of using ``session`` argument.
320
+
321
+ Returns:
322
+ [Schemathesis Response](https://schemathesis.readthedocs.io/en/stable/reference/python/#schemathesis.Response)
323
+ object. Can be used to further validate the response, example check specific headers
324
+ or response body.
325
+ """
326
+ headers = self._dot_dict_to_dict(headers) if headers else None
327
+ self.info(f"Calling case: {case.path} | {case.method} | {case.path_parameters}")
328
+ self._log_case(case)
329
+ if session:
330
+ self.info("Using provided session for the request.")
331
+ response = case.call(base_url=base_url, headers=headers, auth=auth, session=session)
332
+ self._log_request(response)
333
+ return response
334
+
335
+ @keyword
336
+ def validate_response(
337
+ self,
338
+ case: Case,
339
+ response: Response,
340
+ headers: dict[str, Any] | None = None,
341
+ auth: tuple[str, str] | Any | None = None,
342
+ ) -> None:
343
+ """Validate a Schemathesis response.
344
+
345
+ The response is validated against the case's expectations.
346
+ The [Call] and [Validate Response] keywords can be used together
347
+ to call a case and validate the response. See the example from
348
+ [Call] keyword documentation.
349
+
350
+ Arguments:
351
+ case:
352
+ The Schemathesis case to be validated against.
353
+ response:
354
+ The Schemathesis response to be validated. Returned by `Call` keyword.
355
+ headers:
356
+ Optional HTTP headers to be added when calling the API endpoint.
357
+ auth:
358
+ Optional authentication to be used when calling the case. Argument can be any authentication
359
+ supported by [httpx authentication](https://www.python-httpx.org/advanced/authentication/).
360
+ Example a tuple containing username and password for basic authentication or an instance of
361
+ [Digest authentication](https://www.python-httpx.org/advanced/authentication/#digest-authentication)
362
+ """
363
+ self.info(f"Validating response: {response.status_code} | {response.headers}")
364
+ transport_kwargs: dict[str, Any] = {}
365
+ if auth:
366
+ transport_kwargs["auth"] = auth
367
+ if headers:
368
+ transport_kwargs["headers"] = headers
369
+ case.validate_response(response=response, transport_kwargs=transport_kwargs)
370
+ self.info("Response validation passed.")
371
+
372
+ @keyword(name="As cURL")
373
+ def as_curl(self, case: Case) -> str:
374
+ """Convert a Schemathesis case to a cURL command.
375
+
376
+ Example:
377
+ ```robotframework
378
+ *** Settings ***
379
+ Library SchemathesisLibrary
380
+ ... url=http://127.0.0.1/openapi.json
381
+ Test Template Wrapper
382
+
383
+ *** Test Cases ***
384
+ All Tests
385
+ Wrapper test_case_1 # Test is replaced by DataDriver with generated test cases
386
+
387
+ *** Keywords ***
388
+ Wrapper
389
+ ${curl_command} = `As cURL` ${case}
390
+ Log ${curl_command}
391
+ ```
392
+
393
+ Arguments:
394
+ case: The Schemathesis case to be converted.
395
+
396
+ Returns:
397
+ Returns a string containing the cURL command that can be used to execute the same request as the case.
398
+ """
399
+ self.info(f"Converting case to cURL: {case.path} | {case.method} | {case.path_parameters}")
400
+ self._log_case(case)
401
+ curl_command = case.as_curl_command()
402
+ self.debug(f"Generated cURL command: {curl_command}")
403
+ return curl_command
404
+
405
+ def info(self, message: str) -> None:
406
+ logger.info(message)
407
+
408
+ def debug(self, message: str) -> None:
409
+ logger.debug(message)
410
+
411
+ def _log_case(
412
+ self,
413
+ case: Case,
414
+ headers: "dict[str, Any]|None" = None,
415
+ ) -> None:
416
+ body = case.body if not isinstance(case.body, NotSet) else "Not set"
417
+ case_headers = headers or case.headers
418
+ self.debug(
419
+ f"Case headers {case_headers!r} body {body!r} "
420
+ f"cookies {case.cookies!r} path parameters {case.path_parameters!r}"
421
+ )
422
+
423
+ def _log_request(self, resposen: Response) -> None:
424
+ self.debug(
425
+ f"Request: {resposen.request.method} {resposen.request.url} "
426
+ f"headers: {resposen.request.headers!r} body: {resposen.request.body!r}"
427
+ )
428
+
429
+ def _dot_dict_to_dict(self, dot_dict: dict[str, Any]) -> dict[str, Any]:
430
+ if isinstance(dot_dict, DotDict):
431
+ return dict(dot_dict)
432
+ return dot_dict
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: robotframework-schemathesislibrary
3
- Version: 2.3.3
3
+ Version: 2.5.0
4
4
  Summary: Robot Framework SchemathesisLibrary to automatically create test cases from API specifications.
5
5
  Author-email: Tatu Aalto <aalto.tatu@gmail.com>
6
6
  License-Expression: Apache-2.0
@@ -10,8 +10,8 @@ Description-Content-Type: text/markdown
10
10
  License-File: LICENSE
11
11
  Requires-Dist: robotframework>=7.2.2
12
12
  Requires-Dist: robotframework-datadriver>=1.11.2
13
- Requires-Dist: robotframework-pythonlibcore>=4.5.0
14
- Requires-Dist: schemathesis>=4.18.0
13
+ Requires-Dist: robotframework-pythonlibcore>=4.6.0
14
+ Requires-Dist: schemathesis>=4.24.3
15
15
  Dynamic: license-file
16
16
 
17
17
  # Robot Framework SchemathesisLibrary
@@ -175,3 +175,26 @@ def filter_query(ctx, query) -> bool:
175
175
  return True
176
176
  ```
177
177
  Then only two test with `PUT` request are generated.
178
+
179
+ ## Pabot Support
180
+
181
+ [Pabot](https://pabot.org/) is a parallel executor for Robot Framework tests, and
182
+ `SchemathesisLibrary` supports it.
183
+
184
+ However, there's a crucial point to consider. Because `SchemathesisLibrary` uses
185
+ [DataDriver](https://github.com/Snooz82/robotframework-datadriver) to generate
186
+ tests dynamically, you cannot use Schemathesis's built-in parallel execution
187
+ feature (`--workers` option).
188
+
189
+ When using Pabot, you are responsible for organizing your test suites to avoid
190
+ generating the same tests in multiple parallel processes. A good strategy might be to
191
+ split your test into different suites, where each suite targets a specific scenario.
192
+
193
+ For example, you could create separate suites for:
194
+ - A user that is not found in the system.
195
+ - A user that is found and has valid credentials.
196
+ - A user that is found but has invalid credentials.
197
+
198
+ This approach ensures that each Pabot process works on a unique set of tests. Since
199
+ every API and system is different, a universal rule for structuring suites is not
200
+ possible, but this scenario-based division might be good starting point.
@@ -0,0 +1,4 @@
1
+ robotframework>=7.2.2
2
+ robotframework-datadriver>=1.11.2
3
+ robotframework-pythonlibcore>=4.6.0
4
+ schemathesis>=4.24.3
@@ -1,370 +0,0 @@
1
- # Copyright 2025- Tatu Aalto
2
- #
3
- # Licensed under the Apache License, Version 2.0 (the "License");
4
- # you may not use this file except in compliance with the License.
5
- # You may obtain a copy of the License at
6
- #
7
- # http://www.apache.org/licenses/LICENSE-2.0
8
- #
9
- # Unless required by applicable law or agreed to in writing, software
10
- # distributed under the License is distributed on an "AS IS" BASIS,
11
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
- # See the License for the specific language governing permissions and
13
- # limitations under the License.
14
- from typing import TYPE_CHECKING, Any
15
-
16
- from DataDriver import DataDriver # type: ignore
17
- from requests.sessions import Session
18
- from robot.api import logger
19
- from robot.api.deco import keyword
20
- from robot.result.model import TestCase as ResultTestCase # type: ignore
21
- from robot.result.model import TestSuite as ResultTestSuite # type: ignore
22
- from robot.running.model import TestCase, TestSuite # type: ignore
23
- from robot.utils.dotdict import DotDict # type: ignore
24
- from robotlibcore import DynamicCore # type: ignore
25
- from schemathesis import Case
26
- from schemathesis.core import NotSet
27
- from schemathesis.core.transport import Response
28
-
29
- from .schemathesisreader import Options, SchemathesisReader
30
-
31
- if TYPE_CHECKING:
32
- from pathlib import Path
33
-
34
- __version__ = "2.3.3"
35
-
36
-
37
- class SchemathesisLibrary(DynamicCore):
38
- """SchemathesisLibrary is a library for validating API schema using Schemathesis.
39
-
40
- %TOC%
41
-
42
- Example usage of the library and the `Call And Validate` keyword
43
-
44
- Library must be imported with the ``url`` or ``path`` argument to specify the
45
- OpenAPI schema. The library uses
46
- [https://github.com/Snooz82/robotframework-datadriver|DataDriver] to generate
47
- test cases from the OpenAPI schema by using
48
- [https://github.com/schemathesis/schemathesis|Schemathesis]. The library
49
- creates test cases that takes one argument, ``${case}``, which is a
50
- Schemathesis
51
- [https://schemathesis.readthedocs.io/en/stable/reference/python/#schemathesis.Case|Case]
52
- object. The `Call And Validate` keyword can be used to call and validate
53
- the case. The keyword will log the request and response details.
54
-
55
- Example:
56
- | *** Settings ***
57
- | Library SchemathesisLibrary url=http://127.0.0.1/openapi.json
58
- |
59
- | Test Template Wrapper
60
- |
61
- | *** Test Cases ***
62
- | All Tests # This test is deleted by DataDriver
63
- | Wrapper test_case_1
64
- |
65
- | *** Keywords ***
66
- | Wrapper
67
- | [Arguments] ${case}
68
- | Call And Validate ${case}
69
- """
70
-
71
- ROBOT_LIBRARY_VERSION = __version__
72
- ROBOT_LISTENER_API_VERSION = 3
73
- ROBOT_LIBRARY_SCOPE = "TEST SUITE"
74
-
75
- def __init__( # noqa: PLR0913
76
- self,
77
- *,
78
- headers: "dict[str, Any]|None" = None,
79
- max_examples: int = 100,
80
- path: "Path|None" = None,
81
- url: "str|None" = None,
82
- auth: str | None = None,
83
- hook: str | None = None,
84
- ) -> None:
85
- """The SchemathesisLibrary can be initialized with the following arguments:
86
-
87
- | =Argument= | =Description= |
88
- | ``headers`` | Optional HTTP headers to be used when schema is downloaded from ``url``. |
89
- | ``max_examples`` | Maximum number of examples to generate for each operation. Default is 5. |
90
- | ``path`` | Path to the OpenAPI schema file. Using either ``path`` or ``url`` is mandatory. |
91
- | ``url`` | URL where the OpenAPI schema can be downloaded. |
92
- | ``auth`` | Optional authentication class to be used passed for Schemathesis authentication when test cases are executed. |
93
- | ``hook`` | Optional Schemathesis hook https://schemathesis.readthedocs.io/en/stable/guides/extending/. |
94
-
95
- The ``headers`` argument is only needed when the schema is downloaded from a URL and there is need to pass example
96
- authentication headers to the endpoint. The ``headers`` is not used in the API calls are made during test execution.
97
-
98
- ``path`` and ``url`` are mutually exclusive, only one of them should be used to specify the OpenAPI schema location.
99
-
100
- ``auth`` can be used create Schemathesis
101
- [https://schemathesis.readthedocs.io/en/stable/guides/auth/#dynamic-token-authentication|dynamic token]
102
- authentication for the test cases. The dynamic token generation class should
103
- follow the Schemathesis documentation. The only addition is the import and importing
104
- the class must follow the Robot Framework library
105
- [https://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#specifying-library-to-import|import rules]
106
- . Example if importing with filename, filename much match to the class name.
107
- See example from
108
- [https://github.com/aaltat/robotframework-schemathesis?tab=readme-ov-file##dynamic-token-authentication|README.md]
109
- file
110
-
111
- The ``hook`` argument can be used to specify Schemathesis hooks to modify the generated test cases. The hooks
112
- implementation must follow the Schemathesis documentation:
113
- https://schemathesis.readthedocs.io/en/stable/guides/extending/ Hooks importing follows same rules as importing
114
- [https://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#specifying-library-to-import|test libraries]
115
- in Robot Framework. Multiple hooks can be specified by separating them with semicolon (;).
116
-
117
- = Configuration File =
118
-
119
- SchemathesisLibrary automatically discovers and loads configuration from a ``schemathesis.toml`` file if present
120
- in the project directory. The configuration file allows you to customize test data generation and other Schemathesis
121
- settings without modifying the library initialization.
122
-
123
- The library uses Schemathesis's
124
- [https://schemathesis.readthedocs.io/en/stable/reference/configuration/|configuration file discovery]
125
- mechanism to locate ``schemathesis.toml`` in the current directory or parent directories (stopping at .git folder
126
- or filesystem root).
127
-
128
- Configuration options that are automatically applied:
129
-
130
- - ``max-examples``: If specified in ``[project.generation]``, overrides the ``max_examples`` parameter passed to the library
131
- - ``mode``: Generation mode (``positive``, ``negative``, or ``all``) controls whether valid or invalid test data is generated
132
-
133
- Example ``schemathesis.toml``:
134
- | [[project]]
135
- | title = "My API"
136
- |
137
- | [project.generation]
138
- | mode = "positive" # Generate only valid test cases
139
- | max-examples = 5 # Generate 5 test cases per operation
140
-
141
- For complete configuration options, see:
142
- https://schemathesis.readthedocs.io/en/stable/reference/configuration/
143
-
144
- If no configuration file is found, the library uses default values (POSITIVE mode and max_examples from library initialization).
145
- """
146
- self.ROBOT_LIBRARY_LISTENER = self
147
- SchemathesisReader.options = Options(
148
- headers=headers,
149
- max_examples=max_examples,
150
- path=path,
151
- url=url,
152
- auth=auth,
153
- hook=hook,
154
- )
155
- self.data_driver = DataDriver(reader_class=SchemathesisReader)
156
- DynamicCore.__init__(self, [])
157
-
158
- def _start_suite(self, data: TestSuite, result: ResultTestSuite) -> None:
159
- self.data_driver._start_suite(data, result)
160
-
161
- def _start_test(self, data: TestCase, result: ResultTestCase) -> None:
162
- self.data_driver._start_test(data, result)
163
-
164
- @keyword
165
- def call_and_validate(
166
- self,
167
- case: Case,
168
- *,
169
- base_url: str | None = None,
170
- headers: dict[str, Any] | None = None,
171
- auth: tuple[str, str] | Any | None = None,
172
- session: Session | None = None,
173
- ) -> Response:
174
- """Call and validate a Schemathesis case.
175
-
176
- | Argument | Description |
177
- | ``case`` | The Schemathesis [https://schemathesis.readthedocs.io/en/stable/reference/python/#schemathesis.Case|Case] to be called and validated. |
178
- | ``base_url`` | Optional base URL to override the one defined in the schema. Need mostly when schema is read from a file. |
179
- | ``headers`` | Optional HTTP headers to be added when calling the API endpoint. |
180
- | ``auth`` | Optional authentication to be used when calling the case. |
181
- | ``session`` | Optional request session to be used when calling the case. |
182
- ``auth`` can be any authentication supported by
183
- [https://www.python-httpx.org/advanced/authentication/|httpx authentication].
184
- Example a tuple containing username and password for basic authentication or
185
- and instance of
186
- [https://www.python-httpx.org/advanced/authentication/#digest-authentication|Digest authentication]
187
-
188
- ``session`` can be used to pass a pre-configured HTTP session and it must be an instance of
189
- [https://requests.readthedocs.io/en/latest/user/advanced/#session-objects|requests.session.Session].
190
- If, example, session is created by using `RequestsLibrary`, then current session can be obtained
191
- by following way from python code:
192
- | from robot.libraries.BuiltIn import BuiltIn
193
- |
194
- | def get_request_session():
195
- | request_library = BuiltIn().get_library_instance("RequestsLibrary")
196
- | return request_library._cache.current
197
-
198
- Then this can be used in test case like:
199
- | *** Settings ***
200
- | Library RequestsLibrary
201
- | Library SchemathesisLibrary
202
- | ... url=http://127.0.0.1/openapi.json
203
- | Library ${CURDIR}/request_session.py
204
- | Test Template Wrapper
205
- |
206
- | *** Test Cases ***
207
- | All Tests
208
- | Wrapper test_case_1
209
- |
210
- | *** Keywords ***
211
- | Wrapper
212
- | [Arguments] ${case}
213
- | Create Session session http://127.0.0.1/
214
- | ${session} = Get Request Session
215
- | Call And Validate ${case} headers=&{BASIC_AUTH_HEADERS} session=${session}
216
-
217
-
218
- Returns a
219
- [https://schemathesis.readthedocs.io/en/stable/reference/python/#schemathesis.Response|Schemathesis Response]
220
- object. Can be used to further validate the response, example check specific headers
221
- or response body.
222
-
223
- Example:
224
- | ${response} = `Call And Validate` ${case}
225
- | VAR ${body} ${response.json()}
226
- | Should Be True ${body} # Or whatever validation is needed by users
227
- """
228
- logger.info(f"auth: {auth} {type(auth)}")
229
- headers = self._dot_dict_to_dict(headers) if headers else None
230
- self.info(f"Case: {case.path} | {case.method} | {case.path_parameters}")
231
- self._log_case(case, headers)
232
- if session:
233
- self.info("Using provided session for the request.")
234
- response = case.call_and_validate(base_url=base_url, headers=headers, auth=auth, session=session)
235
- self._log_request(response)
236
- self.debug(f"Response: {response.headers} | {response.status_code} | {response.text}")
237
- return response
238
-
239
- @keyword
240
- def call(
241
- self,
242
- case: Case,
243
- *,
244
- base_url: str | None = None,
245
- headers: dict[str, Any] | None = None,
246
- auth: tuple[str, str] | Any | None = None,
247
- session: Session | None = None,
248
- ) -> Response:
249
- """Call a Schemathesis case without validation.
250
-
251
- | =Argument= | =Description= |
252
- | ``case`` | The Schemathesis [https://schemathesis.readthedocs.io/en/stable/reference/python/#schemathesis.Case|Case] to be called and validated. |
253
- | ``base_url`` | Optional base URL to override the one defined in the schema. Need mostly when schema is read from a file. |
254
- | ``headers`` | Optional HTTP headers to be added when calling the API endpoint. |
255
- | ``auth`` | Optional authentication to be used when calling the case. |
256
- | ``session`` | Optional request session to be used when calling the case. |
257
-
258
- ``auth`` can be any authentication supported by
259
- [https://www.python-httpx.org/advanced/authentication/|httpx authentication].
260
- Example a tuple containing username and password for basic authentication or
261
- and instance of
262
- [https://www.python-httpx.org/advanced/authentication/#digest-authentication|Digest authentication]
263
-
264
- See `Call And Validate` keyword for example of using ``session`` argument.
265
-
266
- Returns a
267
- [https://schemathesis.readthedocs.io/en/stable/reference/python/#schemathesis.Response|Schemathesis Response]
268
- object. Can be used to further validate the response, example check specific headers
269
- or response body.
270
-
271
- The `Call` and `Validate Response` keywords can be used together
272
- to call a case and validate the response.
273
-
274
- Example:
275
- | ${response} = `Call` ${case}
276
- | `Validate Response` ${case} ${response}
277
-
278
- """
279
- headers = self._dot_dict_to_dict(headers) if headers else None
280
- self.info(f"Calling case: {case.path} | {case.method} | {case.path_parameters}")
281
- self._log_case(case)
282
- if session:
283
- self.info("Using provided session for the request.")
284
- response = case.call(base_url=base_url, headers=headers, auth=auth, session=session)
285
- self._log_request(response)
286
- return response
287
-
288
- @keyword
289
- def validate_response(
290
- self,
291
- case: Case,
292
- response: Response,
293
- headers: dict[str, Any] | None = None,
294
- auth: tuple[str, str] | Any | None = None,
295
- ) -> None:
296
- """Validate a Schemathesis response.
297
-
298
- | Argument | Description |
299
- | ``case`` | The Schemathesis case to be validated against. |
300
- | ``response`` | The Schemathesis response to be validated. Returned by ``Call`` keyword. |
301
- | ``headers`` | Optional HTTP headers to be added when calling the API endpoint. |
302
- | ``auth`` | Optional authentication to be used when calling the case. |
303
-
304
- ``auth`` can be any authentication supported by
305
- [https://www.python-httpx.org/advanced/authentication/|httpx authentication].
306
- Example a tuple containing username and password for basic authentication or
307
- and instance of
308
- [https://www.python-httpx.org/advanced/authentication/#digest-authentication|Digest authentication]
309
-
310
- The response is validated against the case's expectations.
311
- The `Call` and `Validate Response` keywords can be used together
312
- to call a case and validate the response. See the example from
313
- `Call` keyword documentation.
314
- """
315
- self.info(f"Validating response: {response.status_code} | {response.headers}")
316
- transport_kwargs: dict[str, Any] = {}
317
- if auth:
318
- transport_kwargs["auth"] = auth
319
- if headers:
320
- transport_kwargs["headers"] = headers
321
- case.validate_response(response=response, transport_kwargs=transport_kwargs)
322
- self.info("Response validation passed.")
323
-
324
- @keyword(name="As cURL")
325
- def as_curl(self, case: Case) -> str:
326
- """Convert a Schemathesis case to a cURL command.
327
-
328
- | Argument | Description |
329
- | ``case`` | The Schemathesis case to be converted. |
330
-
331
- Returns a string containing the cURL command that can be used to execute the same request as the case.
332
-
333
- Example:
334
- | ${curl_command} = `As cURL` ${case}
335
- | Log ${curl_command}
336
- """
337
- self.info(f"Converting case to cURL: {case.path} | {case.method} | {case.path_parameters}")
338
- self._log_case(case)
339
- curl_command = case.as_curl_command()
340
- self.debug(f"Generated cURL command: {curl_command}")
341
- return curl_command
342
-
343
- def info(self, message: str) -> None:
344
- logger.info(message)
345
-
346
- def debug(self, message: str) -> None:
347
- logger.debug(message)
348
-
349
- def _log_case(
350
- self,
351
- case: Case,
352
- headers: "dict[str, Any]|None" = None,
353
- ) -> None:
354
- body = case.body if not isinstance(case.body, NotSet) else "Not set"
355
- case_headers = headers or case.headers
356
- self.debug(
357
- f"Case headers {case_headers!r} body {body!r} "
358
- f"cookies {case.cookies!r} path parameters {case.path_parameters!r}"
359
- )
360
-
361
- def _log_request(self, resposen: Response) -> None:
362
- self.debug(
363
- f"Request: {resposen.request.method} {resposen.request.url} "
364
- f"headers: {resposen.request.headers!r} body: {resposen.request.body!r}"
365
- )
366
-
367
- def _dot_dict_to_dict(self, dot_dict: dict[str, Any]) -> dict[str, Any]:
368
- if isinstance(dot_dict, DotDict):
369
- return dict(dot_dict)
370
- return dot_dict
@@ -1,4 +0,0 @@
1
- robotframework>=7.2.2
2
- robotframework-datadriver>=1.11.2
3
- robotframework-pythonlibcore>=4.5.0
4
- schemathesis>=4.18.0