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)