A framework for testing an API based on its OpenAPI schema.
This framework allows to create anything between a manual test and a fully automated test.
Install Python (versions 3.6-3.9 where tested to work)
Clone this repository
git clone https://github.com/specify/open_api_tools
cd open_api_toolsConfigure a virtual environment
python -m venv venvInstall the dependencies
./venv/bin/pip install -r requirements.txtInstall this package locally
pip install -e .There are three main use cases, each going from more automated to more manual.
The most automated is the full_test, which, by default, generates test URLs
with some parameters based on OpenAPI schema, then sends those requests and
makes sure that responses match the schema definition. By default, this method
only tests GET endpoints and it does not generate request body object.
However, this can be changed by providing additional parameters. Also, you can
define parameter constraints (e.x if 'a' is set to True, then response must
contain 'b') to further improve the quality of this test.
Next, there is a Chain test that allows to test a chain of requests and make sure that each request correctly influenced the response of the next request.
Finally, for those that need complete control, there is a make_request method
which facilitates validating the request parameters, sending a single request,
validating the response parameters and returning the result.
The handler function should return a boolean value saying validating whether the response object is as expected
Run the test
from open_api_tools.test.full_test import full_test
from open_api_tools.common.load_schema import load_schema
schema = load_schema('open_api.yaml')
# Error message schema is defined in open_api_tools.validate.index
def after_error_occurred(*error_message):
print(error_message)
full_test(
schema=schema,
max_urls_per_endpoint=50,
failed_request_limit=10,
after_error_occurred=after_error_occurred,
)This script would automatically generate test URLs based on your API schema.
All requests would be sent to the first server
specified in the servers part of the API schema.
max_urls_per_endpoint parameter defines the limit of queries to send to a
single endpoint.
failed_request_limit makes sure that the full_test function quits with an
exception if a certain number of requests failed validation. This is useful for
preventing needless server load when all requests are failing for the same
reason.
By default, the test reads the examples object
in the schema to generate
request parameters. If examples weren't provided, it would try to create some
test values based on the parameter type.
If you would like more customization, an optional after_examples_generated
hook can be provided to the full_test method.
after_examples_generated must be a function that accepts an endpoint name as
the first parameter and
the parameter object
as the second parameter (the parameter object would vary depending on how it is
defined in your schema). In turn, the function must return a list of valid
examples.
Example usage:
from open_api_tools.test.full_test import full_test
from open_api_tools.common.load_schema import load_schema
schema = load_schema('open_api.yaml')
def after_error_occurred(*error_message):
print(error_message)
def after_examples_generated(
endpoint_name,
parameter,
autogenerated_examples
):
if endpoint_name == '/api/posts/' and parameter.name == 'post_id':
return [1, 2, 3, 4, 5]
elif parameter.schema.type == 'string':
# Some naughty strings
return ["ÅÍÎÏ˝ÓÔÒÚÆ☃", "Ω≈ç√∫˜µ≤≥÷", "⅛⅜⅝⅞"]
else:
return autogenerated_examples
full_test(
schema=schema,
max_urls_per_endpoint=50,
failed_request_limit=10,
after_error_occurred=after_error_occurred,
after_examples_generated=after_examples_generated
)The third parameter in after_examples_generated is the list of examples that
were generated by this framework. If you don't want to change the generated
examples, the function can return back this value.
Note that after_examples_generated would also get called with
requestBody as a parameter.name. This allows you to to provide a list of
request objects that would be used in testing. Each request object should be of
type (str, str), where the first string is the MIME type and the second one is
the serialized payload that would be send with the request.
full_test also supports a before_request_send hook that allows you to modify
the request object before a request is sent. This is useful if you want to edit
the headers or add authentication cookies.
Example usage:
from open_api_tools.test.full_test import full_test
from open_api_tools.common.load_schema import load_schema
schema = load_schema('open_api.yaml')
def before_request_send(endpoint_name, request_object):
if endpoint_name == '/api/main/{id}/':
if request_object.headers is None:
request_object.headers = {}
request_object.headers['Authorization']='Basic YWxhZGRpbjpvcGVuc2VzYW1l'
return request_object
full_test(
schema=schema,
max_urls_per_endpoint=50,
failed_request_limit=10,
before_request_send=before_request_send
)The schema for the request object is defined here.
If response object depends on the query parameters, you can
test for these relationships by adding your parameter names
and handler functions to the parameter_constraints dictionary and passing it
to full_test.
Each handler function would receive the following arguments:
- parameter_value (any): the value of the parameter this handler works with
- path (str): name of the current endpoint (useful if the same parameter is shared between multiple endpoints)
- response (any): response object.
Example usage:
from open_api_tools.test.full_test import full_test
from open_api_tools.common.load_schema import load_schema
schema = load_schema('open_api.yaml')
def get_popular_posts(
parameter_value: int,
endpoint: str,
response: object
):
for post in response.json():
if post.popularity < parameter_value:
raise Exception(
f'{endpoint} failed to filter the posts by popularity'
)
full_test(
schema=schema,
max_urls_per_endpoint=50,
failed_request_limit=10,
parameter_constraints={
},
)For more fine-grained testing, there is a chain method that allows to test
a chain of request URLs with request/response object validation and assurance
that each request produced expected results.
Example usage:
from open_api_tools.test.chain import chain, Request, Validate
from open_api_tools.common.load_schema import load_schema
import json
schema = load_schema('open_api.yaml')
post_id = 345
def create_post(_arguments, _response, _previous_values):
return {
"requestBody": [
'application/json',
json.dumps(dict(
id=post_id,
name='Post Name',
body='Post content'
))
]
}
chain(
schema=schema,
definition=[
Request(method='GET', endpoint='/api/posts/'),
Validate(
validate=\
lambda response: post_id not in response.json().posts
),
Request(
method='POST',
endpoint='/api/posts/',
parameters=create_post
),
Validate(
validate=lambda response: post_id in response.json().posts
),
Request(
method='DELETE',
endpoint='/api/posts/',
parameters={'id': post_id}
),
Validate(
validate= \
lambda response: post_id not in response.json().posts
),
],
)The response object for the validation function is described here.
Keep in mind that the endpoint string in the Request class should be
identical to one of the endpoints in your OpenAPI schema. For example, you
should specify /api/user/{user_id}/ instead of /api/user/1/. Both path
parameters and query parameters should be supplied in the parameters
dictionary or returned by the parameters function. If parameters is a
function, it would get called with these arguments:
(list_of_parameter_objects, previous_response, previous_parameter_values).
Alternatively, you can omit the parameters key altogether if the endpoint
doesn't expect any.
Also, Request can omit parameters if there aren't any to define.
Alternatively, you can supply a dictionary, or a function that would get called
with three arguments:
(list_of_parameter_objects, previous_response, previous_parameter_values).
Additionally, you can
supply a requestBody parameter. Unlike most parameters, requestBody must be
a Tuple[str,str] where the first string is a MIME type and the second string
is a serialized version of the request body.
The Validate class expects a function that takes a response object and
returns a boolean saying whether a value is valid. On false, the chain stops.
Note, if Validate returned false, an exception is not thrown, but you can
throw one on your own if you need to.
The chain method also accepts a before_request_send parameter, which is
described in detail in the previous section
make_request method is most useful when you need complete control over the
requests that get send, but still need the assurance that request/response
objects confirm to schema.
Example usage:
from open_api_tools.validate.index import make_request
from open_api_tools.common.load_schema import load_schema
import json
schema = load_schema('open_api.yaml')
response = make_request(
schema=schema,
request_url='http://localhost/api/posts/1/?update_indexes=true',
endpoint_name='/api/posts/<post_id>',
method='POST',
body=('application/json', json.dumps({"name": 'New post name'})),
)For additional control, the make_request method also accepts a
before_request_send parameter, which is described in detail in the previous
section.
If you want to only verify the request object, or want to execute some
additional code before executing the request, the make_request can be
broken down into prepare_request and file_request methods. They are defined
in open_api_tools/validate/index.py.