Python: Flask, Decorators e PyTest

Esse dias tive que fazer uma api que realizava transações de cartões de crédito e fazia diversas validações, como por exemplo, se a quantidade solicitada pelo vendedor é maior do que o limite existente no cartão ou se o cartão está bloqueado.

Levando esses pontos em consideração, temos que efetuar pelo menos 2 testes:

  • Verificar o valor da conta ultrapassa o limite
  • Verificar se o cartão está ativo
  • Verificar se a transação foi aprovada se as verificações acima retornarem falso
  • O JSON que será recebido pela nossa API será conforme abaixo:

    {
      "status": true,
      "number":123456,
      "limit":1000,
      "transaction":{
         "amount":500
       }
    }

    Agora vamos ao código:

    Primeira coisa é instalar as dependências:

    python3 -m pip install pytest flask

    Agora vamos escrever os testes, para isso vou utilizar uma ferramenta chamada Pytest.

    Arquivo: test_app.py

    #!/usr/bin/python3
     
    import os
    import tempfile
     
    import pytest
     
    from app import app
     
    @pytest.fixture
    def client():
        app.config['TESTING'] = True
        client = app.test_client()
     
        yield client
     
    def test_valid_transaction(client):
        card = {
                "status": True,
                "number":123456,
                "limit":1000,
                "transaction":{
                    "amount":500
                }
            }
        rv = client.post("/api/transaction",json=card)    
        assert  True == rv.get_json().get("aprovado")    
        assert  500 == rv.get_json().get("novoLimite")
     
    def test_above_limit(client):
        card = {
                "status": True,
                "number":123456,
                "limit":1000,
                "transaction":{
                    "amount":1500
                }
            }
        rv = client.post("/api/transaction",json=card)    
        assert  False == rv.get_json().get("aprovado")
        assert  "Compra acima do limite" in rv.get_json().get("motivo")
     
    def test_blocked_card(client):
        card = {
                "status": False,
                "number":123456,
                "limit":1000,
                "transaction":{
                    "amount":500
                }
            }
        rv = client.post("/api/transaction",json=card)    
        assert  False == rv.get_json().get("aprovado")
        assert  "Cartao bloqueado" in rv.get_json().get("motivo")

    Agora vamos criar um arquivo chamado app.py que será a nossa API, esse arquivo ainda não está completo, é só para ver se os testes estão funcionando.

    #!/usr/bin/python3
     
    from flask import Flask, request, jsonify
     
    app = Flask(__name__)
     
    @app.route("/api/transaction",methods=["POST"])
    def transacao():
        response = {"aprovado":True,"novoLimite":10}
        return jsonify(response)
     
    if __name__ == '__main__':
        app.run(debug=True)

    Para executar os testes execute o comando:

    pytest

    Obviamente todos os testes vão falhar e o nosso objetivo é fazer eles darem certo.

    Com os testes falhando a saída será parecida com essa:

    ============================================== test session starts ==============================================
    platform linux -- Python 3.6.5, pytest-3.3.2, py-1.5.2, pluggy-0.6.0
    rootdir: /home/alisson, inifile:
    collected 3 items
     
    blog/test_app.py FFF                                                                                      [100%]
     
    =================================================== FAILURES ====================================================
    ____________________________________________ test_valid_transaction _____________________________________________
    client = <FlaskClient <Flask 'app'>>
        def test_valid_transaction(client):        card = {
                    "status": True,
                    "number":123456,
                    "limit":1000,
                    "transaction":{
                        "amount":500
                    }
                }
            rv = client.post("/api/transaction",json=card)
            assert  True == rv.get_json().get("aprovado")
    >       assert  500 == rv.get_json().get("novoLimite")
    E       AssertionError: assert 500 == 10
    E        +  where 10 = <built-in method get of dict object at 0x7f94f5ad81b0>('novoLimite')
    E        +    where <built-in method get of dict object at 0x7f94f5ad81b0> = {'aprovado': True, 'novoLimite': 10}.get
    E        +      where {'aprovado': True, 'novoLimite': 10} = <bound method JSONMixin.get_json of <Response 34 bytes [200 OK]>>()
    E        +        where <bound method JSONMixin.get_json of <Response 34 bytes [200 OK]>> = <Response 34 bytes [200 OK]>.get_json
     
    blog/test_app.py:28: AssertionError
    _______________________________________________ test_above_limit ________________________________________________
     
    client = <FlaskClient <Flask 'app'>>
     
        def test_above_limit(client):
            card = {
                    "status": True,
                    "number":123456,
                    "limit":1000,
                    "transaction":{
                        "amount":1500
                    }
                }
            rv = client.post("/api/transaction",json=card)
    >       assert  False == rv.get_json().get("approved")
    E       AssertionError: assert False == None
    E        +  where None = <built-in method get of dict object at 0x7f94f5a483f0>('approved')
    E        +    where <built-in method get of dict object at 0x7f94f5a483f0> = {'aprovado': True, 'novoLimite': 10}.get
    E        +      where {'aprovado': True, 'novoLimite': 10} = <bound method JSONMixin.get_json of <Response 34 bytes [200 OK]>>()
    E        +        where <bound method JSONMixin.get_json of <Response 34 bytes [200 OK]>> = <Response 34 bytes [200 OK]>.get_json
     
    blog/test_app.py:40: AssertionError
    _______________________________________________ test_blocked_card _______________________________________________
     
    client = <FlaskClient <Flask 'app'>>
     
        def test_blocked_card(client):
            card = {
                    "status": False,
                    "number":123456,
                    "limit":1000,
                    "transaction":{
                        "amount":500
                    }
                }
            rv = client.post("/api/transaction",json=card)
    >       assert  False == rv.get_json().get("approved")
    E       AssertionError: assert False == None
    E        +  where None = <built-in method get of dict object at 0x7f94f5a2cab0>('approved')
    E        +    where <built-in method get of dict object at 0x7f94f5a2cab0> = {'aprovado': True, 'novoLimite': 10}.get
    E        +      where {'aprovado': True, 'novoLimite': 10} = <bound method JSONMixin.get_json of <Response 34 bytes [200 OK]>>()
    E        +        where <bound method JSONMixin.get_json of <Response 34 bytes [200 OK]>> = <Response 34 bytes [200 OK]>.get_json
     
    blog/test_app.py:53: AssertionError
    =========================================== 3 failed in 3.06 seconds ============================================

    Note que falharam no total 3 testes:

  • test_valid_transaction
  • >       assert  500 == rv.get_json().get("novoLimite")
    E       AssertionError: assert 500 == 10
    

    Nesse teste era esperado que o novo limite do cartão fosse 500 e foi retornado 10.

  • test_above_limit
  • >       assert  False == rv.get_json().get("aprovado")
    E       AssertionError: assert False == None
    

    Nesse teste esperado que o valor de aprovado fosse igual a False

  • test_blocked_card
  • >       assert  False == rv.get_json().get("aprovado")
    E       AssertionError: assert False == None
    

    Nesse também era esperado que o valor de aprovado fosse igual a False, pois as transações não podem ser permitidas.

    Agora vamos a aplicação principal.

    Para fazer a validação das transações vou criar um decorator chamado checar_cartao, ele ficará da seguinte forma:

    def checar_cartao(f):
        wraps(f)
        def validacoes(*args, **kwargs):
            dados = request.get_json()
            if not dados.get("status"):
                response = {"aprovado":False,
                "novoLimite":dados.get("limit"),
                "motivo":"Cartao bloqueado"}
                return jsonify(response)
     
            if dados.get("limit") < dados.get("transaction").get("amount"):
                response = {"aprovado":False,
                "novoLimite":dados.get("limit"),
                "motivo":"Compra acima do limite"}
                return jsonify(response)
            return f(*args, **kwargs)
     
        return(validacoes)

    e chamá-lo antes da requisição ser respondida:

    @app.route("/api/transaction",methods=["POST"])
    @checar_cartao
    def transacao():
        // codigo da funcao

    Agora explicando o código acima.

    O Decorator em Python é uma função que retorna uma função, então qual a lógica desse decorator?

    Ao invés de fazer uma série de IFs dentro do código principal da função da minha API, antes da requisição chegar ela é passada pelo decorator que tem a função validacoes, onde nela são verificados o limite do cartão de crédito e o seu status, caso verdadeira as condições é retornada a função jsonify que devolve a transação negada.

    Caso todas as condições sejam falsas, no final temos o return f(*args, **kwargs), que devolve a função original, no caso a função transacao e o fluxo do programa segue normalmente.

    Assim o código do app.py ficou da seguinte maneira:

    #!/usr/bin/python3
     
    from flask import Flask, request, jsonify
    from functools import wraps
     
    app = Flask(__name__)
     
    def checar_cartao(f):
        wraps(f)
        def validacoes(*args, **kwargs):
            dados = request.get_json()
            if not dados.get("status"):
                response = {"aprovado":False,
                "novoLimite":dados.get("limit"),
                "motivo":"Cartao bloqueado"}
                return jsonify(response)
     
            if dados.get("limit") < dados.get("transaction").get("amount"):
                response = {"aprovado":False,
                "novoLimite":dados.get("limit"),
                "motivo":"Compra acima do limite"}
                return jsonify(response)
            return f(*args, **kwargs)
     
        return(validacoes)
     
    @app.route("/api/transaction",methods=["POST"])
    @checar_cartao
    def transacao():
        card = request.get_json()   
        novo_limite = card.get("limit") - card.get("transaction").get("amount")
        response = {"aprovado":True,"novoLimite":novo_limite}
        return jsonify(response)
     
    if __name__ == '__main__':
        app.run(debug=True)

    Faça as alterações no seu código e rode os testes novamente, a saída agora deve ser parecida com a saída abaixo:

    ============================================== test session starts ==============================================
    platform linux -- Python 3.6.5, pytest-3.3.2, py-1.5.2, pluggy-0.6.0
    rootdir: /home/alisson/blog, inifile:
    collected 3 items
     
    test_app.py ...                                                                                           [100%]
     
    =========================================== 3 passed in 0.14 seconds ============================================

    Isso significa que todos os testes passaram.

    É nóis valeu!