Alisson Machado
20 July 2018

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 = >
    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 = ('novoLimite')
E        +    where  = {'aprovado': True, 'novoLimite': 10}.get
E        +      where {'aprovado': True, 'novoLimite': 10} = >()
E        +        where > = .get_json

blog/test_app.py:28: AssertionError
_______________________________________________ test_above_limit ________________________________________________

client = >

    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 = ('approved')
E        +    where  = {'aprovado': True, 'novoLimite': 10}.get
E        +      where {'aprovado': True, 'novoLimite': 10} = >()
E        +        where > = .get_json

blog/test_app.py:40: AssertionError
_______________________________________________ test_blocked_card _______________________________________________

client = >

    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 = ('approved')
E        +    where  = {'aprovado': True, 'novoLimite': 10}.get
E        +      where {'aprovado': True, 'novoLimite': 10} = >()
E        +        where > = .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!