How to write tests for Pydantic models in FastAPI?

Question:

I just started using FastAPI but I do not know how do I write a unit test (using pytest) for a Pydantic model.

Here is a sample Pydantic model:

class PhoneNumber(BaseModel):
    id: int
    country: str
    country_code: str
    number: str
    extension: str

I want to test this model by creating a sample PhoneNumber instance and ensure that the PhoneNumber instance tallies with the field types. For example:

PhoneNumber(1, "country", "code", "number", "extension")

Then, I want to assert that PhoneNumber.country equals "country".

Asked By: PercySherlock

||

Answers:

The test you want to achieve is straightforward to do with pytest:

import pytest

def test_phonenumber():
    pn = PhoneNumber(id=1, country="country", country_code="code", number="number", extension="extension")

    assert pn.id == 1
    assert pn.country == 'country'
    assert pn.country_code == 'code'
    assert pn.number == 'number'
    assert pn.extension == 'extension'

But I agree with this comment:

Generally speaking, you don’t write tests like this. Pydantic has a
good test suite (including a unit test like the one you’re proposing)
. Your test should cover the code and logic you wrote, not the
packages you imported.

If you have a model like PhoneNumber model without any special/complex validations, then writing tests that simply instantiates it and checks attributes won’t be that useful. Tests like those are like testing Pydantic itself.

If, however, your model has some special/complex validator functions, for example, it checks if country and country_code match:

from pydantic import BaseModel, root_validator

class PhoneNumber(BaseModel):
    ...

    @root_validator(pre=True)
    def check_country(cls, values):
        """Check that country_code is the 1st 2 letters of country"""
        country: str = values.get('country')
        country_code: str = values.get('country_code')
        if not country.lower().startswith(country_code.lower()):
            raise ValueError('country_code and country do not match')
        return values

…then a unit test for that specific behavior would be more useful:

import pytest

def test_phonenumber_country_code():
    """Expect test to fail because country_code and country do not match"""
    with pytest.raises(ValueError):
        PhoneNumber(id=1, country='JAPAN', country_code='XY', number='123', extension='456')

Also, as I mentioned in my comment, since you mentioned FastAPI, if you are using this model as part of a route definition (either it’s a request parameter or a response model), then a more useful test would be making sure that your route can use your model correctly.

@app.post("/phonenumber")
async def add_phonenumber(phonenumber: PhoneNumber):
    """The model is used here as part of the Request Body"""
    # Do something with phonenumber
    return JSONResponse({'message': 'OK'}, status_code=200)
from fastapi.testclient import TestClient

client = TestClient(app)

def test_add_phonenumber_ok():
    """Valid PhoneNumber, should be 200/OK"""
    # This would be what the JSON body of the request would look like
    body = {
        "id": 1,
        "country": "Japan",
        "country_code": "JA",
        "number": "123",
        "extension": "81",
    }
    response = client.post("/phonenumber", json=body)
    assert response.status_code == 200


def test_add_phonenumber_error():
    """Invalid PhoneNumber, should be a validation error"""
    # This would be what the JSON body of the request would look like
    body = {
        "id": 1,
        "country": "Japan",
                             # `country_code` is missing
        "number": 99999,     # `number` is int, not str
        "extension": "81",
    }
    response = client.post("/phonenumber", json=body)
    assert response.status_code == 422
    assert response.json() == {
        'detail': [{
            'loc': ['body', 'country_code'],
            'msg': 'field required',
            'type': 'value_error.missing'
        }]
    }
Answered By: Gino Mempin