“A code that cannot be tested is flawed.” – Anonymous

How to perform factory-boy sqlalchemy integration in pytest

Writing tests for your web application is not only an extra layer of guarantee that your end-user won’t have to do it for you, but also a fail-safe in the case new code changes break existing implementations.

Recently I was tasked with developing an API in flask. Needless to say, writing automated tests was a crucial part in ensuring that developing new features would not be a paranoid mine-sweeping excursion. I decided to use pytest which came as a highly recommended framework for testing in Python and factory boy, a package for generating fixtures for testing purposes.

I quickly realized factory-boy sqlalchemy integration in pytest is not that obvious a task. Yet after a tiring day of Googling, eureka!

Let’s do some testing.

Glossary

  • Flask -> Flask is a micro web framework written in Python. It is classified as a microframework because it does not require particular tools or libraries
  • Pytest -> Pytest is a testing framework which allows us to write test codes using python. You can write code to test anything like database, API, even UI if you want.
  • Factory-boy -> factory_boy is a fixtures replacement tool — it aims to replace static, hard to maintain fixtures with easy-to-use factories for complex objects.

Prerequisites:

  • Before we get started you will require a working git environment on your computer so that we can clone our code from GitHub, all other instructions will be indicated on the README file in the repository on GitHub.
  • You’ll also need basic knowledge (very basic) on how git and GitHub work.

We will be testing a TODO API for our particular testing scenario. Since this is not a tutorial about creating a flask API but rather a factory-boy sqlalchemy integration in pytest one, in the benefit of saving time you can get the TODO API code here, the README has detailed instructions on how to set it up. Use the develop branch to follow along, it doesn’t contain what we’ll cover. Alternatively, the final code (tests included) is in the master branch.

Let’s move to the tests folder and start setting up what we’ll need in order to test the TODO API. Since all dependencies have already been installed from the setup from earlier let’s get to the code.

Setting up pytest

Before we can start writing tests we need to set up our pytest environment in the following manner.

  • We will, of course, create our tests folder where all our tests will reside.
  • Consequently, we shall create a conftest file that we will place in our root folder. The purpose of the conftest file is to help in injecting common fixtures among our test cases every time we run out tests. Create a conftest.py in your root folder and add the following code

The conftest file’s configurations are very important in ensuring especially in flask that the app_context is defined as expected both when defining the testing client as well as the db session that we will be calling in every test case.

import pytest
from api import create_app
from api import db as _db

@pytest.fixture
def app():
    app = create_app(config_name='testing')
    yield app

@pytest.fixture
def client(app):
    with app.test_client() as client:
         with app.app_context():
             _db.drop_all()
             _db.create_all()
          yield client


@pytest.fixture
def db(app):
    app.app_context().push()
    _db.init_app(app)
    _db.create_all()
    yield _db
    _db.session.close()
    _db.drop_all()

The contents we’ve added above are as follows, we create a flask app with the testing configuration/environment, we proceed to use that app to create our testing client while ensuring we run it in an app context as flask expects. The final function initializes a DB for later use in our tests when we create our testing model.

Our first test case

Now that our conftest has been setup we can proceed to write our first test. We proceed to create our test files by navigating to the tests folder and creating a few files.

We create a factories.py file where we will be defining our model factories using factory boy. We will also create a test_todo_views.py file where all our testing code will reside. Let’s create the factories.py file and add the below code.

Create the file:

cd tests && touch factories.py test_todo_views.py

Code in factories.py:

import factory
import datetime
import factory.fuzzy as fuzzy
import random
from api import db
from api.app.todos.models import TodoModel


class TodoFactory(factory.alchemy.SQLAlchemyModelFactory):
    class Meta:
        model = TodoModel
        sqlalchemy_session = db.session

    title = factory.Faker('sentence')
    description = factory.Faker('text')
    from_date = fuzzy.FuzzyDate(datetime.date(2020, 3, 20))
    to_date = fuzzy.FuzzyDate(datetime.date(2020, 3, 22))
    completed = factory.LazyFunction(lambda: random.choice([True, False]))

So what exactly is going on in the code above? This is where we perform the sqlalchemy integration into factory boy. On defining the factory class TodoFactory we proceed to create a Meta class which will reference our data model. It is important that sqlalchemy_session be defined in this class and that it references an existing SQLAlchemy session, you can find this description as indicated in the factory-boy docs here.

test_todo_views.py file:

import json
import datetime
from api.apps.tests.factories import TodoFactory

def test_user_can_retrieve_empty_list_todos(client, db):
    response = client.get('/api/v1/todos')
    response_body = response.get_json()
    assert response.status == '200 OK'
    assert len(response_body) == []

def test_user_can_create_a_todo(client, db):
    todo = {
            "title": "Write tests for api",
	     "description": "Write tests for todo api",
	     "from_date": "2020-02-01T00:00:00",
	     "to_date": "2020-02-02T00:00:00",
	      "completed": False,
	     }
    response = client.post('api/v1/todos', 
                             data=json.dumps(todo))
    assert response.status == '201 CREATED'
    assert response.get_json() == todo

Let the tests run, from the terminal in your root folder run below command:

pytest 

We should observe that the two tests we have written pass as expected.

We continue to write more tests, we will test the ability to edit an existing todo as well as the ability to delete one. This is one of the places factory-boy will prove especially useful in creating mock model fixtures for us, let’s get to it:

In order to test edit capabilities, we add the following code in  our test_todo_views.py file:

def test_user_can_edit_existing_todo(client, db):
    TodoFactory(id=1)
    edit_data = {"title": "Cool vacation to Mombasa"}
    response = client.patch('api/v1/todos/1',
                headers={'Content-Type': 'application/json'},     data=json.dumps(edit_data))
    assert response.status == '200 OK'
    assert response.get_json()["title"] == edit_data["title"]

def test_user_can_delete_an_existing_todo(client, db):
    TodoFactory(id=1)
    response = client.delete('api/v1/todos/1',
                    headers={'Content-Type': 'application/json'})
    assert response.status == '200 OK'
    assert response.get_json()["title"] == edit_data["title"]

Running our tests again we see that they all pass as expected:

collected 4 items                                                                                                                                

tests/test_todo_views.py ....                                                                                                              [100%]

=============================================================== 4 passed in 9.01s ================================================================

Learning strategy

I extensively used Google to work out how to integrate factory-boy with sqlalchemy. Factory-boy documentation is not clear about how to do this especially in creating a DB session when defining the factories. In all honesty, I couldn’t list all the resources I had to peruse and the various blogs I had to read through to get this working, they are too many. Pytest documentation on the other is quite thorough and I consulted it before starting testing.

In Retrospect

The importance of TDD was more than evident during the entire process. This was my first major project that was implemented using a TDD approach. Consequently, I learned a lot about how to drive your development using tests and not the other way round. As a result, I was able to have a predictive edge on the kind of changes that might cause errors and bug tracking was a painless process. 

In conclusion

Factory-boy sqlalchemy integration in pytest is evidently not a straight forward process even with the help of documentation. My hope is that this small example will unblock someone traversing the internet in search of a solution. It took me 8 hours to complete the entire write up including coding the TODO API, a process I hope to cover in a future blog post. 

You can also check out more of my blogs on python on LDT here and here.

Learning tools

Pytest documentation.

Factory boy documentation.

Stackoverflow thread about sqlalchemy sessions in factory boy.

Citations

The feature image is courtesy of unsplash