dunossauro / fastapi-do-zero

Curso básico de FastAPI
https://fastapidozero.dunossauro.com/
680 stars 73 forks source link

[Aula 07] Efeito colateral da fixture `user` usando UserFactory #74

Closed brunodelta closed 7 months ago

brunodelta commented 10 months ago

Nota: o titulo dessa issue foi alterado o contexto por atualizações no desenvolvimento da parte 07 do curso Titulo anterior: [Aula 07] Criar nota sobre ordem dos testes afetar o resultado ao usar o factory-boy

Quando implementei as atualizações dos testes usando o factory-boy na rota de atualização das informações do usuário, demorei um tempo para entender porque os testes estavam dando erro por causa do id do usuário, com isso gostaria de fazer uma sugestão para alertar isso para usuários novatos (como eu) com essas implementações mais complexas que a ordem dos testes afetam seu resultado

Nota do ambiente

fast_todo/routes/auth.py ```python # Resto do código... @router.post('/token', response_model=Token) def login_for_access_token( form_data: OAuth2Form, session: Session, ): invalid_access = HTTPException( status_code=400, detail='Email ou senha inválido' ) user = session.scalar(select(User).where(User.email == form_data.username)) if not user: raise invalid_access if not verify_password(form_data.password, user.password): raise invalid_access access_token = create_access_token(data={'sub': user.email}) return {'access_token': access_token, 'token_type': 'bearer'} ```
fast_todo/routes/users.py ```python # Resto do código... @router.put('/{user_id}', status_code=200, response_model=UserPublic) def update_user( user_id: int, user: UserSchema, session: Session, current_user: CurrentUser, ): if current_user.id != user_id: raise HTTPException(status_code=400, detail='Permissões insuficientes') current_user.username = user.username current_user.password = user.password current_user.email = user.email session.commit() session.refresh(current_user) return current_user # Resto do código... ```
tests/conftest.py ```python import factory import pytest from fastapi.testclient import TestClient from sqlalchemy import StaticPool, create_engine from sqlalchemy.orm import sessionmaker from fast_todo.app import app from fast_todo.database import get_session from fast_todo.models import Base, User from fast_todo.security import get_password_hash class UserFactory(factory.Factory): class Meta: model = User id = factory.Sequence(lambda n: n) username = factory.LazyAttribute(lambda obj: f'test{obj.id}') email = factory.LazyAttribute(lambda obj: f'{obj.username}@test.com') password = factory.LazyAttribute(lambda obj: f'{obj.username}@example.com') @pytest.fixture def session(): engine = create_engine( 'sqlite:///:memory:', connect_args={'check_same_thread': False}, poolclass=StaticPool, ) Session = sessionmaker(bind=engine) Base.metadata.create_all(engine) yield Session() Base.metadata.drop_all(engine) @pytest.fixture def client(session): def get_session_override(): return session with TestClient(app) as client: app.dependency_overrides[get_session] = get_session_override yield client app.dependency_overrides.clear() @pytest.fixture def user(session): password = 'secret' user = UserFactory(password=get_password_hash(password)) session.add(user) session.commit() session.refresh(user) user.clean_password = password return user @pytest.fixture def other_user(session): password = 'secret' user = UserFactory(password=get_password_hash(password)) session.add(user) session.commit() session.refresh(user) user.clean_password = password return user @pytest.fixture def token(client, user): response = client.post( '/auth/token', data={'username': user.email, 'password': user.clean_password}, ) json = response.json() return json['access_token'] ```
test/test_users.py ```python from fast_todo.schemas import UserPublic def test_update_user(client, user, token): response = client.put( f'/users/{user.id}', headers={'Authorization': f'Bearer {token}'}, json={ 'username': 'new_user', 'email': 'new@email.com', 'password': 'new_pass', }, ) assert response.status_code == 200 assert response.json() == { 'id': 1, 'username': 'new_user', 'email': 'new@email.com', } def test_update_user_with_wrong_user(client, other_user, token): response = client.put( f'/users/{other_user.id}', headers={'Authorization': f'Bearer {token}'}, json={ 'username': 'invalid_user', 'email': 'invalid_email@example.com', 'password': 'wrong_password', }, ) assert response.status_code == 400 assert response.json() == {'detail': 'Permissões insuficientes'} # Resto do código... ```

Na criação dos meus testes deixava na ordem:

Não tenho certeza se fiz algo de errado, mas só passou nos testes quando deixei na ordem:

brunodelta commented 10 months ago

Atualizei o meu código com a implementação do usuário invalido ao deletar e funcionou com a ordem do código alterada, mas quando fiz a parte de testar a expiração do usuário, ocorreu um problema colateral que quando executo:

(fast-todo-py3.11) PS ...fast-todo> task test tests/test_users.py

# ...

tests/test_users.py::test_update_user PASSED
tests/test_users.py::test_update_user_with_wrong_user PASSED
tests/test_users.py::test_create_user PASSED
tests/test_users.py::test_create_user_but_username_exists PASSED
tests/test_users.py::test_get_users PASSED
tests/test_users.py::test_get_users_with_users PASSED
tests/test_users.py::test_delete_user PASSED
tests/test_users.py::test_delete_user_with_wrong_user PASSED

# ...

Funciona, mas quando executo isso:

(fast-todo-py3.11) PS ...fast-todo> task test

# ...

tests/test_app.py::test_rota_raiz_deve_retornar_200_e_ola_mundo PASSED
tests/test_auth.py::test_get_token PASSED
tests/test_auth.py::test_token_expired_after_time PASSED
tests/test_db.py::test_create_user PASSED
tests/test_security.py::test_jwt PASSED
tests/test_users.py::test_update_user FAILED

======================================================================= FAILURES ======================================================================== 
___________________________________________________________________ test_update_user ____________________________________________________________________ 

client = <starlette.testclient.TestClient object at 0x0000013A8C3F4890>, user = <fast_todo.models.User object at 0x0000013A8C3F7F50>
token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0ZXN0MkB0ZXN0LmNvbSIsImV4cCI6MTcwMzg3NjI3N30.u7Jv1w0DhYHccWSQmpWAXtOWBJoSfrgSHGQeMrXFDhw'        

    def test_update_user(client, user, token):
        response = client.put(
            f'/users/{user.id}',
            headers={'Authorization': f'Bearer {token}'},
            json={
                'username': 'new_user',
                'email': 'new@email.com',
                'password': 'new_pass',
            },
        )

        assert response.status_code == 200
>       assert response.json() == {
            'id': 0,
            'username': 'new_user',
            'email': 'new@email.com',
        }
E       AssertionError: assert {'id': 2, 'username': 'new_user', 'email': 'new@email.com'} == {'id': 0, 'username': 'new_user', 'email': 'new@email.com'} 
E         Common items:
E         {'email': 'new@email.com', 'username': 'new_user'}
E         Differing items:
E         {'id': 2} != {'id': 0}
E         Full diff:
E         - {'email': 'new@email.com', 'id': 0, 'username': 'new_user'}
E         ?                                  ^
E         + {'email': 'new@email.com', 'id': 2, 'username': 'new_user'}
E         ?                                  ^

tests\test_users.py:16: AssertionError

# ...

Não sei se isso ocorre por alguma diferença na ordenação dos testes do windows para linux

Mas sinto que isso não deveria ocorrer, mas não sei se isso é um problema que só ocorreu comigo, já que não vi nenhuma outra issue com esse problema

brunodelta commented 10 months ago

Se precisar de mais informações do código ou testar algo o link dele esta aqui https://github.com/brunodelta/fast-todo/tree/feat-brunodavi-autentificacao-robusta

Ele foi atualizado mais até concluir a parte 7

dunossauro commented 10 months ago

@brunodelta, assim que possível eu vou analisar esse problema.

Muito obrigado pelo feedback :heart:

brunodavi commented 10 months ago

Cabei de testar no linux e ocorreu o mesmo problema, acho que devo ter errado algo no código mesmo

brunodavi commented 10 months ago

Como a cada novo usuário o id é atualizado seria um teste de uma qualidade menor deixar dessa forma?

tests/test_users.py

def test_update_user(client, user, token):
    response = client.put(
        f'/users/{user.id}',
        headers={'Authorization': f'Bearer {token}'},
        json={
            'username': 'new_user',
            'email': 'new@email.com',
            'password': 'new_pass',
        },
    )

    assert response.status_code == 200
    assert response.json() == {
-       'id': 0,
+       'id': user.id,
        'username': 'new_user',
        'email': 'new@email.com',
    }