{"id":6804,"date":"2017-12-08T08:53:10","date_gmt":"2017-12-08T13:53:10","guid":{"rendered":"https:\/\/qxf2.com\/blog\/?p=6804"},"modified":"2024-10-16T16:04:41","modified_gmt":"2024-10-16T20:04:41","slug":"easily-maintainable-api-test-automation-framework","status":"publish","type":"post","link":"https:\/\/qxf2.com\/blog\/easily-maintainable-api-test-automation-framework\/","title":{"rendered":"Easily Maintainable API Test Automation Framework"},"content":{"rendered":"<p>For a long time, Qxf2&#8217;s API tests were not really object-oriented. We were relying on (at best) a facade pattern or (at worst) the God class anti-pattern. Our colleague, David Schwartz of Secure Code Warrior, helped us make our API automation framework more object-oriented and easier to maintain. Thanks, Dex, for all the guidance, examples, code, code reviews and for helping us improve our API tests!<\/p>\n<p>We have not yet managed to implement everything we learned from Dex. By no means we are claiming this is a good, &#8216;final version&#8217;! But we felt our API automation framework had improved enough to share at least a skeleton. We genuinely feel that you could benefit from seeing the code at this stage and then improve from here.<\/p>\n<hr \/>\n<p>For this example of Player-Interface layer based API Automation Framework, we have written a sample API application called <strong>cars_app.py<\/strong> using Python Flask and created Python based automation tests. That way, you can follow along easily.<\/p>\n<p>&nbsp;<\/p>\n<h5>Following image represents workflow of the framework, click on it to enlarge<\/h5>\n<p><a href=\"https:\/\/qxf2.com\/blog\/wp-content\/uploads\/2017\/11\/framework_structure.jpg\" data-rel=\"lightbox-image-0\" data-rl_title=\"\" data-rl_caption=\"\" title=\"\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-7529\" src=\"https:\/\/qxf2.com\/blog\/wp-content\/uploads\/2017\/11\/framework_structure.jpg\" alt=\"\" width=\"916\" height=\"262\" srcset=\"https:\/\/qxf2.com\/blog\/wp-content\/uploads\/2017\/11\/framework_structure.jpg 1017w, https:\/\/qxf2.com\/blog\/wp-content\/uploads\/2017\/11\/framework_structure-300x86.jpg 300w, https:\/\/qxf2.com\/blog\/wp-content\/uploads\/2017\/11\/framework_structure-768x220.jpg 768w\" sizes=\"auto, (max-width: 916px) 100vw, 916px\" \/><\/a><\/p>\n<p>We&#8217;ll spend the next few sections looking at each block along with a useful code snippet.<\/p>\n<p>&nbsp;<\/p>\n<h5>Base Mechanize Module<\/h5>\n<p>We use a Python module called Mechanize to make our REST calls. The Base Mechanize module includes wrappers for the Mechanize API calls like GET, POST, DELETE, PUT. These methods return JSON response with status code\u00a0 or\u00a0 error details with status code for further debugging. Requests is another popular Python library that many people prefer. You can easily substitute Mechanize wrappers for Request wrappers.<\/p>\n<p>code snippet for example:<\/p>\n<pre lang=\"python\">    def get(self,url,headers={}):\r\n        \"Mechanize Get request\"\r\n        browser = self.get_browser()\r\n        request_headers = []\r\n        response = {}\r\n        error = {}\r\n        for key, value in headers.iteritems():\r\n            request_headers.append((key, value))\r\n            browser.addheaders = request_headers\r\n        try:\r\n            response = browser.open(mechanize.Request(url))\r\n            response = json.loads(response.read())\r\n        except (mechanize.HTTPError, mechanize.URLError) as e:\r\n            error = e\r\n            if isinstance(e, mechanize.HTTPError):\r\n                error_message = e.read()\r\n                print(\"\\n******\\GET Error: %s %s\" %\r\n                (url, error_message))\r\n            else:\r\n                print(e.reason.args)\r\n            # bubble error back up after printing relevant details\r\n                raise e\r\n\r\n        return {'response':response,'error':error}\r\n\r\n<\/pre>\n<h5><a href=\"#endpoints\">ENDPOINTS LAYER<\/a><\/h5>\n<p>The endpoints layer abstracts the endpoints of the application under test. We create one <u>endpoints\/[$feature]_endpoint.py<\/u> file. Each <u>[$feature]_endpoint.py<\/u> file contains a class that has methods related to all the endpoints for that features. For example, our cars_app.py application has an endpoint (\/register) related to registration. The <u>[$feature]_endpoint.py<\/u> should not contain any business logic or test logic. It should simply be making REST calls to specific endpoints and returning the data it sees. You could choose to return everything that the REST call has in its response or reorder the response with just the bits you want.<\/p>\n<p>code snippet for example<\/p>\n<pre lang=\"python\">\"\"\"\r\nAPI endpoints for Registration \r\n\"\"\"\r\nfrom Base_Mechanize import Base_Mechanize\r\nclass Registration_API_Endpoints(Base_Mechanize):\r\n\t\"Class for registration endpoints\"\r\n\r\n\tdef registration_url(self,suffix=''):\r\n\t\t\"\"\"Append API end point to base URL\"\"\"\r\n\t\treturn self.base_url+'\/register\/'+suffix\r\n\r\n\r\n\tdef register_car(self,url_params,data,headers):\r\n\t\t\"register car \"\r\n\t\turl = self.registration_url('car?')+url_params\r\n\t\tjson_response = self.post(url,data=data,headers=headers)\r\n\t\treturn {\r\n\t\t\t'url':url,\r\n\t\t\t'response':json_response['response'].read()\r\n\t\t}\r\n<\/pre>\n<h5><\/h5>\n<h5>INTERFACE LAYER<\/h5>\n<p>This layer is a composed interface of all the API endpoint classes. In our cars_app.py application we have features related to car details, registration and users.<\/p>\n<p>code snippet for example :<\/p>\n<pre lang=\"python\">\"\"\"\r\nA composed interface for all the API objects\r\nUse the API_Player to talk to this class\r\n\"\"\"\r\nfrom Cars_API_Endpoints import Cars_API_Endpoints\r\nfrom Registration_API_Endpoints import Registration_API_Endpoints\r\nfrom User_API_Endpoints import User_API_Endpoints\r\n\r\nclass API_Interface(Cars_API_Endpoints,Registration_API_Endpoints,User_API_Endpoints):\r\n\t\"A composed interface for the API objects\"\r\n\r\n\tdef __init__(self, url):\r\n\t\t\"Constructor\"\r\n\t\t# make base_url available to all API endpoints\r\n\t\tself.base_url = url\r\n<\/pre>\n<h5><\/h5>\n<h5>PLAYER LAYER<\/h5>\n<p>This is the layer where business logic and test context is maintained. The player layer does the following:<\/p>\n<ul>\n<li>API_Player serves as an interface between Tests and API_Interface<\/li>\n<li>Contains\u00a0several useful wrappers around commonly used combination of actions<\/li>\n<li>Maintains the test context\/state<\/li>\n<\/ul>\n<p>For this example, we are going to add a method <u>get_cars()<\/u> that gets session information, creates headers, makes a call to the API interface&#8217;s <u>get_cars()<\/u>, processes the result and passes it to whoever called it.<\/p>\n<p>code snippet for example :<\/p>\n<pre lang=\"python\">class API_Player(Results):\r\n    \"The class that maintains the test context\/state\"\r\n    \r\n    def __init__(self, url, log_file_path=None):\r\n        \"Constructor\"\r\n        super(API_Player, self).__init__(\r\n            level=logging.DEBUG, log_file_path=log_file_path)\r\n        self.api_obj = API_Interface(url=url)    \r\n\r\n    def set_auth_details(self, username, password):\r\n        \"encode auth details\"\r\n        user = username\r\n        password = password\r\n        b64login = b64encode('%s:%s' % (user, password))\r\n        \r\n        return b64login\r\n\r\n    def set_header_details(self, auth_details=None):\r\n        \"make header details\"\r\n        if auth_details != '' and auth_details is not None:\r\n            headers = {'content-type': 'application\/json',\r\n                       'Authorization': 'Basic %s' % auth_details}\r\n        else:\r\n            headers = {'content-type': 'application\/json'}\r\n\r\n        return headers\r\n\r\n    def get_cars(self, auth_details=None):\r\n        \"get available cars \"\r\n        headers = self.set_header_details(auth_details)\r\n        json_response = self.api_obj.get_cars(headers=headers)\r\n        json_response = json_response['response']\r\n        result_flag = True if json_response['successful'] is True else False\r\n        self.write(msg=\"Fetched cars list:\\n %s\"%str(json_response))\r\n        self.conditional_write(result_flag,\r\n                               positive=\"Fetched cars\",\r\n                               negative=\"Could not fetch cars\")\r\n\r\n        return json_response<\/pre>\n<hr \/>\n<h4>Examples for adding few API Test Cases to Player-Interface pattern based Framework<\/h4>\n<p>In this section, we&#8217;ll show you examples of writing a couple of tests.<\/p>\n<ul>\n<li>Test Case to check registration of the new car<\/li>\n<li>Test Case to check validation error for the given invalid authentication details<\/li>\n<\/ul>\n<p><a href=\"https:\/\/qxf2.com\/blog\/wp-content\/uploads\/2017\/11\/Test-Management.jpg\" data-rel=\"lightbox-image-1\" data-rl_title=\"\" data-rl_caption=\"\" title=\"\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-7426\" src=\"https:\/\/qxf2.com\/blog\/wp-content\/uploads\/2017\/11\/Test-Management.jpg\" alt=\"\" width=\"777\" height=\"636\" srcset=\"https:\/\/qxf2.com\/blog\/wp-content\/uploads\/2017\/11\/Test-Management.jpg 921w, https:\/\/qxf2.com\/blog\/wp-content\/uploads\/2017\/11\/Test-Management-300x245.jpg 300w, https:\/\/qxf2.com\/blog\/wp-content\/uploads\/2017\/11\/Test-Management-768x628.jpg 768w\" sizes=\"auto, (max-width: 777px) 100vw, 777px\" \/><\/a><\/p>\n<h5>Test cases code snippets<\/h5>\n<pre lang=\"python\"> # add cars\r\n        car_details = conf.car_details\r\n        result_flag = test_obj.add_car(car_details=car_details,\r\n                                       auth_details=auth_details)\r\n        test_obj.log_result(result_flag,\r\n                            positive='Successfully added new car with details %s' % car_details,\r\n                            negative='Could not add new car with details %s' % car_details)\r\n        # test for validation http error 403\r\n        result = test_obj.check_validation_error(auth_details)\r\n        test_obj.log_result(not result['result_flag'],\r\n                            positive=result['msg'],\r\n                            negative=result['msg'])   \r\n<\/pre>\n<h5>API_Player<\/h5>\n<pre lang=\"python\">    def register_car(self, car_name, brand, auth_details=None):\r\n        \"register car\"\r\n        url_params = {'car_name': car_name, 'brand': brand}\r\n        url_params_encoded = urllib.urlencode(url_params)\r\n        customer_details = conf.customer_details\r\n        data = customer_details\r\n        headers = self.set_header_details(auth_details)\r\n        json_response = self.api_obj.register_car(url_params=url_params_encoded,\r\n                                                  data=json.dumps(data),\r\n                                                  headers=headers)\r\n        response = json.loads(json_response['response'])\r\n        result_flag = True if response['registered_car']['successful'] == True else False\r\n\r\n        return result_flag\r\n\r\n    def check_validation_error(self, auth_details=None):\r\n        \"verify validatin error 403\"\r\n        result = self.get_user_list(auth_details)\r\n        user_list = result['user_list']\r\n        response_code = result['response_code']\r\n        result_flag = False\r\n        msg = ''\r\n\r\n        \"verify result based on user list and response code\"\r\n        if user_list is None and response_code == 403:\r\n            msg = \"403 FORBIDDEN: Authentication successful but no access for non admin users\"\r\n\r\n        elif response_code == 200:\r\n            result_flag = True\r\n            msg = \"successful authentication and access permission\"\r\n\r\n        elif response_code == 401:\r\n            msg = \"401 UNAUTHORIZED: Authenticate with proper credentials OR Require Basic Authentication\"\r\n\r\n        elif response_code == 404:\r\n            msg = \"404 NOT FOUND: URL not found\"\r\n\r\n        else:\r\n            msg = \"unknown reason\"\r\n\r\n        return {'result_flag': result_flag, 'msg': msg}<\/pre>\n<p><a href=\"https:\/\/qxf2.com\/blog\/wp-content\/uploads\/2017\/11\/framework_dir_structure-1.jpg\" data-rel=\"lightbox-image-2\" data-rl_title=\"\" data-rl_caption=\"\" title=\"\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-7525\" src=\"https:\/\/qxf2.com\/blog\/wp-content\/uploads\/2017\/11\/framework_dir_structure-1.jpg\" alt=\"\" width=\"749\" height=\"433\" srcset=\"https:\/\/qxf2.com\/blog\/wp-content\/uploads\/2017\/11\/framework_dir_structure-1.jpg 876w, https:\/\/qxf2.com\/blog\/wp-content\/uploads\/2017\/11\/framework_dir_structure-1-300x173.jpg 300w, https:\/\/qxf2.com\/blog\/wp-content\/uploads\/2017\/11\/framework_dir_structure-1-768x444.jpg 768w\" sizes=\"auto, (max-width: 749px) 100vw, 749px\" \/><\/a><\/p>\n<hr \/>\n<h3><span style=\"text-decoration: underline;\">Running Tests<\/span><\/h3>\n<p>There are two steps to run this test:<\/p>\n<ol>\n<li>Start the cars application<\/li>\n<li>Run the test script<\/li>\n<\/ol>\n<h5>1. Start the cars application<\/h5>\n<p>You simply need to navigate to where you have your cars_app.py file and then run <u><strong>python cars_app<\/strong><\/u>.<br \/>\n<a href=\"https:\/\/qxf2.com\/blog\/wp-content\/uploads\/2017\/12\/cmd_cars_api.png\" data-rel=\"lightbox-image-3\" data-rl_title=\"\" data-rl_caption=\"\" title=\"\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-7823\" src=\"https:\/\/qxf2.com\/blog\/wp-content\/uploads\/2017\/12\/cmd_cars_api.png\" alt=\"\" width=\"800\" height=\"188\" srcset=\"https:\/\/qxf2.com\/blog\/wp-content\/uploads\/2017\/12\/cmd_cars_api.png 800w, https:\/\/qxf2.com\/blog\/wp-content\/uploads\/2017\/12\/cmd_cars_api-300x71.png 300w, https:\/\/qxf2.com\/blog\/wp-content\/uploads\/2017\/12\/cmd_cars_api-768x180.png 768w\" sizes=\"auto, (max-width: 800px) 100vw, 800px\" \/><\/a><\/p>\n<h5>2. Run the test script<\/h5>\n<pre>qxf2-page-object-model\\tests&gt;python test_api_example.py<\/pre>\n<p>If things go well, you should see an output similar to the contents below<br \/>\n<a href=\"https:\/\/qxf2.com\/blog\/wp-content\/uploads\/2017\/12\/cmd_cars_api-1.png\" data-rel=\"lightbox-image-4\" data-rl_title=\"\" data-rl_caption=\"\" title=\"\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-7830\" src=\"https:\/\/qxf2.com\/blog\/wp-content\/uploads\/2017\/12\/cmd_cars_api-1.png\" alt=\"\" width=\"1009\" height=\"518\" srcset=\"https:\/\/qxf2.com\/blog\/wp-content\/uploads\/2017\/12\/cmd_cars_api-1.png 1009w, https:\/\/qxf2.com\/blog\/wp-content\/uploads\/2017\/12\/cmd_cars_api-1-300x154.png 300w, https:\/\/qxf2.com\/blog\/wp-content\/uploads\/2017\/12\/cmd_cars_api-1-768x394.png 768w\" sizes=\"auto, (max-width: 1009px) 100vw, 1009px\" \/><\/a><\/p>\n<hr \/>\n<h3>Code is available on GitHub<\/h3>\n<p>1. Qxf2&#8217;s test automation framework: <a href=\"https:\/\/github.com\/qxf2\/qxf2-page-object-model\">https:\/\/github.com\/qxf2\/qxf2-page-object-model<\/a><br \/>\n2. The cars application: <a href=\"https:\/\/github.com\/qxf2\/cars-api\">https:\/\/github.com\/qxf2\/cars-api<\/a><\/p>\n<p>If you liked this article, learn more <strong><a href=\"https:\/\/qxf2.com\/blog\/about-qxf2\/\">about Qxf2&#8217;s<\/a> testing services for startups.<\/strong><\/p>\n<hr \/>\n<h3>Hire QA consultants from Qxf2<\/h3>\n<p>This post was possible only because of Qxf2&#8217;s commitment to open-source software. Surprised that a testing company continually contributes to open-source? Well, we have always been testers with strong technical skills working in a fully-remote company that has a strong open-source culture. We also offer several unique and <a href=\"https:\/\/qxf2.com\/?utm_source=api-framework-basics&#038;utm_medium=click&#038;utm_campaign=From%20blog\">tailored QA solutions for startups and early-stage products<\/a> that most folks are not aware of.If we sound like the kind of QA consultants you want to work with, reach out today!<\/p>\n<hr \/>\n","protected":false},"excerpt":{"rendered":"<p>For a long time, Qxf2&#8217;s API tests were not really object-oriented. We were relying on (at best) a facade pattern or (at worst) the God class anti-pattern. Our colleague, David Schwartz of Secure Code Warrior, helped us make our API automation framework more object-oriented and easier to maintain. Thanks, Dex, for all the guidance, examples, code, code reviews and for [&hellip;]<\/p>\n","protected":false},"author":6,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[43,38,18],"tags":[],"class_list":["post-6804","post","type-post","status-publish","format-standard","hentry","category-api-testing","category-automation","category-python"],"_links":{"self":[{"href":"https:\/\/qxf2.com\/blog\/wp-json\/wp\/v2\/posts\/6804","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/qxf2.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/qxf2.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/qxf2.com\/blog\/wp-json\/wp\/v2\/users\/6"}],"replies":[{"embeddable":true,"href":"https:\/\/qxf2.com\/blog\/wp-json\/wp\/v2\/comments?post=6804"}],"version-history":[{"count":101,"href":"https:\/\/qxf2.com\/blog\/wp-json\/wp\/v2\/posts\/6804\/revisions"}],"predecessor-version":[{"id":22912,"href":"https:\/\/qxf2.com\/blog\/wp-json\/wp\/v2\/posts\/6804\/revisions\/22912"}],"wp:attachment":[{"href":"https:\/\/qxf2.com\/blog\/wp-json\/wp\/v2\/media?parent=6804"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/qxf2.com\/blog\/wp-json\/wp\/v2\/categories?post=6804"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/qxf2.com\/blog\/wp-json\/wp\/v2\/tags?post=6804"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}