Michael Bukachi Michael is a Software Engineer at Mobidev Kenya Limited
Published

September 10, 2018

←Home

Maintaining states while using Pytest

Pre-requisites

  • Beginner level Python
  • Python2 or Python3 installed
  • Basic knowledge on TDD and python UnitTesting
  • Pytest installed

Intro

alt text

Ah, testing, a topic many devs don't like hearing. I really don't blame them. Testing is tricky. It's difficult to get it right and it's always evolving. I personally struggled with it for quite some time before I got the hang of it. But it is necessary. These days I can't start a project before making sure that there are testing frameworks available for the specific language I am using.

Disclaimer: This guide is not an introduction to testing using pytest. For that, just open our friendly search engine and search "getting started with pytest". Trust me, you won't regret it.

Getting started

For this guide, I'll be using flask for demonstration. The demo project is available here. The basic structure of the project as follows:

├── app
│   ├── __init__.py
│   └── views.py
├── requirements.txt
└── test
    ├── conftest.py
    ├── __init__.py
    └── test_token.py

app/init.py

from flask import Flask


def create_app():
    app = Flask(__name__)
    from .views import auth
    app.register_blueprint(auth)
    return app

Notice that I've used the lazy pattern so that it is easy to test.

app/views.py

from uuid import uuid4

from flask import Blueprint, jsonify, request

auth = Blueprint('auth', __name__)

TOKEN = str(uuid4())


@auth.route('/token')
def get_token():
    return jsonify(TOKEN)


@auth.route('/secure', methods=['POST'])
def secure_page():
    args = request.get_json(force=True)
    if 'token' in args:
        if args['token'] == TOKEN:
            return jsonify('This is a secure page')

    res = jsonify('Unauthorized')
    res.status_code = 401
    return res

test/conftest.py

import json

import pytest
from flask import Response
from flask.testing import FlaskClient
from werkzeug.utils import cached_property

from app import create_app


class JSONResponse(Response):

    @cached_property
    def json(self):
        return json.loads(self.get_data(as_text=True))


@pytest.fixture('session')
def flask_app():
    app = create_app()
    yield app


@pytest.fixture('session')
def client(flask_app):
    app = flask_app
    ctx = flask_app.test_request_context()
    ctx.push()
    app.test_client_class = FlaskClient
    app.response_class = JSONResponse
    return app.test_client()

test/test_token.py

import pytest
from flask.testing import FlaskClient


@pytest.fixture('module')
def token():
    return __name__ + ':token'


def test_get_token(client: FlaskClient, token, request):
    res = client.get('/token')
    assert res.status_code == 200
    token_ = res.json
    request.config.cache.set(token, token_)


def test_secure_page(client: FlaskClient, token, request):
    token_ = request.config.cache.get(token, None)
    res = client.post('/secure', json={})
    assert res.status_code == 401
    res = client.post('/secure', json={'token': token_})
    assert res.status_code == 200

For those who have not used pytest before, pytest basically uses dependency injection to provide dependencies to tests. This has numerous applications, from providing dummy data to providing configurations and constants. If you take a look at test/conftest.py you'll notice that the dependencies to be injected have been defined.

All the magic happens in test/test_token.py in the following lines of code:

@pytest.fixture('module')
def token():
    return __name__ + ':token'

The name of the function can be anything. What's important to note is the use of __name__ + ':token:. Since state changes are persisted into a file, using __name__ enables creation of unique files in case multiple test are being run in parallel.

The request dependency is provided by pytest and is used to store and retrieve persisted data

I find state persistence very useful in situations why I need to share data between tests. Yes, yes. I know. Test are supposed to be unique and independent. But sometimes it's necessary. For instance, when a token is required to access an api, like in the example above or, if you want to maintain data from a response for use in future requests.

alt text

Go Top