TransWikia.com

How to test the GET method that takes parser inputs in flask/flask-restx?

Stack Overflow Asked by Junkrat on January 23, 2021

I am making a flask app using Flask-restx and I take inputs from the user by request parsing as follows:

from flask_restx import Resource, reqparse
from .services.calculator import DimensionCalculator
parser = reqparse.RequestParser()
parser.add_argument("dimensions", type=float,
                    required=True,
                    action='split',
                    help="Dimensions of the rectangle (in meters)")
parser.add_argument("angle_inclination", type=float,
                    required=True,
                    action='append',
                    help="Angle of inclination of the Dachfläche (Neigung)")
@ns.route("/output")
class UserOutput(Resource):
    @ns.expect(parser, validation=True)
    def get(self):
        args = parser.parse_args()
        return DimensionCalculator.inputs(**args)

where ns is a namespace I have defined and the simplified version of DimensionCalculator.inputs is:

class DimensionCalculator:
    def inputs(**user_input):
        installation_place = user_input['installation_place']
        num_rectangles = user_input['num_rectangles']
        dimensions = user_input['dimensions']
        angle_inclination = user_input['angle_inclination']
        alignment = user_input['alignment']
        direction = user_input['direction']
        vendor = user_input['vendor']
        output = {
                    "installation_place": installation_place,
                    "num_rectangles": num_rectangles,
                    "area_shape": area_shape,
                    "vendor": vendor
                }
        return output

I am writing tests using pytest. I have written the tests for all the classes and methods and the only one that I am unable to test is the GET method defined in the UserOutput. Is there a way to test the GET method?

Any help is appreciated.

One Answer

Considering unit-testing tag, I'll present what I came up with on how you could test it in total isolation. Basically, get method makes two function calls on dependencies, so in unit sense, you have to check if these calls have indeed been made, as well as assert the arguments, right?

Project structure for purpose of the example:

+---Project
|   |   
|   |   __init__.py
|   |   config.py
|   |   dimension_calculator.py
|   |   parser_impl.py
|   |   user_output.py
|   |   user_output_test.py

So, everything is flat for simplicity.

Most importantly, you have to decouple your UserOutput module from dependencies. You shouldn't be hard-coding dependencies like that:

from .services.calculator import DimensionCalculator

Hypothetically, DimensionCalculator could contain complex business logic which shouldn't be in scope of the test. So, here's how the UserOutput module could look like:

from flask_restx import Resource, Api
from flask import Flask
from .config import Config

app = Flask(__name__)
api = Api(app)
ns = api.namespace('todos', description='TODO operations')

@ns.route("/output")
class UserOutput(Resource):
    @ns.expect(Config.get_parser_impl(), validation=True)
    def get(self):
        args = Config.get_parser_impl().parse_args()
        return Config.get_dimension_calculator_impl().inputs(**args)


if __name__ == '__main__':
    app.run(debug=True)

As you can see, "external" dependencies can now be easily stubbed (this is part of a common pattern called dependency injection). Config module looks as follows:

from .parser_impl import parser
from .dimension_calculator import DimensionCalculator


class Config(object):
    parser_impl = parser
    calculator = DimensionCalculator

    @staticmethod
    def configure_dimension_calculator_impl(impl):
        Config.calculator = impl

    @staticmethod
    def configure_parser_impl(impl):
        Config.parser_impl = impl

    @staticmethod
    def get_dimension_calculator_impl():
        return Config.calculator

    @staticmethod
    def get_parser_impl():
        return Config.parser_impl

Last, but not least, is the place where we'll be stubbing the dependencies and injecting them:

from .user_output import UserOutput
from flask import Flask
from .config import Config

class ParserStub(object):
    parse_args_call_count = 0
    @staticmethod
    def parse_args():
        ParserStub.parse_args_call_count = ParserStub.parse_args_call_count + 1
        return {'parser_stub': 2}

class DimensionCalculatorStub(object):
    inputs_call_count = 0
    @staticmethod
    def inputs(**args):
        DimensionCalculatorStub.inputs_call_count = DimensionCalculatorStub.inputs_call_count + 1
        return {'stub': 1}

app = Flask(__name__)

def test_user_request_get():
    with app.test_request_context():
        # given
        Config.configure_dimension_calculator_impl(DimensionCalculatorStub)
        Config.configure_parser_impl(ParserStub)
        uo = UserOutput()
        
        # when
        uo.get()
        
        # then
        assert DimensionCalculatorStub.inputs_call_count == 1
        assert ParserStub.parse_args_call_count == 1
        # assert arguments as well!

Test passes in my case. One thing missing is validation of arguments.

For completeness, I'll also include DimensionCalculator and the parser itself, though they are exactly the same as in your example. I've only modularized them:

from flask_restx import reqparse

parser = reqparse.RequestParser()
parser.add_argument("dimensions", type=float,
                    required=True,
                    action='split',
                    help="Dimensions of the rectangle (in meters)")
parser.add_argument("angle_inclination", type=float,
                    required=True,
                    action='append',
                    help="Angle of inclination of the Dachfläche (Neigung)")

and the dimension_calculator.py:

class DimensionCalculator:
    @staticmethod
    def inputs(**user_input):
        installation_place = user_input['installation_place']
        num_rectangles = user_input['num_rectangles']
        dimensions = user_input['dimensions']
        angle_inclination = user_input['angle_inclination']
        alignment = user_input['alignment']
        direction = user_input['direction']
        vendor = user_input['vendor']
        output = {
                    "installation_place": installation_place,
                    "num_rectangles": num_rectangles,
                    "area_shape": "EMPTY",
                    "vendor": vendor
                }
        return output

Important definitely there are dedicated frameworks for such stubs/mocks preparation and configuration (for example: https://pypi.org/project/pytest-mock/). I just wanted to present the concept and easiest approach possible.

Correct answer by Marek Piotrowski on January 23, 2021

Add your own answers!

Ask a Question

Get help from others!

© 2024 TransWikia.com. All rights reserved. Sites we Love: PCI Database, UKBizDB, Menu Kuliner, Sharing RPP