{"id":22643,"date":"2024-09-12T10:32:29","date_gmt":"2024-09-12T14:32:29","guid":{"rendered":"https:\/\/qxf2.com\/blog\/?p=22643"},"modified":"2024-09-12T10:32:29","modified_gmt":"2024-09-12T14:32:29","slug":"asynchronous-api-automation-testing-using-qxf2s-framework","status":"publish","type":"post","link":"https:\/\/qxf2.com\/blog\/asynchronous-api-automation-testing-using-qxf2s-framework\/","title":{"rendered":"Asynchronous API Automation testing using Qxf2&#8217;s Framework"},"content":{"rendered":"<p>You can now create asynchronous API automation test using <a href=\"https:\/\/github.com\/qxf2\/qxf2-page-object-model\">Qxf2&#8217;s API Automation Framework<\/a>.<\/p>\n<p>In this post we will go about explaining:<br \/>\n&#8211; Why we need Async?<br \/>\n&#8211; How we modified our Synchronous framework to support Asynchronous HTTP calls<br \/>\n&#8211; How to create an Async API Automation test using our framework<br \/>\n&#8211; Cases where Async is not the right fit<br \/>\n<br \/>\n<strong>Note:<\/strong> This post is mostly about how a few changes to our framework helped make it support running tests asynchronously but if you have not used our API framework and have no idea on how it works you can still follow along to know how to create <a href=\"https:\/\/en.wikipedia.org\/wiki\/Non-blocking_algorithm\">non-blocking<\/a> functions for <a href=\"https:\/\/en.wikipedia.org\/wiki\/Blocking_(computing)\">blocking<\/a> functions and how to create coroutine test functions for <a href=\"https:\/\/docs.pytest.org\/en\/stable\/\">pytest<\/a>.<\/p>\n<hr>\n<h3>Why Async?<\/h3>\n<p>Modern computers equipped with multi cpu cores can perform multiple tasks at the same time. When running an <a href=\"https:\/\/en.wikipedia.org\/wiki\/I\/O_bound\">I\/O bound<\/a> task computers sometime wait on the <code>I\/O operation<\/code> idly. <a href=\"https:\/\/en.wikipedia.org\/wiki\/Async\/await\">Async<\/a> helps reduce this idle wait time by running another task during the wait. Running steps asynchronously in most cases can also help reduce execution time drastically. When we ran 300 API requests synchronously and asynchronously using our framework, we noticed the asynchronous steps ran more than <code>10x<\/code> faster. We find making asynchronous calls for setup steps to be especially useful. <\/p>\n<pre lang=\"python\">\r\n2024-08-28 14:44:12 | WARNING | tests.test_api_sync_example| The total number of sync requests is 300\r\n2024-08-28 14:44:12 | WARNING | tests.test_api_sync_example| The time taken to complete sync call is 90.46587109565735\r\n2024-08-28 14:44:13 | WARNING | test_api_async_example     | The total number of async requests is 300\r\n2024-08-28 14:44:13 | WARNING | test_api_async_example     | The time taken to complete async call is 7.610826253890991\r\n<\/pre>\n<hr>\n<h3>Changes we made to our Framework:<\/h3>\n<p>We have added an image below implementing our framework against a sample <a href=\"http:\/\/github.com\/qxf2\/cars-api\">Cars-API<\/a> application to show you have our Player-Interface API Automation framework works.<\/p>\n<p><a href=\"https:\/\/qxf2.com\/blog\/wp-content\/uploads\/2024\/08\/API_Automation_Framework.png\" data-rel=\"lightbox-image-0\" data-rl_title=\"\" data-rl_caption=\"\" title=\"\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/qxf2.com\/blog\/wp-content\/uploads\/2024\/08\/API_Automation_Framework.png\" alt=\"API Automation Framework\" width=\"683\" height=\"478\" class=\"aligncenter size-full wp-image-22642\" srcset=\"https:\/\/qxf2.com\/blog\/wp-content\/uploads\/2024\/08\/API_Automation_Framework.png 683w, https:\/\/qxf2.com\/blog\/wp-content\/uploads\/2024\/08\/API_Automation_Framework-300x210.png 300w\" sizes=\"auto, (max-width: 683px) 100vw, 683px\" \/><\/a><\/p>\n<p>The following layers form the crux of the framework:<br \/>\n1. Endpoints layer<br \/>\n2. Interface layer<br \/>\n3. Player layer<br \/>\nThe API endpoints in the application are abstracted in the <a href=\"https:\/\/github.com\/qxf2\/qxf2-page-object-model\/blob\/master\/endpoints\/Cars_API_Endpoints.py\">Endpoints layer<\/a>(all the *_API_Endpoints.py modules), the <a href=\"https:\/\/github.com\/qxf2\/qxf2-page-object-model\/blob\/master\/endpoints\/API_Interface.py\">Interface layer<\/a>(the API_Interface.py module) collects all the API endpoint abstractions &#038; the <a href=\"https:\/\/github.com\/qxf2\/qxf2-page-object-model\/blob\/master\/endpoints\/API_Player.py\">Player layer<\/a>(API_Player.py module) houses the business logic to validate the application.<br \/>\nThe <a href=\"https:\/\/github.com\/qxf2\/qxf2-page-object-model\/blob\/master\/endpoints\/Base_API.py\">Base_API<\/a> file that the endpoints abstractions extend to make the HTTP calls using <a href=\"https:\/\/requests.readthedocs.io\/en\/latest\/\">requests<\/a> module only used blocking functions to make GET, POST, PUT &#038; DELETE requests so to start supporting Async API automation test we added non blocking functions to make those requests.<\/p>\n<h5>Blocking GET HTTP method:<\/h5>\n<pre lang=\"python\">\r\ndef get(self, url, headers=None):\r\n    \"Run HTTP Get request against an url\"\r\n    headers = headers if headers else {}\r\n    response = None\r\n    try:\r\n        response = requests.get(url=url,headers=headers)\r\n    except Exception as generalexcep:\r\n        print(f\"Unable to run GET request against {url} due to {generalexcep}\")\r\n    return response\r\n<\/pre>\n<p><strong>Disclaimer: <\/strong>The code in this blog post is intended for demonstration purposes only. We recommend following better coding standards for real-world applications.<\/p>\n<h5>Non-blocking GET HTTP method for the previous synchoronous method:<\/h5>\n<pre lang=\"python\">\r\nasync def async_get(self, url, headers={}):\r\n    \"Run the blocking GET method in a thread\"\r\n    response = await asyncio.to_thread(self.get, url, headers)\r\n    return response\r\n<\/pre>\n<p>Although running code on a separate thread is not asynchronous but using <code>asyncio.to_thread<\/code> allows us to await on the result asynchronously.<br \/>\nWe used <code>asyncio.to_thread<\/code> to add similar <code>non-blocking<\/code> functions for the corresponding blocking functions in the *_API_Endpoints, API_Player modules too.<\/p>\n<hr>\n<h3>Create an Async API Automation test<\/h3>\n<p>A simple test cannot be used to call the <a href=\"https:\/\/en.wikipedia.org\/wiki\/Coroutine\">coroutines<\/a> we added in the API_Player module. The test function needs to be a couroutine too. <a href=\"https:\/\/pypi.org\/project\/pytest-asyncio\/\">pytest-asyncio<\/a> plugin allows creating coroutine test functions.<\/p>\n<pre lang=\"python\">\r\n@pytest.mark.asyncio\r\nasync def test_api_async_example(test_api_obj):\r\n    \"Run api test\"\r\n<\/pre>\n<p>The <code>@pytest.mark.asyncio<\/code> is required for pytest to collect the coroutine as a test.<br \/>\nThe test scenarios are then added as tasks inside the coroutine test function, we used <a href=\"https:\/\/docs.python.org\/3\/library\/asyncio-task.html#id6\">asyncio.TaskGroup<\/a> object to group the test scenarios and run them asynchronously.<\/p>\n<pre lang=\"python\">\r\nasync with asyncio.TaskGroup() as group:\r\n    get_cars = group.create_task(test_api_obj.async_get_cars(auth_details))\r\n    add_new_car = group.create_task(test_api_obj.async_add_car(car_details=car_details,\r\n                                                               auth_details=auth_details))\r\n    get_car = group.create_task(test_api_obj.async_get_car(auth_details=auth_details,\r\n                                                           car_name=existing_car,\r\n                                                           brand=brand))\r\n<\/pre>\n<p>Putting it all together, this is how our sample Async API Automation test for the Cars-API app looks:<\/p>\n<pre lang=\"python\">\r\n\"\"\"\r\nAPI Async EXAMPLE TEST\r\nThis test collects tasks using asyncio.TaskGroup object \\\r\nand runs these scenarios asynchronously:\r\n1. Get the list of cars\r\n2. Add a new car\r\n3. Get a specifi car from the cars list\r\n4. Get the registered cars\r\n\"\"\"\r\n\r\nimport asyncio\r\nimport os\r\nimport sys\r\nimport pytest\r\nsys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))\r\nfrom conf import api_example_conf\r\n\r\n@pytest.mark.asyncio\r\n# Skip running the test if Python version < 3.11\r\n@pytest.mark.skipif(sys.version_info < (3,11),\r\n                    reason=\"requires Python3.11 or higher\")\r\nasync def test_api_async_example(test_api_obj):\r\n    \"Run api test\"\r\n    try:\r\n        expected_pass = 0\r\n        actual_pass = -1\r\n\r\n        # set authentication details\r\n        username = api_example_conf.user_name\r\n        password = api_example_conf.password\r\n        auth_details = test_api_obj.set_auth_details(username, password)\r\n\r\n        # Get an existing car detail from conf\r\n        existing_car = api_example_conf.car_name_1\r\n        brand = api_example_conf.brand\r\n        # Get a new car detail from conf\r\n        car_details = api_example_conf.car_details\r\n\r\n        async with asyncio.TaskGroup() as group:\r\n            get_cars = group.create_task(test_api_obj.async_get_cars(auth_details))\r\n            add_new_car = group.create_task(test_api_obj.async_add_car(car_details=car_details,\r\n                                                                       auth_details=auth_details))\r\n            get_car = group.create_task(test_api_obj.async_get_car(auth_details=auth_details,\r\n                                                                   car_name=existing_car,\r\n                                                                   brand=brand))\r\n            get_reg_cars = group.create_task(test_api_obj.async_get_registered_cars(auth_details=auth_details))\r\n\r\n        test_api_obj.log_result(get_cars.result(),\r\n                                positive=\"Successfully obtained the list of cars\",\r\n                                negative=\"Failed to get the cars\")\r\n        test_api_obj.log_result(add_new_car.result(),\r\n                                positive=f\"Successfully added new car {car_details}\",\r\n                                negative=\"Failed to add a new car\")\r\n        test_api_obj.log_result(get_car.result(),\r\n                                positive=f\"Successfully obtained a car - {existing_car}\",\r\n                                negative=\"Failed to add a new car\")\r\n        test_api_obj.log_result(get_reg_cars.result(),\r\n                                positive=\"Successfully obtained registered cars\",\r\n                                negative=\"Failed to get registered cars\")\r\n        # write out test summary\r\n        expected_pass = test_api_obj.total\r\n        actual_pass = test_api_obj.passed\r\n        test_api_obj.write_test_summary()\r\n        # Assertion\r\n        assert expected_pass == actual_pass,f\"Test failed: {__file__}\"\r\n\r\n    except Exception as e:\r\n        test_api_obj.write(f\"Exception when trying to run test: {__file__}\")\r\n        test_api_obj.write(f\"Python says: {str(e)}\")\r\n\r\n<\/pre>\n<p>We have used <code>@pytest.mark.skipif<\/code> marker to skip running the test for Python versions less than 3.11, the <code>TaskGroup<\/code> object api is available on versions 3.11 and later.<\/p>\n<hr>\n<h3>Scenarios where Async API test is not the right choice<\/h3>\n<p>Though Async API Automation test can help validate an app and reduce the time it takes to run the validations, there are cases where making HTTP calls asynchronously might not be the right approach. Here are a few scenarios where we think synchronous test is a better choice:<br \/>\n- Test where a few scenarios need to run in an order are not a right fit for asynchronous testing, because the order of execution of the coroutines collected as tasks in the test cannot be controlled, hence is susceptible to <a href=\"https:\/\/stackoverflow.com\/questions\/34510\/what-is-a-race-condition\">race condition<\/a>.<br \/>\n- Test with very few scenarios are better off being run synchronously, because the overhead required to collect and run very less scenarios asynchronously minimises its advantage.<br \/>\n- Projects that cannot be maintained regularly. The asyncio Python module has gone through drastic changes with each Python version and hence warrant frequent maintenance.<\/p>\n<hr>\n<h3>References<\/h3>\n<p>1. <a href=\"https:\/\/www.linkedin.com\/advice\/1\/how-do-you-test-apis-use-asynchronous-communication-k4zif\">How do you test APIs that use asynchronous communication?<\/a><br \/>\n2. <a href=\"https:\/\/stackoverflow.com\/questions\/65316863\/is-asyncio-to-thread-method-different-to-threadpoolexecutor\">Using asyncio.to_thread to create non blocking functions<\/a><br \/>\n3. <a href=\"https:\/\/pytest-asyncio.readthedocs.io\/en\/latest\/\">pytest-asyncio<\/a><br \/>\n4. <a href=\"https:\/\/docs.python.org\/3\/library\/asyncio-task.html#id6\">asyncio Task Groups<\/a><\/p>\n<hr>\n<h3>Hire testers from Qxf2<\/h3>\n<p>We at <a href=\"https:\/\/qxf2.com?utm_source=asynchronous_api_automation_testing_using_qxf2_framework&#038;utm_medium=click&#038;utm_campaign=From%20blog\" target=\"_blank\" rel=\"noopener\">Qxf2 services<\/a> make a dedicated effort to constantly evaluate our Automation framework against new frameworks to identify and implement new useful features. You can find our open-source framework on GitHub here - <a href=\"https:\/\/github.com\/qxf2\/qxf2-page-object-model\">Qxf2 Page Object Model Framework<\/a>.<br \/>\nOur expertise extend beyond maintaining our own framework, we have used various other frameworks at clients and even helped maintain a few of them. If you are looking for testers to help create automation tests using our framework or maintain your own automation framework <a href=\"https:\/\/qxf2.com\/contact?utm_source=asynchronous_api_automation_testing_using_qxf2_framework&#038;utm_medium=click&#038;utm_campaign=From%20blog\" target=\"_blank\" rel=\"noopener\">contact Qxf2<\/a>.<\/p>\n<hr>\n","protected":false},"excerpt":{"rendered":"<p>You can now create asynchronous API automation test using Qxf2&#8217;s API Automation Framework. In this post we will go about explaining: &#8211; Why we need Async? &#8211; How we modified our Synchronous framework to support Asynchronous HTTP calls &#8211; How to create an Async API Automation test using our framework &#8211; Cases where Async is not the right fit Note: [&hellip;]<\/p>\n","protected":false},"author":9,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[43,322,107,180],"tags":[],"class_list":["post-22643","post","type-post","status-publish","format-standard","hentry","category-api-testing","category-asyncio","category-pytest","category-requests"],"_links":{"self":[{"href":"https:\/\/qxf2.com\/blog\/wp-json\/wp\/v2\/posts\/22643","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\/9"}],"replies":[{"embeddable":true,"href":"https:\/\/qxf2.com\/blog\/wp-json\/wp\/v2\/comments?post=22643"}],"version-history":[{"count":53,"href":"https:\/\/qxf2.com\/blog\/wp-json\/wp\/v2\/posts\/22643\/revisions"}],"predecessor-version":[{"id":22805,"href":"https:\/\/qxf2.com\/blog\/wp-json\/wp\/v2\/posts\/22643\/revisions\/22805"}],"wp:attachment":[{"href":"https:\/\/qxf2.com\/blog\/wp-json\/wp\/v2\/media?parent=22643"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/qxf2.com\/blog\/wp-json\/wp\/v2\/categories?post=22643"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/qxf2.com\/blog\/wp-json\/wp\/v2\/tags?post=22643"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}