Testes de API

Plano de testes

Os casos de uso que iremos testar:

  • Como um usuário anonimo consigo listar os usuarios e não posso ver o saldo
  • Como um usuário anonimo consigo listar os detalhes de um usuário sem o saldo
  • Como usuário admin consigo atualizar o perfil de um usuário
  • Como um usuário autenticado consigo atualziar meu próprio perfil
  • Como um usuário autenticado não consigo atualizar o perfil de outro usuário
  • Como usuário admin consigo transferir qualquer quantidade de pontos para todos os usuários
  • Como um usuário autenticado consigo tranferir 20 pontos para outro usuário e ver o saldo
  • Como um usuário admin consigo ver o saldo de todos os usuários
  • Como um usuário admin consigo ver todas as transações
  • Como um usuário autenticado consigo ver apenas minhas transações

Pytest

Agora vamos converter os casos de uso em funções de teste com o Pytest.

EDITE tests/test_api.py

import pytest

USER_RESPONSE_KEYS = {"name", "username", "dept", "avatar", "bio", "currency"}
USER_RESPONSE_WITH_BALANCE_KEYS = USER_RESPONSE_KEYS | {"balance"}


@pytest.mark.order(1)
def test_user_list(
    api_client,
    api_client_user1,  # pyright: ignore
    api_client_user2,  # pyright: ignore
    api_client_user3,  # pyright: ignore
):
    """Ensure that all needed users are created and showing on the /user/ API

    NOTE: user fixtures are called just to trigger creation of users.
    """
    users = api_client.get("/user/").json()
    expected_users = ["admin", "user1", "user2", "user3"]
    assert len(users) == len(expected_users)
    for user in users:
        assert user["username"] in expected_users
        assert user["dept"] in ["management", "sales"]
        assert user["currency"] == "USD"
        assert set(user.keys()) == USER_RESPONSE_KEYS


@pytest.mark.order(2)
def test_user_detail(api_client):
    """Ensure that the /user/{username} API is working"""
    user = api_client.get("/user/user1/").json()
    assert user["username"] == "user1"
    assert set(user.keys()) == USER_RESPONSE_KEYS


@pytest.mark.order(3)
def test_update_user_profile_by_admin(api_client_admin):
    """Ensure that admin can patch any user data"""
    data = {"avatar": "https://example.com/avatar.png", "bio": "I am a user1"}
    api_client_admin.patch("/user/user1/", json=data)
    user = api_client_admin.get("/user/user1/").json()
    assert user["avatar"] == data["avatar"]
    assert user["bio"] == data["bio"]


@pytest.mark.order(3)
def test_update_user_profile_by_user(api_client_user2):
    """Ensure that user can patch their own data"""
    data = {"avatar": "https://example.com/avatar.png", "bio": "I am a user2"}
    api_client_user2.patch("/user/user2/", json=data)
    user = api_client_user2.get("/user/user2/").json()
    assert user["avatar"] == data["avatar"]
    assert user["bio"] == data["bio"]


@pytest.mark.order(3)
def test_fail_update_user_profile_by_other_user(api_client_user2):
    """User 2 will attempt to patch User 1 profile and it will fail"""
    response = api_client_user2.patch("/user/user1/", json={})
    assert response.status_code == 403


@pytest.mark.order(4)
def test_add_transaction_for_users_from_admin(api_client_admin):
    """Admin user adds a transaction for all users"""
    usernames = ["user1", "user2", "user3"]

    for username in usernames:
        api_client_admin.post(f"/transaction/{username}/", json={"value": 500})

    for username in usernames:
        user = api_client_admin.get(f"/user/{username}/?show_balance=true").json()
        assert user["balance"] == 500


@pytest.mark.order(5)
def test_user1_transfer_20_points_to_user2(api_client_user1):
    """Ensure that user1 can transfer points to user2"""
    api_client_user1.post("/transaction/user2/", json={"value": 20})
    user1 = api_client_user1.get("/user/user1/?show_balance=true").json()
    assert user1["balance"] == 480

    # user1 can see balance of user2 because user1 is a manager
    user2 = api_client_user1.get("/user/user2/?show_balance=true").json()
    assert user2["balance"] == 520


@pytest.mark.order(6)
def test_user_list_with_balance(api_client_admin):
    """Ensure that admin can see user balance"""
    users = api_client_admin.get("/user/?show_balance=true").json()
    expected_users = ["admin", "user1", "user2", "user3"]
    assert len(users) == len(expected_users)
    for user in users:
        assert user["username"] in expected_users
        assert set(user.keys()) == USER_RESPONSE_WITH_BALANCE_KEYS


@pytest.mark.order(6)
def test_admin_can_list_all_transactions(api_client_admin):
    """Admin can list all transactions"""
    transactions = api_client_admin.get("/transaction/").json()
    assert transactions["total"] == 4


@pytest.mark.order(6)
def test_regular_user_can_see_only_own_transaction(api_client_user3):
    """Regular user can see only own transactions"""
    transactions = api_client_user3.get("/transaction/").json()
    assert transactions["total"] == 1
    assert transactions["items"][0]["value"] == 500
    assert transactions["items"][0]["user"] == "user3"
    assert transactions["items"][0]["from_user"] == "admin"

E para executar os tests podemos ir na raiz do projeto FORA DO CONTAINER

Garantimos que o script de testes é eecutável.

$ chmod +x test.sh

Executamos o script:

$ ./test.sh

[+] Running 3/3
 ⠿ Network dundie-api_default  Created                                                     0.1s
 ⠿ Container dundie-api-db-1   Started                                                     0.7s
 ⠿ Container dundie-api-api-1  Started                                                     1.7s
INFO  [alembic.runtime.migration] Context impl PostgresqlImpl.
INFO  [alembic.runtime.migration] Will assume transactional DDL.
INFO  [alembic.runtime.migration] Running stamp_revision 9aa820fb7f01 -> 
INFO  [alembic.runtime.migration] Context impl PostgresqlImpl.
INFO  [alembic.runtime.migration] Will assume transactional DDL.
INFO  [alembic.runtime.migration] Running upgrade  -> f39cbdb1efa7, initial
INFO  [alembic.runtime.migration] Running upgrade f39cbdb1efa7 -> b0abf3428204, transaction
INFO  [alembic.runtime.migration] Running upgrade b0abf3428204 -> 9aa820fb7f01, ensure_admin_user
===================================== test session starts ======================================
platform linux -- Python 3.10.8, pytest-7.2.0, pluggy-1.0.0 -- /usr/local/bin/python
cachedir: .pytest_cache
rootdir: /home/app/api, configfile: pyproject.toml
plugins: order-1.0.1, anyio-3.6.2
collected 10 items                                                                             

tests/test_api.py::test_user_list PASSED                                                 [ 10%]
tests/test_api.py::test_user_detail PASSED                                               [ 20%]
tests/test_api.py::test_update_user_profile_by_admin PASSED                              [ 30%]
tests/test_api.py::test_update_user_profile_by_user PASSED                               [ 40%]
tests/test_api.py::test_fail_update_user_profile_by_other_user PASSED                    [ 50%]
tests/test_api.py::test_add_transaction_for_users_from_admin PASSED                      [ 60%]
tests/test_api.py::test_user1_transfer_20_points_to_user2 PASSED                         [ 70%]
tests/test_api.py::test_user_list_with_balance PASSED                                    [ 80%]
tests/test_api.py::test_admin_can_list_all_transactions PASSED                           [ 90%]
tests/test_api.py::test_regular_user_can_see_only_own_transaction PASSED                 [100%]

====================================== 10 passed in 4.62s ======================================
[+] Running 3/3
 ⠿ Container dundie-api-api-1  Removed                                                     1.2s
 ⠿ Container dundie-api-db-1   Removed                                                     0.7s
 ⠿ Network dundie-api_default  Removed                                                     0.3s

Se tudo deu certo então todos os testes devem ter passado, caso contrário tente encontrar onde está o erro e corrija antes de prosseguir.


Finalizamos assim a fase 1 do nosso projeto com a maior parte das funcionalidades testadas, vamos partir agora para a fase 2 ->