Testes unitários

Utilizamos o pytest, escrevemos testes unitários para as camadas de UseCase e Repo.

Importante entender

  • Como funciona estrutura do backend
  • Como escrever teste unitário do python
    • Como funciona Mock (return_value, side_effect)
    • Como funciona patch

Testes unitários UseCase

No UseCase temos que adiciona alguns mocks, para isola o UseCase e não executa realmente as operações do repo, execução de comandos no system, publicação de eventos, etc. No UseCase so testamos a operação de sucesso, os testes de erros fica no BussinessRules que será explicado em seguida.

Para funciona precisamos de algumas fixtures:

  • Mock da instância do repo.
from unittest.mock import Mock
from itflex_certs.cas.repo import SQLCAsRepo

@pytest.fixture
def repo() -> Mock:
    return Mock(SQLCAsRepo)
  • Mock da instância do events.
from unittest.mock import Mock
from itflex_certs.cas.events import CAsEvents

@pytest.fixture
def events() -> Mock:
    return Mock(CAsEvents)
  • Mock da instância do audit.
from unittest.mock import Mock
from itflex_audit.action_logs.interfaces import IAuditEvents

@pytest.fixture
def audit() -> Mock:
    return Mock(IAuditEvents)
  • Instância do caso de uso.
from unittest.mock import Mock
from itflex_certs.cas.use_cases import CAsUseCases

@pytest.fixture
def uc(repo: Mock, audit: Mock, events: Mock) -> CAsUseCases:
    return CAsUseCases(repo=repo, audit=audit, events=events)

O que deve ser testado no UseCase

Como temos a classe base para o caso de uso BaseUC, já existem testes unitários implementado. Na maioria dos caso não há necessidade de cria testes unitário para:

  • get_by_id
  • get_all
  • delete
  • get_page

Só deve ser criado teste unitário para:

  • Métodos não implementados no BaseUC, ex: create, update, get_by_name.
  • Métodos implementados pelo BaseUC que foram sobrescritos.

Exemplo

# Código do caso de uso
class CAsUseCases(BaseUC):
    def __init__(
        self, repo: SQLCAsRepo, events: CAsEvents, audit: IAuditEvents
    ):
        super().__init__(repo, events, audit, ca_audit)

    @register_request(CreateCA)
    def create(self, req: CreateCA) -> ItemResp:
        buss = CAsBussiness(uc=self, req=req).check_name().create_ca()

        if not buss.ok:
            return buss.response

        ca_audit(self.audit, req.consumer, buss.ca, "create")
        self.events.created(buss.ca)
        return ItemResp(item=buss.ca)

# Código teste unitário
@freeze_time(datetime.datetime(2018, 7, 21, 10, 55, 15))
def test_create_ca(
    uc: CAsUseCases, repo: SQLCAsRepo, ca: CA, events: CAsEvents, audit: Mock
):
    repo.get_by_name.return_value = ItemResp(status=Status.not_found)
    repo.insert.return_value = ItemResp(item=ca)
    resp = uc.create(
        name="ca01",
        description="CA 01 - Company XYZ",
        country="BR",
        province="Rio de Janeiro",
        city="Rio de Janeiro",
        organization="Company XYZ",
        sector="Support",
        email="company01@email.com",
        expiration=3652,
        consumer=Consumer(),
    )

    assert resp.ok
    assert resp.item == ca
    repo.insert.assert_called_once_with(ca.replace(id=None))

    audit.log.assert_called_once_with(
        CreateActionLog(
            consumer=Consumer(),
            module="certs",
            target_type="ca",
            target_desc="ca01",
            target_id=1,
            action="create",
            payload={
                "id": 1,
                "name": "ca01",
                "description": "CA 01 - Company XYZ",
                "country": "BR",
                "province": "Rio de Janeiro",
                "city": "Rio de Janeiro",
                "organization": "Company XYZ",
                "sector": "Support",
                "email": "company01@email.com",
                "expiry_date": "2028-07-20T23:59:59.999999Z",
                "revoked": False,
            },
        )
    )
    events.created.assert_called_once_with(ca)

Testes unitários BussinessRules

Para funciona precisamos de algumas fixtures:

  • Mock da instância do cas_uc
from unittest.mock import Mock
from itflex_certs.cas.use_cases import CAsUseCases

@pytest.fixture
def cas_uc() -> Mock:
    return Mock(CAsUseCases)
  • Mock da instância do repo.
from unittest.mock import Mock
from itflex_certs.cas.repo import SQLCAsRepo

@pytest.fixture
def repo() -> Mock:
    return Mock(SQLCAsRepo)

O que deve ser testado no BussinessRules

Deve ser testado todas as funções que existir no bussiness.py. Exceto a classe que herda do BussinessRules, essa classe não tem implementação que precisa ser testada.

Exemplo função get_ca

# Código do BussinessRules
def get_ca(buss: BussinessRules, repo: SQLCAsRepo, id: int):
    resp = repo.get_by_id(id)
    if resp.ok:
        buss.ca = resp.item
        return

    buss.add_error(status=Status.not_found)
    buss.exec_continue = False

# Código teste unitário
def test_get_ca(repo: Mock, ca: CA):
    repo.get_by_id.side_effect = [
        ItemResp(item=ca),
        ItemResp(status=Status.not_found),
    ]

    buss = BussinessRules()

    get_ca(buss, repo, 1)
    assert buss.ok
    assert buss.ca == ca

    buss.clean()

    get_ca(buss, repo, 0)
    assert not buss.ok
    assert buss.response == ItemResp(status=Status.not_found, errors=[])

Nos testes do BussinessRules, temos comportamento diferente. No caso da função get_ca tem duas posibilidades que precisam ser testadas, uma quando repo encontra o objeto e outra quando não encontra o objeto. Como essas posibilidades são simples juntamos em um unico teste unitário.

Testes unitários Repo

Para os testes unitários do repo, utilizamos o SQLite em memória para simula as operações de banco. Portando nos testes unitários do repo não utilizamos mock, executamos os métodos do repo e comparamos os valores retornados.

Para funciona precisamos de algumas fixtures:

  • Instância do repo que sera testado.
from sqlalchemy.orm import sessionmaker as SessionMaker

@pytest.fixture
def repo(db_sessionmaker: SessionMaker):
    return SQLCAsRepo(db_sessionmaker)
  • A sessão do banco SQLite.
from sqlalchemy.orm import Session, sessionmaker as SessionMaker

@pytest.fixture
def session(db_sessionmaker: SessionMaker) -> Session:
    return db_sessionmaker()

O que deve ser testado no repo

Como temos a classe base para o caso de uso SQLRepoBase, já existem testes unitários implementado. Na maioria dos caso não há necessidade de cria testes unitário para:

  • create
  • update
  • get_by_id
  • get_all
  • delete
  • get_page

Só deve ser criado teste unitário para:

  • Funções que transforma o objeto entities para objeto do banco, e objeto do banco para entities.
  • Métodos não implementados no SQLRepoBase, ex: get_by_name, revoke.
  • Métodos implementados pelo SQLRepoBase que foram sobreescritos.

Exemplo transformação de objeto

from itflex_certs.cas.repo import ca_to_db, db_to_ca


def test_ca_to_db(ca: CA, db_ca: SQLCA):
    result = ca_to_db(ca)
    assert result
    assert result.name == ca.name
    assert result.description == ca.description
    assert result.country == ca.country
    assert result.province == ca.province
    assert result.city == ca.city
    assert result.organization == ca.organization
    assert result.sector == ca.sector
    assert result.email == ca.email
    assert result.expiry_date == as_naive_dt(EXPIRY_DT)
    assert result.created_at == as_naive_dt(EXAMPLE_DT)
    assert result.updated_at == as_naive_dt(EXAMPLE_DT)


def test_db_to_a(ca: CA, db_ca: SQLCA):
    result = db_to_ca(db_ca)
    assert result == ca

Exemplo método get_by_name

Como este método é uma consulta ao banco. Para funciona, precisamos inserir um objeto no banco antes de chama o método do repo.

session.add(db_ca)
session.commit()

def test_get_by_name(repo: SQLCAsRepo, session: Session, ca: CA, db_ca: SQLCA):
    session.add(db_ca)
    session.commit()

    resp = repo.get_by_name("ca01")
    assert resp.ok
    assert resp.item == ca.replace(id=1)