The things specified in this section are only applicable if you are using Tavern to test a HTTP API (ie, unless you are specifically checking MQTT or some other plugin).
Using multiple status codes¶
If the server you are contacting might return one of a few different status codes depending on it’s internal state, you can write a test that has a list of status codes in the expected response.
Say for example we want to try and get a user’s details from a server - if it exists, it returns a 200. If not, it returns a 404. We don’t care which one, as long as it it only one of those two codes.
---
test_name: Make sure that the server will either return a 200 or a 404
stages:
- name: Try to get user
request:
url: "{host}/users/joebloggs"
method: GET
response:
status_code:
- 200
- 404Note that there is no way to do something like this for the body of the
response, so unless you are expecting the same response body for every possible
status code, the json key should be left blank.
Sending form encoded data¶
Though Tavern can only currently verify JSON data in the response, data can be
sent using x-www-form-urlencoded encoding by using the data key instead of
json in a request. An example of sending form data rather than json:
request:
url: "{test_host}/form_data"
method: POST
data:
id: abc123Authorisation¶
Persistent cookies¶
Tavern uses
requests
under the hood, and uses a persistent Session for each test. This means that
cookies are propagated forward to further stages of a test. Cookies can also be
required to pass a test. For example, say we have a server that returns a cookie
which then needs to be used for future requests:
---
test_name: Make sure cookie is required to log in
includes:
- !include common.yaml
stages:
- name: Try to check user info without login information
request:
url: "{host}/userinfo"
method: GET
response:
status_code: 401
json:
error: "no login information"
headers:
content-type: application/json
- name: login
request:
url: "{host}/login"
json:
user: test-user
password: correct-password
method: POST
headers:
content-type: application/json
response:
status_code: 200
cookies:
- session-cookie
headers:
content-type: application/json
- name: Check user info
request:
url: "{host}/userinfo"
method: GET
response:
status_code: 200
json:
name: test-user
headers:
content-type: application/jsonThis test ensures that a cookie called session-cookie is returned from the
‘login’ stage, and this cookie will be sent with all future stages of that test.
Choosing cookies¶
If you have multiple cookies for a domain, the cookies key
can also be used in the request block to specify which one to send:
---
test_name: Test receiving and sending cookie
includes:
- !include common.yaml
stages:
- name: Expect multiple cookies returned
request:
url: "{host}/get_cookie"
method: POST
response:
status_code: 200
cookies:
- tavern-cookie-1
- tavern-cookie-2
- name: Only send one cookie
request:
url: "{host}/expect_cookie"
method: GET
cookies:
- tavern-cookie-1
response:
status_code: 200
json:
status: okTrying to specify a cookie which does not exist will fail the stage.
To send no cookies, simply use an empty array:
---
test_name: Test receiving and sending cookie
includes:
- !include common.yaml
stages:
- name: get cookie for domain
request:
url: "{host}/get_cookie"
method: POST
response:
status_code: 200
cookies:
- tavern-cookie-1
- name: Send no cookies
request:
url: "{host}/expect_cookie"
method: GET
cookies: [ ]
response:
status_code: 403
json:
status: access deniedOverriding cookies¶
If you want to override the value of a cookie, then instead of passing a string
to the cookies block in the request, use a mapping of cookie name: cookie value:
- name: Override cookie value
request:
url: "{host}/expect_cookie"
method: GET
cookies:
- tavern-cookie-2: abc
response:
status_code: 200
json:
status: ok
This will create a new cookie with the name tavern-cookie-2 with the value
abc and send it in the request. If this cookie already exists from a previous
stage, it will be overwritten. Trying to override the cookie multiple times in
one stage will cause an error to occur at runtime.
HTTP Basic Auth¶
For a server that expects HTTP Basic Auth, the auth keyword can be used in the
request block. This expects a list of two items - the first item is the user
name, and the second name is the password:
---
test_name: Check we can access API with HTTP basic auth
includes:
- !include common.yaml
stages:
- name: Get user info
request:
url: "{host}/userinfo"
method: GET
auth:
- user@api.com
- password123
response:
status_code: 200
json:
user_id: 123
headers:
content-type: application/jsonCustom auth via $ext¶
For authentication schemes beyond HTTP Basic Auth (such as HTTP Digest Auth or
custom token-based auth), use the $ext key with the auth field. The
$ext function should return an instance of
requests.auth.AuthBase
(or any callable that accepts a PreparedRequest and returns it).
This is useful for HTTP Digest Auth, OAuth, custom header-based auth, or any custom authentication flow.
---
test_name: Test digest auth via external function
includes:
- !include common.yaml
stages:
- name: Send with digest auth
request:
url: "{host}/digest-endpoint"
method: GET
auth:
$ext:
function: ext_functions:get_digest_auth
response:
status_code: 200Your external module would return an AuthBase instance:
from requests.auth import HTTPDigestAuth
def get_digest_auth():
return HTTPDigestAuth("myuser", "mypassword")You can also use custom auth classes for non-standard schemes:
from requests.auth import AuthBase
class TokenAuth(AuthBase):
def __init__(self, token):
self.token = token
def __call__(self, r):
r.headers["X-api-token"] = f"Token {self.token}"
return r
def get_token_auth():
return TokenAuth("abc123")See the requests documentation for how this works.
Custom auth header¶
If you’re using a form of authorisation not covered by the above examples to
authorise against your test server (for example, a JWT-based system), specify a
custom Authorization header. If you are using a JWT, you can use the built in
validate_jwt external function as defined above to check that the claims are
what you’d expect.
---
test_name: Check we can login then use a JWT to access the API
includes:
- !include common.yaml
stages:
- name: login
request:
url: "{host}/login"
json:
user: test-user
password: correct-password
method: POST
headers:
content-type: application/json
response:
status_code: 200
json:
$ext: &verify_token
function: tavern.helpers:validate_jwt
extra_kwargs:
jwt_key: "token"
key: CGQgaG7GYvTcpaQZqosLy4
options:
verify_signature: true
verify_aud: true
verify_exp: true
audience: testserver
headers:
content-type: application/json
save:
json:
test_login_token: token
- name: Get user info
request:
url: "{host}/userinfo"
method: GET
headers:
Authorization: "Bearer {test_login_token:s}"
response:
status_code: 200
json:
user_id: 123
headers:
content-type: application/jsonControlling secure access¶
Running against an unverified server¶
If you’re testing against a server which has SSL certificates that fail
validation (for example, testing against a local development server with
self-signed certificates), the verify keyword can be used in the request
stage to disable certificate checking for that request.
Using self signed certificates¶
In case you need to use a self-signed certificate to connect to a server,
you can use the cert key in the request to control which certificates
will be used by Requests.
If you just want to pass your client certificate with a request, pass
the path to it using the cert key:
---
test_name: Access an API which requires a client certificate
stages:
- name: Get user info
request:
url: "{host}/userinfo"
method: GET
cert: "/path/to/certificate"
# Or use a format variable:
# cert: "{cert_path}"
response:
...If you need to pass a SSL key file as well, pass a list of length two with the first element being the certificate and the second being the path to the key:
---
test_name: Access an API which requires a client certificate
stages:
- name: Get user info
request:
url: "{host}/userinfo"
method: GET
cert:
- "/path/to/certificate"
- "/path/to/key"
response:
...See the Requests documentation for more details about this option.
Uploading files as part of the request¶
To upload a file along with the request, the files key can be used:
---
test_name: Test files can be uploaded with tavern
includes:
- !include common.yaml
stages:
- name: Upload multiple files
request:
url: "{host}/fake_upload_file"
method: POST
files:
test_files: "test_files.tavern.yaml"
common: "common.yaml"
response:
status_code: 200This expects a mapping of the ‘name’ of the file in the request to the path on your computer.
By default, the sending of files is handled by the Requests library - to see the implementation details, see their documentation.
Uploading a file as the body of a request¶
In some cases it may be required to upload the entire contents of a file in the
request body - for example, when posting a binary data blob from a file. This
can be done for JSON and YAML using the !include tag, but for other data
formats the file_body key can be used:
- name: Upload a file in the request body
request:
url: "{host}/data_blob"
method: POST
file_body: "/path/to/blobfile"
response:
status_code: 200The path can be absolute or relative to:
the directory of the test file (like
!includedoes)any path in the
TAVERN_INCLUDEenvironment variable (colon-separated list of paths)
Like the files key, this is mutually exclusive with the json key.
Specifying custom content type and encoding¶
If you need to use a custom file type and/or encoding when uploading the file,
there is a ‘long form’ specification for uploading files. Instead of just
passing the path to the file to upload, use the file_path and
content_type/content_encoding in the block for the file:
---
test_name: Test files can be uploaded with tavern
stages:
- name: Upload multiple files
request:
url: "{host}/fake_upload_file"
method: POST
files:
# simple style - guess the content type and encoding
test_files: "test_files.tavern.yaml"
# long style - specify them manually
common:
file_path: "common.yaml"
content_type: "application/customtype"
content_encoding: "UTF16"To specify the same file multiple times with a different form field name, use a list for the files:
---
test_name: Test reusing the same file name multiple times
stages:
- name: Upload multiple files
request:
url: "{host}/fake_upload_file"
method: POST
files:
- form_field_name: group_2
file_path: OK.txt
- form_field_name: group_1
file_path: OK.txt
- form_field_name: group_1
file_path: OK.json.gz
content_encoding: gzipTimeout on requests¶
If you want to specify a timeout for a request, this can be done using the
timeout parameter:
---
test_name: Get server info from slow endpoint
stages:
- name: Get info
request:
url: "{host}/get-info-slow"
method: GET
timeout: 0.5
response:
status_code: 200
json:
n_users: 2048
n_queries: 10000If this request takes longer than 0.5 seconds to respond, the test will be considered as failed. A 2-tuple can also be passed - the first value will be a connection timeout, and the second value will be the response timeout. By default this uses the Requests implementation of timeouts - see their documentation for more details.
Redirects¶
By default, Tavern will not follow redirects. This allows you to check whether an endpoint is indeed redirecting a user to a certain page.
To disable this behaviour, use either the --tavern-always-follow-redirects
command line flag or set tavern-always-follow-redirects to True in your Pytest
settings file.
This can also be disabled or enabled on a per-stage basis by using the follow_redirects flag:
---
test_name: Expect a redirect when setting the flag
stages:
- name: Expect to be redirected
request:
url: "{host}/redirect/source"
follow_redirects: true
response:
status_code: 200
json:
status: successful redirectSpecifying follow_redirects on a stage will override any global setting, so if
you just want to change the behaviour for one stage then use this flag.
Matching plain text responses¶
If your API returns plain text (non-JSON) responses, you can use the text key
to validate the response body:
---
test_name: Test plain text response
stages:
- name: Get plain text response
request:
url: "{host}/text_response"
method: GET
response:
status_code: 200
text: "Hello, World!"This also works with multiline text:
---
test_name: Test ASCII table response
stages:
- name: Get ASCII table
request:
url: "{host}/ascii_table"
method: GET
response:
status_code: 200
text: |
+----+----------+-------+
| id | name | score |
+----+----------+-------+
| 1 | Alice | 95 |
| 2 | Bob | 87 |
| 3 | Charlie | 92 |
+----+----------+-------+Matching text response body from a file¶
If you have a large expected response body, you can store it in a file and use
!include_raw inside the text block to validate the response against the file contents:
---
test_name: Test response against file content
stages:
- name: Match response against file
request:
url: "{host}/ascii_table"
method: GET
response:
status_code: 200
text: !include_raw expected_table.txtThe !include_raw tag, similar to the !include tag, is a special tag that will read the file contents and
include them in the test. This will include the file ‘as-is’ without loading as
JSON like the !include tag does.