Lucas Simon

Web Developer. lucassrod@gmail.com

Série API em Flask - Parte 6 - Criando e testando nosso modelo de usuários

Oi pessoal, vamos criar nosso modelo de usuários e realizar alguns testes em cima dele. Graças ao pacote mongoengine fica bem fácil de criar nossas classes referente ao modelo de dados e é bem parecido com o Django, porém sem as migrations.

Para entendimento, segue os capítulos que serão abordados.

O repositório com todo o código fonte esta aqui. Os capítulos estão em branches.

Model Users

Dentro de nossa pasta apps vamos criar um novo diretório chamado users e adicionar nosso módulo models.py. Não se esqueçam de sempre criar o __init__.py.

Ficando assim nossa estrutura:

$ tree .
.
├── api.py
├── db.py
├── __init__.py
└── users
    ├── __init__.py
    └── models.py

Em nosso models.py vamos primeiro criar uma classe abtrata chamada UserMixin, apenas para titulo de curiosidade e saber que existe, em seguida criaremos nossa classe User. Vamos ao código:

# Python
from datetime import datetime

# Third
from mongoengine import (
    BooleanField,
    DateTimeField,
    DictField,
    EmailField,
    EmbeddedDocument,
    EmbeddedDocumentField,
    StringField,
    URLField
)

# Apps
from apps.db import db


class Roles(EmbeddedDocument):
    """
    Roles permissions
    """
    admin = BooleanField(default=False)


class UserMixin(db.Document):
    """
    Default implementation for User fields
    """
    meta = {
        'abstract': True,
        'ordering': ['email']
    }

    email = EmailField(required=True, unique=True)
    password = StringField(required=True)
    roles = EmbeddedDocumentField(Roles, default=Roles)
    created = DateTimeField(default=datetime.now)
    active = BooleanField(default=False)

    def is_active(self):
        return self.active

    def is_admin(self):
        return self.roles.admin

# Abaixo fica o código para a classe Adress

Em complemento vamos criar uma classe Address que será utilizada em nosso model Users. Essa classe herda de EmbbededDocument do mongoengine e nada mais é de uma maneira simples colocar dicionários dentro de um campo, com campos fixos.


class Address(EmbeddedDocument):
    """
    Default implementation for address fields
    """
    meta = {
        'ordering': ['zip_code']
    }
    zip_code = StringField(default='')
    address = StringField(default='')
    number = StringField(default='')
    complement = StringField(default='')
    neighborhood = StringField(default='')
    city = StringField(default='')
    city_id = StringField(default='')
    state = StringField(default='')
    country = StringField(default='BRA')

# Abaixo fica o código para a classe User

E por fim criaremos nossa classe User a qual possui herança da nossa classe abstrata UserMixin.


class User(UserMixin):
    '''
    Users
    '''
    meta = {'collection': 'users'}

    full_name = StringField(required=True)
    cpf_cnpj = StringField(default='')
    address = EmbeddedDocumentField(Address, default=Address)

O que fizemos até aqui foi modelar nossa entidade usuário com os campos herdados da classe UserMixin e alteramos o nome da nossa coleção através do meta = {'collection': 'users'}.

É importante ler a documentação do mongoengine pois ela irá ajudar muito quando precisar de criar novos campos como uma lista de strings, tags = ListField(StringField(max_length=30)) e opções mais avançadas quando se trabalha com o MongoDB.

Testes

Neste artigo eu não quis fazer através de TDD pois iria ficar muito extenso. Logo nada impede que você e o time que está trabalhando faça o desenvolvimento utilizando o TDD e refatorando o sistema.

Antes de codificar os testes eu gostaria de reforçar que geralmente eu faço as seguintes validações em cima de modelos.

  1. Verifico se o campo existe

  2. Verifico se o campo é requerido

  3. Verifico se o campo é unique

  4. Verifico o tipo do campo: String, Int, Bool

Isso é o básico de um teste. Lógico que você pode aumentar a sua gama verificando tamanho da string, validadores, mensagens de erro e etc…

Agora vamos la. Na nossa pasta tests, vamos criar um novo diretório users e criar um módulo chamado test_models.py. Ficando com essa estrutura:

.
├── conftest.py
├── home
│   ├── __init__.py
│   └── test_home.py
├── __init__.py
└── users
    ├── __init__.py
    └── test_models.py

Abaixo segue o código auto explicativo nas docstrings:

# Third

from mongoengine import (
    BooleanField,
    StringField,
)

# Apps

from apps.users.models import User


class TestUser:

    def setup_method(self):
        self.data = {
            'email': 'teste1@teste.com', 'password': 'teste123',
            'active': True, 'full_name': 'Teste',
            'cpf_cnpj': '11111111111'
        }

        # Crio uma instancia do modelo User
        self.model = User(**self.data)

    def test_email_field_exists(self):
        """
        Verifico se o campo email existe
        """
        assert 'email' in self.model._fields

    def test_email_field_is_required(self):
        """
        Verifico se o campo email é requirido
        """
        assert self.model._fields['email'].required is True

    def test_email_field_is_unique(self):
        """
        Verifico se o campo email é unico
        """
        assert self.model._fields['email'].unique is True

    def test_email_field_is_str(self):
        """
        Verifico se o campo email é do tipo string
        """
        assert isinstance(self.model._fields['email'], StringField)

    def test_active_field_exists(self):
        assert 'active' in self.model._fields

    def test_active_field_is_default_true(self):
        assert self.model._fields['active'].default is False

    def test_active_field_is_bool(self):
        """
        Verifico se o campo active é booleano
        """
        assert isinstance(self.model._fields['active'], BooleanField)

    def test_full_name_field_exists(self):
        """
        Verifico se o campo full_name existe
        """
        assert 'full_name' in self.model._fields

    def test_full_name_field_is_str(self):
        assert isinstance(self.model._fields['full_name'], StringField)

    def test_all_fields_in_model(self):
        """
        Verifico se todos os campos estão de fato no meu modelo
        """
        fields = [
            'active', 'address', 'cpf_cnpj', 'created', 'email',
            'full_name', 'id', 'password', 'roles'
        ]

        model_keys = [i for i in self.model._fields.keys()]

        fields.sort()
        model_keys.sort()

        assert fields == model_keys

Em seguida basta executarmos nosso comando de testes make test:

$ make test
find . -name '*.pyc' -exec rm --force {} +
find . -name '*.pyo' -exec rm --force {} +
find . | grep -E "__pycache__|.pyc|.DS_Store$" | xargs rm -rf
flake8
pytest --verbose --color=yes
========================================================================== test session starts ==========================================================================
platform linux -- Python 3.6.5, pytest-3.6.1, py-1.5.3, pluggy-0.6.0 -- /workspaces/venvs/flask-api-users/bin/python3
cachedir: .pytest_cache
rootdir: /workspaces/python/flask-api-users, inifile: pytest.ini
plugins: flask-0.10.0, cov-2.5.1
collected 12 items

tests/home/test_home.py::test_index_response_200 PASSED                                                                                                           [  8%]
tests/home/test_home.py::test_home_response_hello PASSED                                                                                                          [ 16%]
tests/users/test_models.py::TestUser::test_email_field_exists PASSED                                                                                              [ 25%]
tests/users/test_models.py::TestUser::test_email_field_is_required PASSED                                                                                         [ 33%]
tests/users/test_models.py::TestUser::test_email_field_is_unique PASSED                                                                                           [ 41%]
tests/users/test_models.py::TestUser::test_email_field_is_str PASSED                                                                                              [ 50%]
tests/users/test_models.py::TestUser::test_active_field_exists PASSED                                                                                             [ 58%]
tests/users/test_models.py::TestUser::test_active_field_is_default_true PASSED                                                                                    [ 66%]
tests/users/test_models.py::TestUser::test_active_field_is_bool PASSED                                                                                            [ 75%]
tests/users/test_models.py::TestUser::test_full_name_field_exists PASSED                                                                                          [ 83%]
tests/users/test_models.py::TestUser::test_full_name_field_is_str PASSED                                                                                          [ 91%]
tests/users/test_models.py::TestUser::test_all_fields_in_model PASSED                                                                                             [100%]

======================================================================= 12 passed in 0.34 seconds =======================================================================
(flask-api-users)

Chegamos ao fim deste artigo. Caso queiram baixar o projeto acesse o repositório no Github. Abraços a todos.

Próximo artigo: Criando usuários