Por: @mariliars
Publicado em: 2018-08-28
Clean Architecture e o Desenvolvimento de Testes Doubles em Python

Uma das etapas da codificação dos produtos iTFLEX é o de desenvolvimento de testes para garantir o funcionamento do código. Atualmente, nossa equipe utiliza o pytest
para escrever testes de código Python
.
A refatoração dos códigos dos produtos iTFLEX para a estrutura Clean Architecture facilitou a implementação dos testes, pois cada camada da aplicação pode implementar um tipo de teste diferente. No entanto, é necessário escolher a abordagem mais adequada para cada camada da aplicação.
Nesse sentido, foi realizado um estudo sobre testes para entender qual a melhor forma de implementá-los.
Estudo
Este estudo incluiu os seguintes critérios de aceitação:
- Pesquisar soluções para simplificar a criação de testes de APIs, casos de uso e repositórios
- Elaborar um protótipo implementando a lógica
Testes Doubles
Teste double é um termo utilizado para casos em que se deseja substituir ou simular um objeto para fins de teste. São divididos nas seguintes categorias:
- Stub: Fornece uma resposta fixa para chamadas de métodos e são independentes dos parâmetros de entrada do método. Enquadrado na abordagem de verificação de estado final do método que está sendo simulado.
- Mock: Simula um objeto completo, no entanto retorna uma resposta fixa para o método que está sendo testado. Além disso, verifica se o método simulado recebeu o parâmetro esperado. Enquadrado na abordagem de verificação de comportamento do objeto que está sendo simulado.
- Fake: É a implementação funcional do objeto. Não é necessário implementar todas as regras do métodos. Conhecido como “funcional para testes, mas incompletos para produção”.
- Spies: São objetos que gravam suas intenções com outros objetos e são utilizados para testar callbacks.
- Dummy: São objetos nulos. Portanto não fazem nada.
Saiba mais sobre testes double em xUnit Patterns - Test Double.
Protótipo
O protótipo foi elaborado considerando a API de usuário. Com o estudo sobre os conceitos de tests doubles percebeu-se que a implementação dos objetos simulados atualmente estão na estrutura de Fake Objects
, no entando esta sendo chamado de Stub
, como mostra o Código 01. Procurando simplificar a implementação destes objetos simulados para testar as APIs, foram desenvolvidos protótipos dos casos de uso de usuário com Stubs
(Código 03 e Código 04).
Código 02: Fake objects dos casos de uso de usuário
# user_fake.py
class UsersUseCasesStub(UsersUseCases):
def __init__(self):
self._last_id = -1
self._data = {}
def _is_unique(self, user: User) -> bool:
for id, db_user in self._data.items():
if id == user.id:
continue
duplicated = False
if user.username == db_user.username:
duplicated = True
if user.email == db_user.email:
duplicated = True
if duplicated:
return False
return True
def reset(self):
self._last_id = -1
self._data = {}
def create_user(self, req: CreateUser) -> UserResp:
user = User(
username=req.username,
fullname=req.fullname,
email=req.email,
password_hash=req.password,
superuser=req.superuser,
)
if not self._is_unique(user):
return UserResp(
status=Status.duplicated,
errors=[
FieldError("username", ErrorTypes.duplicated),
FieldError("email", ErrorTypes.duplicated),
],
)
id = self._last_id + 1
self._last_id = id
user = user.replace(id=id)
user = user.replace(created_at=datetime.now())
user = user.replace(updated_at=datetime.now())
self._data[id] = user
return UserResp(user=user)
Código 02: Implementação dos testes das APIs dos usuário utilizando o fake object do Código 02
# test_user_api.py
@pytest.fixture
def users_uc_stub():
return UsersUseCasesStub()
def test_get_user_by_id(
client_app, headers_user, user_token: str, users_uc_stub: UsersUseCasesStub
):
users_uc_stub.reset()
user = CreateUser(
token=user_token,
username="user01",
fullname="User Test",
email="user01@email.com",
password="teste",
superuser=False,
)
users_uc_stub.create_user(user)
res = client_app.get("/api/users/0", headers=headers_user)
assert res.status_code == HTTPStatus.OK
assert res.json == {
"username": "user01",
"email": "user01@email.com",
"superuser": False,
"fullname": "User Test",
"id": 0,
}
Código 03: Stub dos casos de uso de usuário
# user_stub.py
class UsersUseCasesStub(UsersUseCases):
def __init__(self):
self._resp = None
self._data = {}
def set_resp(self, resp) -> UserResp:
self._resp = resp
return self._resp
def insert_data(self, user) -> UserResp:
self._data[user.id] = user
return self._data
def reset(self):
self._resp = None
self._data = {}
def create_user(self, req: CreateUser) -> UserResp:
return self._resp
# other methods
Código 04: Implementação dos testes das APIs dos usuário utilizando o stub do Código 03
# test_user_api.py
# imports
@pytest.fixture
def users_uc_stub():
return UsersUseCasesStub()
@pytest.fixture
def user() -> User:
return User(
id=0,
username="user01",
fullname="User Test",
email="user01@email.com",
password_hash=passwords.encrypt("admin123"),
superuser=False,
created_at=datetime.datetime(2018, 7, 21, 10, 55, 15),
updated_at=datetime.datetime(2018, 7, 21, 10, 55, 15),
)
@pytest.fixture
def user_resp(user: user) -> UserResp:
return UserResp(user=user)
@pytest.fixture
def user_resp_duplicated() -> UserResp:
errors = [FieldError("email", ErrorTypes.duplicated)]
return UserResp(status=Status.duplicated, errors=errors)
@pytest.fixture
def users_page_resp(user: User) -> UsersPage:
return UsersPage(
users=[user], cursor=0, next_cursor=None, previous_cursor=None
)
def test_create_user_duplicated(
client_app,
headers_user,
user_token: str,
users_uc_stub: UsersUseCasesStub,
user_resp_duplicated: UserResp,
user: User,
):
users_uc_stub.reset()
users_uc_stub.insert_data(user)
users_uc_stub.set_resp(user_resp_duplicated)
res = client_app.post(
"/api/users",
json={
"username": "other_user",
"email": user.email,
"superuser": False,
"fullname": "User Test",
"password": "teste",
},
headers=headers_user,
)
assert res.status_code == HTTPStatus.CONFLICT
assert res.json == {"errors": [{"field": "email", "type": "duplicated"}]}
def test_get_user_by_id(
client_app,
headers_user,
user_token: str,
users_uc_stub: UsersUseCasesStub,
user_resp: UserResp,
user: User,
):
users_uc_stub.reset()
users_uc_stub.set_resp(user_resp)
res = client_app.get("/api/users/0", headers=headers_user)
assert res.status_code == HTTPStatus.OK
assert res.json == {
"id": user.id,
"username": user.username,
"email": user.email,
"superuser": user.superuser,
"fullname": user.fullname,
}
def test_get_users_page(
client_app,
headers_user,
user_token: str,
users_uc_stub: UsersUseCasesStub,
users_page_resp: UsersPage,
user: User,
):
users_uc_stub.reset()
users_uc_stub.set_resp(users_page_resp)
res = client_app.get("/api/users?cursor=0&size=15", headers=headers_user)
assert res.status_code == HTTPStatus.OK
assert res.json == {
"users": [
{
"id": user.id,
"username": user.username,
"email": user.email,
"superuser": user.superuser,
"fullname": user.fullname,
}
],
"cursor": {"current": 0, "next": None, "previous": None},
}
# other tests
Outra alternativa para a implementação dos testes foi utilizar oMock
. O Código 05 e *Código 06 *apresentam, respectivamente, a implementação do Mock
nos testes das APIs e dos casos de uso de usuário.
Código 05: Implementação dos testes das APIs dos usuário utilizando mock dos casos de uso
# test_user_api.py
# imports
@pytest.fixture
def users_uc_mock() -> UsersUseCases:
return Mock(UsersUseCases)
def test_create_user(
client_app,
headers_user,
user_token: str,
user: User,
users_uc_mock: UsersUseCases,
):
users_uc_mock.create_user.return_value = UserResp(user=user)
resp = client_app.post(
"/api/users",
json={
"fullname": user.fullname,
"username": user.username,
"email": user.email,
"password": user.password_hash,
"superuser": user.superuser,
},
headers=headers_user,
)
assert resp.status_code == HTTPStatus.OK
assert resp.json == {
"id": user.id,
"fullname": user.fullname,
"username": user.username,
"email": user.email,
"superuser": user.superuser,
}
users_uc_mock.create_user.assert_called_with(
CreateUser(
token=user_token,
fullname=user.fullname,
username=user.username,
email=user.email,
password=user.password_hash,
superuser=user.superuser,
)
)
# other tests
Código 06: Implementação dos testes dos casos de uso dos usuário utilizando mock do repositório e dos jobs em background
# test_user_use_case.py
# imports
@pytest.fixture
def users_repo_mock() -> UsersRepo:
return Mock(UsersRepo)
@pytest.fixture
def users_jobs_mock() -> UsersJobs:
return Mock(UsersJobs)
@pytest.fixture
def use_cases(
users_repo_mock: UsersRepo, users_jobs_mock: UsersJobs
) -> UsersUseCases:
return UsersUseCases(users_repo_mock, users_jobs_mock)
def db_call_side_effect(param, resp):
def side_effect(user):
assert user.fullname == param.fullname
assert user.username == param.username
assert user.email == param.email
assert user.superuser == param.superuser
assert user.password_hash.startswith("$pbkdf2-sha512$")
return resp
return side_effect
@freeze_time(EXAMPLE_DT)
def test_create_user(
user_token: str,
user: User,
use_cases: UsersUseCases,
users_repo_mock: UsersRepo,
):
request = CreateUser(
token=user_token,
fullname=user.fullname,
username=user.username,
email=user.email,
password=user.password_hash,
superuser=user.superuser,
)
user_resp = UserResp(user=user)
users_repo_mock.insert_user.side_effect = db_call_side_effect(
user, user_resp
)
resp = use_cases.create_user(request)
assert resp.ok
assert resp.user == user
def test_get_user_by_id(
user_token: str,
user: User,
use_cases: UsersUseCases,
users_repo_mock: UsersRepo,
):
users_repo_mock.get_user_by_id.return_value = UserResp(user=user)
request = GetUserById(token=user_token, id=user.id)
resp = use_cases.get_user_by_id(request)
assert resp.ok
assert resp.user == user
users_repo_mock.get_user_by_id.assert_called_with(user.id)
Materiais de apoio: