Skip to main content

Property-based testing framework for Open API and GraphQL based apps

Project description

Build codecov.io status for master branch Version Python versions Documentation Status Gitter License

Schemathesis is a tool for testing your web applications built with Open API / Swagger or GraphQL specifications.

It reads the application schema and generates test cases, which will ensure that your application is compliant with its schema.

The application under test could be written in any language; the only thing you need is a valid API schema in a supported format.

Supported specification versions:

  • Swagger 2.0

  • Open API 3.0.x

  • GraphQL June 2018

Docs: https://schemathesis.readthedocs.io/en/stable/

Gitter: https://gitter.im/kiwicom/schemathesis

More information:

  • An article about Schemathesis by @Stranger6667

  • A video from FlaskCon 2020 by @hultner

Also, I occasionally write posts about Schemathesis in my blog and offer consulting services for businesses.

I started this project during my work at Kiwi.com. I am grateful to them for all the support they provided to this project during its early days and for the opportunity to evolve Schemathesis independently.

Please, help us to improve!

We’d kindly ask you to share your experience with Schemathesis. It will help me to make improvements to it and prioritize new features! 🙂 It will take 5 minutes. The results are anonymous.

Survey: https://forms.gle/dv4s5SXAYWzvuwFWA

Installation

To install Schemathesis via pip run the following command:

pip install schemathesis

Usage

There are two basic ways to use Schemathesis:

CLI is pretty simple to use and requires no coding; the in-code approach gives more flexibility.

Command Line Interface

The schemathesis command can be used to perform Schemathesis test cases:

schemathesis run https://example.com/api/swagger.json
https://github.com/schemathesis/schemathesis/blob/master/img/schemathesis.gif

If your application requires authorization, then you can use the --auth option for Basic Auth and --header to specify custom headers to be sent with each request.

To filter your tests by endpoint name, HTTP method, Open API tags, or operationId, you could use -E, -M, -T, -O options, respectively.

CLI supports passing options to hypothesis.settings. All of them are prefixed with --hypothesis-:

schemathesis run --hypothesis-max-examples=1000 https://example.com/api/swagger.json

To speed up the testing process, Schemathesis provides -w/--workers option for concurrent test execution:

schemathesis run -w 8 https://example.com/api/swagger.json

In the example above, all tests will be distributed among 8 worker threads.

If you’d like to test your web app (Flask or AioHTTP or FastAPI, for example), then there is --app option for you:

schemathesis run --app=importable.path:app /swagger.json

You need to specify an importable path to the module where your app instance resides and a variable name after : that points to your app. Note, app factories are not supported. The schema location could be:

  • A full URL;

  • An existing filesystem path;

  • In-app endpoint with the schema.

This method is significantly faster for WSGI apps since it doesn’t involve a network.

For the full list of options, run:

schemathesis --help
# Or
schemathesis run --help

Docker

Schemathesis CLI also available as a docker image

docker run kiwicom/schemathesis:stable run http://example.com/schema.json

To run it against the localhost server, add --network=host parameter:

docker run --network="host" kiwicom/schemathesis:stable run http://127.0.0.1/schema.json

Pre-run CLI hook

Sometimes you need to execute custom code before the CLI run, for example, set up an environment, register custom string format strategies or modify Schemathesis behavior in runtime you can use --pre-run hook:

schemathesis --pre-run importable.path.to.module run https://example.com/api/swagger.json

NOTE. This option should be passed before the run part.

The passed value will be processed as an importable Python path, where you can execute your code. An example - https://github.com/schemathesis/schemathesis#custom-string-strategies

Registering custom checks for CLI

There is a special function to add a new check for the Schemathesis CLI:

import schemathesis

@schemathesis.register_check
def new_check(response, case):
    # some awesome assertions!
    pass

The registered check should accept a response with requests.Response / schemathesis.utils.WSGIResponse type and case with schemathesis.models.Case type.

After registration, your checks will be available in Schemathesis CLI, and you can use them via the -c command-line option.

schemathesis --pre-run module.with.checks run -c new_check https://example.com/api/swagger.json

Additionally, checks may return True to skip the check under certain conditions. For example, you may only want to run checks when the response code is 200.

import schemathesis

@schemathesis.register_check
def conditional_check(response, case):
    if response.status_code == 200:
        # some awesome assertions!
    else:
        # check not relevant to this response, skip test
        return True

In-code

To examine your application with Schemathesis, you need to:

  • Setup & run your application, so it is accessible via the network;

  • Write a couple of tests in Python;

  • Run the tests via pytest.

Suppose you have your application running on http://0.0.0.0:8080 and its schema is available at http://0.0.0.0:8080/swagger.json.

A basic test that will verify that any data that fit into the schema will not cause any internal server error could look like this:

# test_api.py
import requests
import schemathesis

schema = schemathesis.from_uri("http://0.0.0.0:8080/swagger.json")

@schema.parametrize()
def test_no_server_errors(case):
    # `requests` will make an appropriate call under the hood
    response = case.call()  # use `call_wsgi` if you used `schemathesis.from_wsgi`
    # You could use built-in checks
    case.validate_response(response)
    # Or assert the response manually
    assert response.status_code < 500

It consists of four main parts:

  1. Schema preparation; schemathesis package provides multiple ways to initialize the schema - from_path, from_dict, from_uri, from_file and from_wsgi.

  2. Test parametrization; @schema.parametrize() generates separate tests for all endpoint/method combinations available in the schema.

  3. A network call to the running application; case.call does it.

  4. Verifying a property you’d like to test; In the example, we verify that any app response will not indicate a server-side error (HTTP codes 5xx).

NOTE. Look for from_wsgi usage below

Run the tests:

pytest test_api.py

Other properties that could be tested:

  • Any call will be processed in <50 ms - you can verify the app performance;

  • Any unauthorized access will end with a 401 HTTP response code;

Each test function should have the case fixture, which represents a single test case.

Important Case attributes:

  • method - HTTP method

  • formatted_path - full endpoint path

  • headers - HTTP headers

  • query - query parameters

  • body - request body

You can use them manually in network calls or can convert to a dictionary acceptable by requests.request:

import requests

schema = schemathesis.from_uri("http://0.0.0.0:8080/swagger.json")

@schema.parametrize()
def test_no_server_errors(case):
    kwargs = case.as_requests_kwargs()
    response = requests.request(**kwargs)

For each test, Schemathesis will generate a bunch of random inputs acceptable by the schema. This data could be used to verify that your application works in the way described in the schema or that schema describes expected behavior.

By default, there will be 100 test cases per endpoint/method combination. To limit the number of examples, you could use hypothesis.settings decorator on your test functions:

from hypothesis import settings

@schema.parametrize()
@settings(max_examples=5)
def test_something(client, case):
    ...

To narrow down the scope of the schemathesis tests, it is possible to filter by method or endpoint:

@schema.parametrize(method="GET", endpoint="/pet")
def test_no_server_errors(case):
    ...

The acceptable values are regexps or a list of regexps (matched with re.search).

WSGI applications support

Schemathesis supports making calls to WSGI-compliant applications instead of real network calls, in this case the test execution will go much faster.

app = Flask("test_app")

@app.route("/schema.json")
def schema():
    return {...}

@app.route("/v1/users", methods=["GET"])
def users():
    return jsonify([{"name": "Robin"}])

schema = schemathesis.from_wsgi("/schema.json", app)

@schema.parametrize()
def test_no_server_errors(case):
    response = case.call_wsgi()
    assert response.status_code < 500

Explicit examples

If the schema contains parameter examples, then they will be additionally included in the generated cases. Schemathesis supports the use of both OpenAPI example and examples (more information available in the OpenAPI documentation). Note that examples were added in OpenAPI 3, but Schemathesis supports this feature for OpenAPI 2 using x-examples.

paths:
  get:
    parameters:
    - in: body
      name: body
      required: true
      schema: '#/definitions/Pet'

definitions:
  Pet:
    additionalProperties: false
    example:
      name: Doggo
    properties:
      name:
        type: string
    required:
    - name
    type: object

With this Swagger schema example, there will be a case with body {"name": "Doggo"}. Examples handled with example decorator from Hypothesis, more info about its behavior is here.

If you’d like to test only examples provided in the schema, you could utilize --hypothesis-phases=explicit CLI option:

$ schemathesis run --hypothesis-phases=explicit https://example.com/api/swagger.json

Or add this decorator to your test if you use Schemathesis in your Python tests:

from hypothesis import settings, Phase

...
@schema.parametrize()
@settings(phases=[Phase.explicit])
def test_api(case):
    ...

NOTE. Schemathesis supports examples in individual properties. See below:

...
paths:
  /users:
    parameters:
      - in: query
        name: foo
        schema:
          type: object
          properties:
            prop1:
              type: string
              example: prop1 example    # SUPPORTED!
    post:
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                foo:
                  type: string
                  example: bar          # SUPPORTED!

Direct strategies access

For convenience, you can explore the schemas and strategies manually:

>>> import schemathesis
>>> schema = schemathesis.from_uri("http://0.0.0.0:8080/petstore.json")
>>> endpoint = schema["/pet"]["POST"]
>>> strategy = endpoint.as_strategy()
>>> strategy.example()
Case(
    path='/pet',
    method='POST',
    path_parameters={},
    headers={},
    cookies={},
    query={},
    body={
        'name': '\x15.\x13\U0008f42a',
        'photoUrls': ['\x08\U0009f29a', '\U000abfd6\U000427c4', '']
    },
    form_data={}
)

Schema instances implement the Mapping protocol.

NOTE. Paths are relative to the schema’s base path (host + basePath in Open API 2.0 and server.url in Open API 3.0):

# your ``basePath`` is ``/api/v1``
>>> schema["/pet"]["POST"]  # VALID
>>> schema["/api/v1/pet"]["POST"]  # INVALID

Lazy loading

If you have a schema that is not available when the tests are collected, for example, it is built with tools like apispec and requires an application instance available, you can parametrize the tests from a pytest fixture.

# test_api.py
import schemathesis

schema = schemathesis.from_pytest_fixture("fixture_name")

@schema.parametrize()
def test_api(case):
    ...

In this case, the test body will be used as a sub-test via the pytest-subtests library.

NOTE: the used fixture should return a valid schema that could be created via schemathesis.from_dict or other schemathesis.from_ variations.

Extending schemathesis

If you’re looking for a way to extend schemathesis or reuse it in your own application, then the runner module might help you. It can run tests against the given schema URI and will do some simple checks for you.

from schemathesis import runner

events = runner.prepare("http://127.0.0.1:8080/swagger.json")
for event in events:
    # do something with event

runner.prepare creates a generator that yields events of different kinds - BeforeExecution, AfterExecution, etc. They provide a lot of useful information about what happens during tests, but your responsibility is handling these events. You can take some inspiration from Schemathesis CLI implementation. See full description of events in the source code.

If you want to use Schemathesis CLI with your custom checks, look at this section

The built-in checks list includes the following:

  • Not a server error. Asserts that the response’s status code is less than 500;

  • Status code conformance. Asserts that the response’s status code is listed in the schema;

  • Content-type conformance. Asserts that the response’s content type is listed in the schema;

  • Response schema conformance. Asserts that the response’s content conforms to the declared schema;

You can provide your custom checks to the execute function; the check is a callable that accepts one argument of requests.Response type.

from datetime import timedelta
from schemathesis import runner, models

def not_too_long(response, case: models.Case):
    assert response.elapsed < timedelta(milliseconds=300)

events = runner.prepare("http://127.0.0.1:8080/swagger.json", checks=[not_too_long])
for event in events:
    # do something with event

Custom string strategies

Some string fields could use custom format and validators, e.g., card_number and Luhn algorithm validator.

For such cases, it is possible to register custom strategies:

  1. Create hypothesis.strategies.SearchStrategy object

  2. Optionally provide predicate function to filter values

  3. Register it via schemathesis.register_string_format

strategy = strategies.from_regex(r"\A4[0-9]{15}\Z").filter(luhn_validator)
schemathesis.register_string_format("visa_cards", strategy)

Unittest support

Schemathesis supports Python’s built-in unittest framework out of the box, you only need to specify strategies for hypothesis.given:

from unittest import TestCase
from hypothesis import given
import schemathesis

schema = schemathesis.from_uri("http://0.0.0.0:8080/petstore.json")
new_pet_strategy = schema["/v2/pet"]["POST"].as_strategy()

class TestSchema(TestCase):

    @given(case=new_pet_strategy)
    def test_pets(self, case):
        response = case.call()
        assert response.status_code < 500

Schema validation

To avoid obscure and hard to debug errors during test runs, Schemathesis validates input schemas for conformance with the relevant spec. If you’d like to disable this behavior, use --validate-schema=false in CLI and validate_schema=False argument in loaders.

Local development

First, you need to prepare a virtual environment with poetry. Install poetry (check out the installation guide) and run this command inside the project root:

poetry install

For simpler local development Schemathesis includes a aiohttp-based server with the following endpoints in Swagger 2.0 schema:

  • /api/success - always returns {"success": true}

  • /api/failure - always returns 500

  • /api/slow - always returns {"slow": true} after 250 ms delay

  • /api/unsatisfiable - parameters for this endpoint are impossible to generate

  • /api/invalid - invalid parameter definition. Uses int instead of integer

  • /api/flaky - returns 1/1 ratio of 200/500 responses

  • /api/multipart - accepts multipart data

  • /api/teapot - returns 418 status code, that is not listed in the schema

  • /api/text - returns plain/text responses, which are not declared in the schema

  • /api/malformed_json - returns malformed JSON with application/json content type header

To start the server:

./test_server.sh 8081

It is possible to configure available endpoints via the --endpoints option. The value is expected to be a comma-separated string with endpoint names (success, failure, slow, etc.):

./test_server.sh 8081 --endpoints=success,slow

Then you could use CLI against this server:

schemathesis run http://127.0.0.1:8081/schema.yaml
================================== Schemathesis test session starts =================================
platform Linux -- Python 3.7.4, schemathesis-0.12.2, hypothesis-4.39.0, hypothesis_jsonschema-0.9.8
rootdir: /
hypothesis profile 'default' -> database=DirectoryBasedExampleDatabase('/.hypothesis/examples')
Schema location: http://127.0.0.1:8081/schema.yaml
Base URL: http://127.0.0.1:8081
Specification version: Swagger 2.0
collected endpoints: 2

GET /api/slow .                                                                               [ 50%]
GET /api/success .                                                                            [100%]

============================================== SUMMARY ==============================================

not_a_server_error            2 / 2 passed          PASSED

========================================= 2 passed in 0.29s =========================================

Running tests

You could run tests via tox:

tox -p all -o

or pytest in your current environment:

pytest test/ -n auto

Contributing

Any contribution to development, testing, or any other area is highly appreciated and useful to the project.

Please, see the CONTRIBUTING.rst file for more details.

Python support

Schemathesis supports Python 3.6, 3.7, and 3.8.

License

The code in this project is licensed under MIT license. By contributing to schemathesis, you agree that your contributions will be licensed under its MIT license.

Project details


Release history Release notifications | RSS feed

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

schemathesis-2.5.0.tar.gz (90.2 kB view details)

Uploaded Source

Built Distribution

schemathesis-2.5.0-py3-none-any.whl (105.3 kB view details)

Uploaded Python 3

File details

Details for the file schemathesis-2.5.0.tar.gz.

File metadata

  • Download URL: schemathesis-2.5.0.tar.gz
  • Upload date:
  • Size: 90.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/1.0.10 CPython/3.8.5 Linux/5.4.0-1025-azure

File hashes

Hashes for schemathesis-2.5.0.tar.gz
Algorithm Hash digest
SHA256 f857acce317ed42fd6b6f434be7a8d8df139f529d5d98eae49112799fba37c07
MD5 5aed95168ada88663fbd4a1c37cc6651
BLAKE2b-256 eb4ec497b9c69bb23448688f8147584832bf813e6904fb73a03c4681770558af

See more details on using hashes here.

File details

Details for the file schemathesis-2.5.0-py3-none-any.whl.

File metadata

  • Download URL: schemathesis-2.5.0-py3-none-any.whl
  • Upload date:
  • Size: 105.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/1.0.10 CPython/3.8.5 Linux/5.4.0-1025-azure

File hashes

Hashes for schemathesis-2.5.0-py3-none-any.whl
Algorithm Hash digest
SHA256 11be353b768f7866a5630b1ec394d5a3458889bb91d0306d0e8dc5bc4af21820
MD5 73e6ced6052eb37edac7f4f7dc625517
BLAKE2b-256 454719fbe2ac1698d91f86c347c813be9ea4d0d4b13e24f4ba031925aad82a42

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page