13

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".

Gino Mempin
  • 25,369
  • 29
  • 96
  • 135
PercySherlock
  • 371
  • 1
  • 2
  • 12
  • What do you expect the test to check? How does it relate to FastAPI? Because you can use pydantic models *without* using FastAPI. – Gino Mempin Aug 09 '21 at 00:32
  • @GinoMempin Please check my edit. – PercySherlock Aug 09 '21 at 00:42
  • 2
    @Mark Yes, it was. I've made changes. Thanks for pointing it out. – PercySherlock Aug 09 '21 at 00:52
  • 6
    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](https://github.com/samuelcolvin/pydantic/blob/master/tests/test_construction.py#L15)) . Your test should cover the code and logic you wrote, not the packages you imported. – Mark Aug 09 '21 at 01:00
  • @Mark Oh! I see. Thanks a lot! – PercySherlock Aug 09 '21 at 01:08
  • 3
    Since you tagged this with FastAPI, and if you are using this model as part of a route (either as a request parameter or a response model), then a more useful test is to check that calling your route/API correctly uses your model (ex. passing a JSON body translates correctly into you PhoneNumber model). – Gino Mempin Aug 09 '21 at 01:43
  • 2
    @GinoMempin I understand now. Thanks! – PercySherlock Aug 09 '21 at 02:16

1 Answers1

28

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'
        }]
    }
Gino Mempin
  • 25,369
  • 29
  • 96
  • 135