specmatic 0.2.3__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.
Potentially problematic release.
This version of specmatic might be problematic. Click here for more details.
- specmatic-0.2.3/MANIFEST.in +2 -0
- specmatic-0.2.3/PKG-INFO +66 -0
- specmatic-0.2.3/README.md +57 -0
- specmatic-0.2.3/setup.cfg +4 -0
- specmatic-0.2.3/setup.py +53 -0
- specmatic-0.2.3/specmatic/__init__.py +0 -0
- specmatic-0.2.3/specmatic/core/__init__.py +0 -0
- specmatic-0.2.3/specmatic/core/decorators.py +61 -0
- specmatic-0.2.3/specmatic/core/specmatic.jar +0 -0
- specmatic-0.2.3/specmatic/core/specmatic.py +49 -0
- specmatic-0.2.3/specmatic/core/specmatic_stub.py +89 -0
- specmatic-0.2.3/specmatic/core/specmatic_test.py +47 -0
- specmatic-0.2.3/specmatic/generators/__init__.py +0 -0
- specmatic-0.2.3/specmatic/generators/pytest_generator.py +28 -0
- specmatic-0.2.3/specmatic/generators/test_generator_base.py +15 -0
- specmatic-0.2.3/specmatic/generators/unittest_generator.py +26 -0
- specmatic-0.2.3/specmatic/server/__init__.py +0 -0
- specmatic-0.2.3/specmatic/server/flask_server.py +20 -0
- specmatic-0.2.3/specmatic/server/server_thread.py +20 -0
- specmatic-0.2.3/specmatic/utils.py +14 -0
- specmatic-0.2.3/specmatic/version.py +2 -0
- specmatic-0.2.3/specmatic.egg-info/PKG-INFO +66 -0
- specmatic-0.2.3/specmatic.egg-info/SOURCES.txt +28 -0
- specmatic-0.2.3/specmatic.egg-info/dependency_links.txt +1 -0
- specmatic-0.2.3/specmatic.egg-info/requires.txt +11 -0
- specmatic-0.2.3/specmatic.egg-info/top_level.txt +2 -0
- specmatic-0.2.3/test/__init__.py +0 -0
- specmatic-0.2.3/test/test_contract_with_pytest.py +16 -0
- specmatic-0.2.3/test/test_contract_with_stub_pytest.py +24 -0
- specmatic-0.2.3/test/test_contract_with_unittest.py +15 -0
specmatic-0.2.3/PKG-INFO
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: specmatic
|
|
3
|
+
Version: 0.2.3
|
|
4
|
+
Summary: A Python module for using the Specmatic Library.
|
|
5
|
+
Home-page: https://github.com/znsio/specmatic-python-extensions
|
|
6
|
+
Author: Specmatic Builders
|
|
7
|
+
Author-email: info@core.in
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
|
|
10
|
+
This is a Python library to run the [Specmatic](https://specmatic.in) library.
|
|
11
|
+
Specmatic is a contract driven development tool that allows us to turn OpenAPI contracts into executable specifications.
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
- The specmatic python library is geared towards integrating Specmatic capabilities in PyTest tests.
|
|
15
|
+
It provides three main function:
|
|
16
|
+
- The ability to start and stop a python flask app in a test class
|
|
17
|
+
- The ability to run specmatic in test mode against a local contract/spec file, or against a spec defined in a Central Contract Repository (as defined in specmatic.json).
|
|
18
|
+
- The ability to stub out an api dependency using the specmatic stub feature.
|
|
19
|
+
|
|
20
|
+
**NOTE**:
|
|
21
|
+
**If you are using an IDE like PyCharm to run tests, please edit the test configuration and set the working directory to your project root directory.**
|
|
22
|
+
|
|
23
|
+
- To run Specmatic in test mode against a flask app, add the **start_flask_app** and **specmatic_contract_test** decorator functions on your test class:
|
|
24
|
+
``````
|
|
25
|
+
@specmatic_contract_test(host, port)
|
|
26
|
+
@start_flask_app(app, host, port)
|
|
27
|
+
class TestApiContract:
|
|
28
|
+
@classmethod
|
|
29
|
+
def teardown_class(cls):
|
|
30
|
+
cls.flask_server.stop()
|
|
31
|
+
``````
|
|
32
|
+
|
|
33
|
+
- The **@start_flask_app** decorator adds an attribute 'flask_server' on the test class, which can be used in the
|
|
34
|
+
teardown method to stop the flask app.
|
|
35
|
+
- The **@start_flask_app** accepts an instance of your flask app and the host/port to run it on.
|
|
36
|
+
- The **@specmatic_contract_test** accepts host/port of your flask app and the path to the specmatic.json file in your project.
|
|
37
|
+
- Specmatic will look for a specmatic.json file in your project root directory.
|
|
38
|
+
If you don't wish to use specmatic.json, you can also pass the path to the actual contract/specification file to the specmatic_contract_test decorator.
|
|
39
|
+
- You can run this test class either from your IDE or from command line (from your project root directory) :
|
|
40
|
+
``````pytest test -v -s``````
|
|
41
|
+
[Click here](https://specmatic.in/documentation/contract_tests.html) to learn more about Specmatic test mode.
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
- If you want to stub out a service dependency, add the **specmatic_stub** decorator on top of the **start_flask_app**.
|
|
46
|
+
``````
|
|
47
|
+
@specmatic_contract_test(host, port)
|
|
48
|
+
@specmatic_stub(stub_host, stub_port, [expectation_json_file])
|
|
49
|
+
@start_flask_app(app, host, port)
|
|
50
|
+
class TestApiContract:
|
|
51
|
+
@classmethod
|
|
52
|
+
def teardown_class(cls):
|
|
53
|
+
cls.flask_server.stop()
|
|
54
|
+
cls.stub.stop()
|
|
55
|
+
``````
|
|
56
|
+
- The **specmatic_stub** decorator adds an attribute 'stub' on the test class, which can be used in the
|
|
57
|
+
teardown method to stop the stub process.
|
|
58
|
+
- The **@specmatic_stub** accepts host/port on which the dependency is expected to be running.
|
|
59
|
+
- It also accepts a list of expectation json file paths which can be used to return stubbed responses.
|
|
60
|
+
- The Specmatic stub will look for a specmatic.json file in your project root directory.
|
|
61
|
+
If you don't wish to use specmatic.json, you can also pass the path to the actual contract/specification file to the specmatic_stub decorator.
|
|
62
|
+
- Run this test class either from your IDE or from command line (from your project root directory) :
|
|
63
|
+
``````pytest test -v -s``````
|
|
64
|
+
[Click here](https://specmatic.in/documentation/service_virtualization_tutorial.html) to learn more about Specmatic stub mode and using expectation json files.
|
|
65
|
+
|
|
66
|
+
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
This is a Python library to run the [Specmatic](https://specmatic.in) library.
|
|
2
|
+
Specmatic is a contract driven development tool that allows us to turn OpenAPI contracts into executable specifications.
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
- The specmatic python library is geared towards integrating Specmatic capabilities in PyTest tests.
|
|
6
|
+
It provides three main function:
|
|
7
|
+
- The ability to start and stop a python flask app in a test class
|
|
8
|
+
- The ability to run specmatic in test mode against a local contract/spec file, or against a spec defined in a Central Contract Repository (as defined in specmatic.json).
|
|
9
|
+
- The ability to stub out an api dependency using the specmatic stub feature.
|
|
10
|
+
|
|
11
|
+
**NOTE**:
|
|
12
|
+
**If you are using an IDE like PyCharm to run tests, please edit the test configuration and set the working directory to your project root directory.**
|
|
13
|
+
|
|
14
|
+
- To run Specmatic in test mode against a flask app, add the **start_flask_app** and **specmatic_contract_test** decorator functions on your test class:
|
|
15
|
+
``````
|
|
16
|
+
@specmatic_contract_test(host, port)
|
|
17
|
+
@start_flask_app(app, host, port)
|
|
18
|
+
class TestApiContract:
|
|
19
|
+
@classmethod
|
|
20
|
+
def teardown_class(cls):
|
|
21
|
+
cls.flask_server.stop()
|
|
22
|
+
``````
|
|
23
|
+
|
|
24
|
+
- The **@start_flask_app** decorator adds an attribute 'flask_server' on the test class, which can be used in the
|
|
25
|
+
teardown method to stop the flask app.
|
|
26
|
+
- The **@start_flask_app** accepts an instance of your flask app and the host/port to run it on.
|
|
27
|
+
- The **@specmatic_contract_test** accepts host/port of your flask app and the path to the specmatic.json file in your project.
|
|
28
|
+
- Specmatic will look for a specmatic.json file in your project root directory.
|
|
29
|
+
If you don't wish to use specmatic.json, you can also pass the path to the actual contract/specification file to the specmatic_contract_test decorator.
|
|
30
|
+
- You can run this test class either from your IDE or from command line (from your project root directory) :
|
|
31
|
+
``````pytest test -v -s``````
|
|
32
|
+
[Click here](https://specmatic.in/documentation/contract_tests.html) to learn more about Specmatic test mode.
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
- If you want to stub out a service dependency, add the **specmatic_stub** decorator on top of the **start_flask_app**.
|
|
37
|
+
``````
|
|
38
|
+
@specmatic_contract_test(host, port)
|
|
39
|
+
@specmatic_stub(stub_host, stub_port, [expectation_json_file])
|
|
40
|
+
@start_flask_app(app, host, port)
|
|
41
|
+
class TestApiContract:
|
|
42
|
+
@classmethod
|
|
43
|
+
def teardown_class(cls):
|
|
44
|
+
cls.flask_server.stop()
|
|
45
|
+
cls.stub.stop()
|
|
46
|
+
``````
|
|
47
|
+
- The **specmatic_stub** decorator adds an attribute 'stub' on the test class, which can be used in the
|
|
48
|
+
teardown method to stop the stub process.
|
|
49
|
+
- The **@specmatic_stub** accepts host/port on which the dependency is expected to be running.
|
|
50
|
+
- It also accepts a list of expectation json file paths which can be used to return stubbed responses.
|
|
51
|
+
- The Specmatic stub will look for a specmatic.json file in your project root directory.
|
|
52
|
+
If you don't wish to use specmatic.json, you can also pass the path to the actual contract/specification file to the specmatic_stub decorator.
|
|
53
|
+
- Run this test class either from your IDE or from command line (from your project root directory) :
|
|
54
|
+
``````pytest test -v -s``````
|
|
55
|
+
[Click here](https://specmatic.in/documentation/service_virtualization_tutorial.html) to learn more about Specmatic stub mode and using expectation json files.
|
|
56
|
+
|
|
57
|
+
|
specmatic-0.2.3/setup.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from setuptools import setup, find_packages
|
|
5
|
+
import urllib.request
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def download_specmatic_jar(version):
|
|
9
|
+
file_url = f'https://github.com/znsio/specmatic/releases/download/{version}/specmatic.jar'
|
|
10
|
+
file_path = 'specmatic/core/specmatic.jar'
|
|
11
|
+
print(f"Downloading core jar from: {file_url}")
|
|
12
|
+
urllib.request.urlretrieve(file_url, file_path)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_version():
|
|
16
|
+
version = {}
|
|
17
|
+
with open(os.path.join('specmatic', 'version.py')) as file:
|
|
18
|
+
exec(file.read(), version)
|
|
19
|
+
return version
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
version = get_version()
|
|
23
|
+
|
|
24
|
+
download_specmatic_jar(version['__specmatic_version__'])
|
|
25
|
+
|
|
26
|
+
this_directory = Path(__file__).parent
|
|
27
|
+
long_description = (this_directory / "README.md").read_text()
|
|
28
|
+
|
|
29
|
+
setup(
|
|
30
|
+
name='specmatic',
|
|
31
|
+
version=version['__version__'],
|
|
32
|
+
description='A Python module for using the Specmatic Library.',
|
|
33
|
+
long_description=long_description,
|
|
34
|
+
long_description_content_type='text/markdown',
|
|
35
|
+
author='Specmatic Builders',
|
|
36
|
+
author_email='info@core.in',
|
|
37
|
+
url='https://github.com/znsio/specmatic-python-extensions',
|
|
38
|
+
packages=find_packages(),
|
|
39
|
+
include_package_data=True,
|
|
40
|
+
install_requires=[
|
|
41
|
+
'blinker==1.6.2',
|
|
42
|
+
'click==8.1.3',
|
|
43
|
+
'Flask==2.3.2',
|
|
44
|
+
'iniconfig==2.0.0',
|
|
45
|
+
'itsdangerous==2.1.2',
|
|
46
|
+
'Jinja2==3.1.2',
|
|
47
|
+
'MarkupSafe==2.1.2',
|
|
48
|
+
'packaging==23.1',
|
|
49
|
+
'pluggy==1.0.0',
|
|
50
|
+
'pytest==7.3.1',
|
|
51
|
+
'Werkzeug==2.3.3'
|
|
52
|
+
]
|
|
53
|
+
)
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from flask import Flask
|
|
2
|
+
from specmatic.server.flask_server import FlaskServer
|
|
3
|
+
from specmatic.core.specmatic import Specmatic
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def specmatic_stub(host: str, port: int, expectation_json_files=None, contract_file='', specmatic_json_file: str = ''):
|
|
7
|
+
if expectation_json_files is None:
|
|
8
|
+
expectation_json_files = []
|
|
9
|
+
|
|
10
|
+
def decorator(cls):
|
|
11
|
+
try:
|
|
12
|
+
stub = Specmatic() \
|
|
13
|
+
.stub(host, port) \
|
|
14
|
+
.with_specmatic_json_at(specmatic_json_file) \
|
|
15
|
+
.with_contract_file(contract_file) \
|
|
16
|
+
.build()
|
|
17
|
+
stub.start()
|
|
18
|
+
cls.stub = stub
|
|
19
|
+
stub.set_expectations(expectation_json_files)
|
|
20
|
+
except Exception as e:
|
|
21
|
+
if hasattr(cls, 'stub'):
|
|
22
|
+
cls.stub.stop()
|
|
23
|
+
print(f"Error: {e}")
|
|
24
|
+
raise e
|
|
25
|
+
return cls
|
|
26
|
+
|
|
27
|
+
return decorator
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def specmatic_contract_test(host: str, port: int, contract_file='', specmatic_json_file: str = ''):
|
|
31
|
+
def decorator(cls):
|
|
32
|
+
try:
|
|
33
|
+
Specmatic() \
|
|
34
|
+
.test(host, port) \
|
|
35
|
+
.with_specmatic_json_at(specmatic_json_file) \
|
|
36
|
+
.with_contract_file(contract_file) \
|
|
37
|
+
.configure_py_tests(cls)
|
|
38
|
+
return cls
|
|
39
|
+
except Exception as e:
|
|
40
|
+
if hasattr(cls, 'stub'):
|
|
41
|
+
cls.stub.stop()
|
|
42
|
+
print(f"Error: {e}")
|
|
43
|
+
raise e
|
|
44
|
+
|
|
45
|
+
return decorator
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def start_flask_app(app: Flask, host: str, port: int):
|
|
49
|
+
def decorator(cls):
|
|
50
|
+
try:
|
|
51
|
+
flask_server = FlaskServer(app, host, port)
|
|
52
|
+
flask_server.start()
|
|
53
|
+
cls.flask_server = flask_server
|
|
54
|
+
return cls
|
|
55
|
+
except Exception as e:
|
|
56
|
+
if hasattr(cls, 'stub'):
|
|
57
|
+
cls.stub.stop()
|
|
58
|
+
print(f"Error: {e}")
|
|
59
|
+
raise e
|
|
60
|
+
|
|
61
|
+
return decorator
|
|
Binary file
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from specmatic.generators.pytest_generator import PyTestGenerator
|
|
2
|
+
from specmatic.generators.unittest_generator import UnitTestGenerator
|
|
3
|
+
from specmatic.core.specmatic_test import SpecmaticTest
|
|
4
|
+
from specmatic.core.specmatic_stub import SpecmaticStub
|
|
5
|
+
from specmatic.utils import get_junit_report_file_path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Specmatic:
|
|
9
|
+
|
|
10
|
+
def __init__(self):
|
|
11
|
+
self.stub_server_port = None
|
|
12
|
+
self.stub_server_host = None
|
|
13
|
+
self.test_server_port = None
|
|
14
|
+
self.test_server_host = None
|
|
15
|
+
self.contract_file_path = ''
|
|
16
|
+
self.specmatic_json_file_path = ''
|
|
17
|
+
|
|
18
|
+
def test(self, test_server_host: str = "127.0.0.1", test_server_port: int = 5000):
|
|
19
|
+
self.test_server_host = test_server_host
|
|
20
|
+
self.test_server_port = test_server_port
|
|
21
|
+
return self
|
|
22
|
+
|
|
23
|
+
def with_contract_file(self, contract_file_path: str):
|
|
24
|
+
self.contract_file_path = contract_file_path
|
|
25
|
+
return self
|
|
26
|
+
|
|
27
|
+
def with_specmatic_json_at(self, specmatic_json_file_path: str):
|
|
28
|
+
self.specmatic_json_file_path = specmatic_json_file_path
|
|
29
|
+
return self
|
|
30
|
+
|
|
31
|
+
def configure_unit_tests(self, test_class):
|
|
32
|
+
SpecmaticTest(self.test_server_host, self.test_server_port, self.contract_file_path,
|
|
33
|
+
self.specmatic_json_file_path).run()
|
|
34
|
+
UnitTestGenerator(test_class, get_junit_report_file_path()).generate()
|
|
35
|
+
|
|
36
|
+
def configure_py_tests(self, test_class):
|
|
37
|
+
SpecmaticTest(self.test_server_host, self.test_server_port, self.contract_file_path,
|
|
38
|
+
self.specmatic_json_file_path).run()
|
|
39
|
+
PyTestGenerator(test_class, get_junit_report_file_path()).generate()
|
|
40
|
+
|
|
41
|
+
def stub(self, stub_server_host: str = '0.0.0.1',
|
|
42
|
+
stub_server_port: int = 9000):
|
|
43
|
+
self.stub_server_host = stub_server_host
|
|
44
|
+
self.stub_server_port = stub_server_port
|
|
45
|
+
return self
|
|
46
|
+
|
|
47
|
+
def build(self):
|
|
48
|
+
return SpecmaticStub(self.stub_server_host, self.stub_server_port, self.specmatic_json_file_path,
|
|
49
|
+
self.contract_file_path)
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import subprocess
|
|
4
|
+
import threading
|
|
5
|
+
import traceback
|
|
6
|
+
from queue import Queue
|
|
7
|
+
|
|
8
|
+
import requests
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SpecmaticStub:
|
|
12
|
+
|
|
13
|
+
def __init__(self, host: str, port: int, specmatic_json_file_path: str, contract_file_path: str):
|
|
14
|
+
self.stub_started_event = None
|
|
15
|
+
self.process = None
|
|
16
|
+
self.host = host
|
|
17
|
+
self.port = port
|
|
18
|
+
self.specmatic_json_file_path = specmatic_json_file_path
|
|
19
|
+
self.contract_file_path = contract_file_path
|
|
20
|
+
self.expectation_api = f'http://{self.host}:{self.port}/_specmatic/expectations'
|
|
21
|
+
self.stub_running_success_message = f'Stub server is running on http://{self.host}:{self.port}'
|
|
22
|
+
self.error_queue = Queue()
|
|
23
|
+
|
|
24
|
+
def start(self):
|
|
25
|
+
self.stub_started_event = threading.Event()
|
|
26
|
+
stub_command = self._create_stub_process_command()
|
|
27
|
+
print(f"\n Starting specmatic stub server on {self.host}:{self.port}")
|
|
28
|
+
self.process = subprocess.Popen(stub_command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
|
29
|
+
stdout_reader = threading.Thread(target=self.read_process_output, daemon=True)
|
|
30
|
+
stdout_reader.start()
|
|
31
|
+
|
|
32
|
+
def read_process_output(self):
|
|
33
|
+
def signal_event_if_stub_has_started(line):
|
|
34
|
+
if self.stub_running_success_message in line:
|
|
35
|
+
self.stub_started_event.set()
|
|
36
|
+
|
|
37
|
+
def read_and_print_output_line_by_line():
|
|
38
|
+
for line in iter(self.process.stdout.readline, ''):
|
|
39
|
+
if line:
|
|
40
|
+
line = line.decode().rstrip()
|
|
41
|
+
print(line)
|
|
42
|
+
signal_event_if_stub_has_started(line)
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
read_and_print_output_line_by_line()
|
|
46
|
+
except Exception:
|
|
47
|
+
tb = traceback.format_exc()
|
|
48
|
+
self.error_queue.put(tb)
|
|
49
|
+
self.stub_started_event.set()
|
|
50
|
+
|
|
51
|
+
def stop(self):
|
|
52
|
+
print(f"\n Shutting down specmatic stub server on {self.host}:{self.port}, please wait ...")
|
|
53
|
+
self.process.kill()
|
|
54
|
+
|
|
55
|
+
def set_expectations(self, file_paths: list[str]):
|
|
56
|
+
self.stub_started_event.wait()
|
|
57
|
+
if not self.error_queue.empty():
|
|
58
|
+
error = self.error_queue.get()
|
|
59
|
+
raise Exception(f"An exception occurred while reading the stub process output: {error}")
|
|
60
|
+
|
|
61
|
+
for file_path in file_paths:
|
|
62
|
+
with open(file_path, 'r') as file:
|
|
63
|
+
json_string = json.load(file)
|
|
64
|
+
headers = {
|
|
65
|
+
"Content-Type": "application/json"
|
|
66
|
+
}
|
|
67
|
+
response = requests.post(self.expectation_api, json=json_string, headers=headers)
|
|
68
|
+
if response.status_code != 200:
|
|
69
|
+
self.stop()
|
|
70
|
+
raise Exception(f"{response.content} received for expectation json file: {json_string}")
|
|
71
|
+
|
|
72
|
+
def _create_stub_process_command(self):
|
|
73
|
+
jar_path = os.path.dirname(os.path.realpath(__file__)) + "/specmatic.jar"
|
|
74
|
+
cmd = [
|
|
75
|
+
"java",
|
|
76
|
+
"-jar",
|
|
77
|
+
jar_path,
|
|
78
|
+
"stub"
|
|
79
|
+
]
|
|
80
|
+
if self.specmatic_json_file_path != '':
|
|
81
|
+
cmd.append("--config=" + self.specmatic_json_file_path)
|
|
82
|
+
else:
|
|
83
|
+
if self.contract_file_path != '':
|
|
84
|
+
cmd.append(self.contract_file_path)
|
|
85
|
+
cmd += [
|
|
86
|
+
'--host=' + self.host,
|
|
87
|
+
"--port=" + str(self.port)
|
|
88
|
+
]
|
|
89
|
+
return cmd
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import shutil
|
|
3
|
+
import subprocess
|
|
4
|
+
|
|
5
|
+
from specmatic.utils import get_junit_report_dir_path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SpecmaticTest:
|
|
9
|
+
def __init__(self, host: str = "127.0.0.1", port: int = 5000, contract_file_path: str = '',
|
|
10
|
+
specmatic_json_file_path: str = ''):
|
|
11
|
+
self.host = host
|
|
12
|
+
self.port = port
|
|
13
|
+
self.contract_file_path = contract_file_path
|
|
14
|
+
self.specmatic_json_file_path = specmatic_json_file_path
|
|
15
|
+
|
|
16
|
+
def _delete_existing_report_if_exists(self):
|
|
17
|
+
junit_report_dir_path = get_junit_report_dir_path()
|
|
18
|
+
if os.path.exists(junit_report_dir_path):
|
|
19
|
+
shutil.rmtree(junit_report_dir_path)
|
|
20
|
+
|
|
21
|
+
def _execute_specmatic(self):
|
|
22
|
+
jar_path = os.path.dirname(os.path.realpath(__file__)) + "/specmatic.jar"
|
|
23
|
+
cmd = [
|
|
24
|
+
"java",
|
|
25
|
+
"-jar",
|
|
26
|
+
jar_path,
|
|
27
|
+
"test"
|
|
28
|
+
]
|
|
29
|
+
if self.specmatic_json_file_path != '':
|
|
30
|
+
cmd.append("--config=" + self.specmatic_json_file_path)
|
|
31
|
+
else:
|
|
32
|
+
if self.contract_file_path != '':
|
|
33
|
+
cmd.append(self.contract_file_path)
|
|
34
|
+
|
|
35
|
+
cmd += [
|
|
36
|
+
"--junitReportDir=" + get_junit_report_dir_path(),
|
|
37
|
+
'--host=' + self.host,
|
|
38
|
+
"--port=" + str(self.port)
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
print(f"\n Running specmatic tests for api at {self.host}:{self.port}")
|
|
42
|
+
subprocess.run(cmd)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def run(self):
|
|
46
|
+
self._delete_existing_report_if_exists()
|
|
47
|
+
self._execute_specmatic()
|
|
File without changes
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from specmatic.generators.test_generator_base import TestGeneratorBase
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class PyTestGenerator(TestGeneratorBase):
|
|
7
|
+
|
|
8
|
+
def __init__(self, test_class, junit_report_path):
|
|
9
|
+
self.test_class = test_class
|
|
10
|
+
self.junit_report_path = junit_report_path
|
|
11
|
+
|
|
12
|
+
def generate(self):
|
|
13
|
+
self.generate_tests(self.junit_report_path, self.test_class, PyTestGenerator._generate_passing_test,
|
|
14
|
+
PyTestGenerator._generate_failing_test)
|
|
15
|
+
|
|
16
|
+
@staticmethod
|
|
17
|
+
def _generate_passing_test():
|
|
18
|
+
def test(self):
|
|
19
|
+
assert 1 == 1
|
|
20
|
+
|
|
21
|
+
return test
|
|
22
|
+
|
|
23
|
+
@staticmethod
|
|
24
|
+
def _generate_failing_test(error):
|
|
25
|
+
def test(self):
|
|
26
|
+
pytest.fail(error)
|
|
27
|
+
|
|
28
|
+
return test
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import xml.etree.ElementTree as ET
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class TestGeneratorBase:
|
|
5
|
+
@staticmethod
|
|
6
|
+
def generate_tests(junit_report_path, test_class, passing_test_fn, failing_test_fn):
|
|
7
|
+
root = ET.parse(junit_report_path).getroot()
|
|
8
|
+
for testcase in root.iter('testcase'):
|
|
9
|
+
scenario = testcase.find('system-out').text.split('display-name: ')[1]
|
|
10
|
+
test_name = "test_" + scenario
|
|
11
|
+
failure = testcase.find('failure')
|
|
12
|
+
if failure is None:
|
|
13
|
+
setattr(test_class, test_name, passing_test_fn())
|
|
14
|
+
else:
|
|
15
|
+
setattr(test_class, test_name, failing_test_fn(failure.get('message') + failure.text))
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from specmatic.generators.test_generator_base import TestGeneratorBase
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class UnitTestGenerator(TestGeneratorBase):
|
|
5
|
+
|
|
6
|
+
def __init__(self, test_class, junit_report_path):
|
|
7
|
+
self.test_class = test_class
|
|
8
|
+
self.junit_report_path = junit_report_path
|
|
9
|
+
|
|
10
|
+
@staticmethod
|
|
11
|
+
def _gen_passing_test():
|
|
12
|
+
def test(self):
|
|
13
|
+
self.assertTrue(1 == 1)
|
|
14
|
+
|
|
15
|
+
return test
|
|
16
|
+
|
|
17
|
+
@staticmethod
|
|
18
|
+
def _gen_failing_test(error):
|
|
19
|
+
def test(self):
|
|
20
|
+
self.fail(error)
|
|
21
|
+
|
|
22
|
+
return test
|
|
23
|
+
|
|
24
|
+
def generate(self):
|
|
25
|
+
self.generate_tests(self.junit_report_path, self.test_class, UnitTestGenerator._gen_passing_test,
|
|
26
|
+
UnitTestGenerator._gen_failing_test)
|
|
File without changes
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from flask import Flask
|
|
2
|
+
|
|
3
|
+
from specmatic.server.server_thread import ServerThread
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class FlaskServer:
|
|
7
|
+
server: ServerThread = None
|
|
8
|
+
|
|
9
|
+
def __init__(self, app: Flask, host: str, port: int):
|
|
10
|
+
self.app = app
|
|
11
|
+
self.host = host
|
|
12
|
+
self.port = port
|
|
13
|
+
|
|
14
|
+
def start(self):
|
|
15
|
+
self.server = ServerThread(self.app, self.host, self.port)
|
|
16
|
+
self.server.start()
|
|
17
|
+
print(f'Flask server started on {self.host}:{self.port}')
|
|
18
|
+
|
|
19
|
+
def stop(self):
|
|
20
|
+
self.server.shutdown()
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
|
|
3
|
+
from flask import Flask
|
|
4
|
+
from werkzeug.serving import make_server
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ServerThread(threading.Thread):
|
|
8
|
+
|
|
9
|
+
def __init__(self, app: Flask, host: str, port: int):
|
|
10
|
+
threading.Thread.__init__(self)
|
|
11
|
+
self.server = make_server(host, port, app)
|
|
12
|
+
self.ctx = app.app_context()
|
|
13
|
+
self.ctx.push()
|
|
14
|
+
|
|
15
|
+
def run(self):
|
|
16
|
+
print('Starting flask server...')
|
|
17
|
+
self.server.serve_forever()
|
|
18
|
+
|
|
19
|
+
def shutdown(self):
|
|
20
|
+
self.server.shutdown()
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def get_project_root() -> str:
|
|
6
|
+
return os.path.dirname(Path(__file__).parent)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_junit_report_dir_path() -> str:
|
|
10
|
+
return get_project_root() + "/junit_report"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_junit_report_file_path() -> str:
|
|
14
|
+
return get_junit_report_dir_path() + "/TEST-junit-jupiter.xml"
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: specmatic
|
|
3
|
+
Version: 0.2.3
|
|
4
|
+
Summary: A Python module for using the Specmatic Library.
|
|
5
|
+
Home-page: https://github.com/znsio/specmatic-python-extensions
|
|
6
|
+
Author: Specmatic Builders
|
|
7
|
+
Author-email: info@core.in
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
|
|
10
|
+
This is a Python library to run the [Specmatic](https://specmatic.in) library.
|
|
11
|
+
Specmatic is a contract driven development tool that allows us to turn OpenAPI contracts into executable specifications.
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
- The specmatic python library is geared towards integrating Specmatic capabilities in PyTest tests.
|
|
15
|
+
It provides three main function:
|
|
16
|
+
- The ability to start and stop a python flask app in a test class
|
|
17
|
+
- The ability to run specmatic in test mode against a local contract/spec file, or against a spec defined in a Central Contract Repository (as defined in specmatic.json).
|
|
18
|
+
- The ability to stub out an api dependency using the specmatic stub feature.
|
|
19
|
+
|
|
20
|
+
**NOTE**:
|
|
21
|
+
**If you are using an IDE like PyCharm to run tests, please edit the test configuration and set the working directory to your project root directory.**
|
|
22
|
+
|
|
23
|
+
- To run Specmatic in test mode against a flask app, add the **start_flask_app** and **specmatic_contract_test** decorator functions on your test class:
|
|
24
|
+
``````
|
|
25
|
+
@specmatic_contract_test(host, port)
|
|
26
|
+
@start_flask_app(app, host, port)
|
|
27
|
+
class TestApiContract:
|
|
28
|
+
@classmethod
|
|
29
|
+
def teardown_class(cls):
|
|
30
|
+
cls.flask_server.stop()
|
|
31
|
+
``````
|
|
32
|
+
|
|
33
|
+
- The **@start_flask_app** decorator adds an attribute 'flask_server' on the test class, which can be used in the
|
|
34
|
+
teardown method to stop the flask app.
|
|
35
|
+
- The **@start_flask_app** accepts an instance of your flask app and the host/port to run it on.
|
|
36
|
+
- The **@specmatic_contract_test** accepts host/port of your flask app and the path to the specmatic.json file in your project.
|
|
37
|
+
- Specmatic will look for a specmatic.json file in your project root directory.
|
|
38
|
+
If you don't wish to use specmatic.json, you can also pass the path to the actual contract/specification file to the specmatic_contract_test decorator.
|
|
39
|
+
- You can run this test class either from your IDE or from command line (from your project root directory) :
|
|
40
|
+
``````pytest test -v -s``````
|
|
41
|
+
[Click here](https://specmatic.in/documentation/contract_tests.html) to learn more about Specmatic test mode.
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
- If you want to stub out a service dependency, add the **specmatic_stub** decorator on top of the **start_flask_app**.
|
|
46
|
+
``````
|
|
47
|
+
@specmatic_contract_test(host, port)
|
|
48
|
+
@specmatic_stub(stub_host, stub_port, [expectation_json_file])
|
|
49
|
+
@start_flask_app(app, host, port)
|
|
50
|
+
class TestApiContract:
|
|
51
|
+
@classmethod
|
|
52
|
+
def teardown_class(cls):
|
|
53
|
+
cls.flask_server.stop()
|
|
54
|
+
cls.stub.stop()
|
|
55
|
+
``````
|
|
56
|
+
- The **specmatic_stub** decorator adds an attribute 'stub' on the test class, which can be used in the
|
|
57
|
+
teardown method to stop the stub process.
|
|
58
|
+
- The **@specmatic_stub** accepts host/port on which the dependency is expected to be running.
|
|
59
|
+
- It also accepts a list of expectation json file paths which can be used to return stubbed responses.
|
|
60
|
+
- The Specmatic stub will look for a specmatic.json file in your project root directory.
|
|
61
|
+
If you don't wish to use specmatic.json, you can also pass the path to the actual contract/specification file to the specmatic_stub decorator.
|
|
62
|
+
- Run this test class either from your IDE or from command line (from your project root directory) :
|
|
63
|
+
``````pytest test -v -s``````
|
|
64
|
+
[Click here](https://specmatic.in/documentation/service_virtualization_tutorial.html) to learn more about Specmatic stub mode and using expectation json files.
|
|
65
|
+
|
|
66
|
+
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
MANIFEST.in
|
|
2
|
+
README.md
|
|
3
|
+
setup.py
|
|
4
|
+
specmatic/__init__.py
|
|
5
|
+
specmatic/utils.py
|
|
6
|
+
specmatic/version.py
|
|
7
|
+
specmatic.egg-info/PKG-INFO
|
|
8
|
+
specmatic.egg-info/SOURCES.txt
|
|
9
|
+
specmatic.egg-info/dependency_links.txt
|
|
10
|
+
specmatic.egg-info/requires.txt
|
|
11
|
+
specmatic.egg-info/top_level.txt
|
|
12
|
+
specmatic/core/__init__.py
|
|
13
|
+
specmatic/core/decorators.py
|
|
14
|
+
specmatic/core/specmatic.jar
|
|
15
|
+
specmatic/core/specmatic.py
|
|
16
|
+
specmatic/core/specmatic_stub.py
|
|
17
|
+
specmatic/core/specmatic_test.py
|
|
18
|
+
specmatic/generators/__init__.py
|
|
19
|
+
specmatic/generators/pytest_generator.py
|
|
20
|
+
specmatic/generators/test_generator_base.py
|
|
21
|
+
specmatic/generators/unittest_generator.py
|
|
22
|
+
specmatic/server/__init__.py
|
|
23
|
+
specmatic/server/flask_server.py
|
|
24
|
+
specmatic/server/server_thread.py
|
|
25
|
+
test/__init__.py
|
|
26
|
+
test/test_contract_with_pytest.py
|
|
27
|
+
test/test_contract_with_stub_pytest.py
|
|
28
|
+
test/test_contract_with_unittest.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
File without changes
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from specmatic.core.decorators import specmatic_contract_test
|
|
4
|
+
from specmatic.utils import get_project_root
|
|
5
|
+
|
|
6
|
+
host = "127.0.0.1"
|
|
7
|
+
port = 5000
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@specmatic_contract_test(host, port)
|
|
11
|
+
class TestContract:
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
if __name__ == '__main__':
|
|
16
|
+
pytest.main()
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from specmatic.core.decorators import specmatic_contract_test, specmatic_stub
|
|
4
|
+
from specmatic.utils import get_project_root
|
|
5
|
+
|
|
6
|
+
host = "127.0.0.1"
|
|
7
|
+
port = 5000
|
|
8
|
+
stub_host = "127.0.0.1"
|
|
9
|
+
stub_port = 8080
|
|
10
|
+
expectation_json_file = get_project_root() + '/test/data/expectation.json'
|
|
11
|
+
service_contract_file = get_project_root() + '/test/spec/product-search-bff-api.yaml'
|
|
12
|
+
stub_contract_file = get_project_root() + '/test/spec/api_order_v1.yaml'
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@specmatic_contract_test(host, port, service_contract_file)
|
|
16
|
+
@specmatic_stub(stub_host, stub_port, [expectation_json_file])
|
|
17
|
+
class TestApiContract:
|
|
18
|
+
@classmethod
|
|
19
|
+
def teardown_class(cls):
|
|
20
|
+
cls.stub.stop()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
if __name__ == '__main__':
|
|
24
|
+
pytest.main()
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
|
|
3
|
+
from specmatic.core.decorators import specmatic_contract_test
|
|
4
|
+
from specmatic.utils import get_project_root
|
|
5
|
+
|
|
6
|
+
host = "127.0.0.1"
|
|
7
|
+
port = 5000
|
|
8
|
+
|
|
9
|
+
@specmatic_contract_test(host, port)
|
|
10
|
+
class TestContractUnitTest(unittest.TestCase):
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
if __name__ == '__main__':
|
|
15
|
+
unittest.main()
|