{"id":4259,"date":"2016-07-12T01:04:16","date_gmt":"2016-07-12T05:04:16","guid":{"rendered":"https:\/\/qxf2.com\/blog\/?p=4259"},"modified":"2021-02-16T06:20:49","modified_gmt":"2021-02-16T11:20:49","slug":"modify-python-gui-automation-use-pytest","status":"publish","type":"post","link":"https:\/\/qxf2.com\/blog\/modify-python-gui-automation-use-pytest\/","title":{"rendered":"Python+Selenium+pytest tutorial"},"content":{"rendered":"<p>One of our clients recently introduced us to a really nice, Pythonic test runner called <a href=\"http:\/\/pytest.org\/latest\/\">pytest<\/a>. Thank you <strong>Vachan Wodeyar of Kahuna, Inc.<\/strong> for introducing and helping us get started with pytest!<br \/>\n&nbsp;<br \/>\nOur GUI automation framework lacked a good test runner. So it was tough to run all our tests with just one command. It also made reporting results to a CI server somewhat limited. Sigh us! We knew we lacked a good test runner. It was just that the standard options (nose, unittest, etc.) did not fit our thinking. But with pytest, we seem to have found a really good test runner that fits us. So if you are like us, unhappy about the current state of test runners in Python, do consider using pytest.  <\/p>\n<hr>\n<h3>Why this post?<\/h3>\n<p>The documentation for most test runners, are written for people who had the time and resources to start with a test runner. However, integrating a test runner with an existing test suite is a genuine problem that many testers face. We try to address this problem in this post. We&#8217;ll show you how to modify your existing tests to use pytest.<\/p>\n<p>In this post, we&#8217;ll assume the problem before you is this:<br \/>\n1. you already have a set of GUI automation scripts written in Python<br \/>\n2. you want to add a test runner on top of your existing tests<br \/>\n3. your tests do not follow the unit test pattern of asserting every single check<\/p>\n<hr>\n<h3>When do I need a test runner?<\/h3>\n<p>As testers at startups, we rarely get an opportunity to focus on only automation. We split up our time between testing new features and writing automation for the most important features of the application. Even when we write automation, our first tests are of the &#8216;super hero&#8217; variety &#8211; the ones that catch only the &#8216;super villains&#8217;. Why do we do that? Because we have limited time and a <a href=\"http:\/\/martinfowler.com\/bliki\/BroadStackTest.html\">broadstack test<\/a> exercising multiple workflows is an efficient use of time and money. At this stage, it is easy enough to get by without a test runner. Just make the tests flexible with command line options. Provide a way for your developers to run them after every commit. Once your startup gets financially healthy, your test suite will begin to grow &#8211; one test at a time. At some point, you will have a hard time keeping track of every test added. You will also have a hard time communicating what tests your development team should run. Further, the results you post on your CI server will start getting very hard to read and maintain. This is about the time you should start looking for a test runner. <\/p>\n<hr>\n<h3>An existing GUI automation test<\/h3>\n<p>Let us pretend that we have an existing automated test that fills out a form on <a href=\"https:\/\/qxf2.com\/selenium-tutorial-main\">this page<\/a>. The test will fill the name, phone number, email and then click submit. Let us also try running this test against Firefox or the Chrome based on a command line option. Let us also assume we have functional style test. <\/p>\n<p>The initial test script, say <code>fill_example_form.py<\/code> would look something like this-<\/p>\n<pre lang='python'>\r\n\"\"\"\r\nQxf2 Services: Utility script to test example form\r\nNOTE: This is a contrived example that was written up to make this blog post clear\r\nWe do not use this coding pattern at our clients\r\n\"\"\"\r\n\r\nfrom selenium import webdriver\r\nimport sys,time\r\n \r\ndef fill_example_form(browser):\r\n\t\"Test example form\"\r\n \r\n\t#Create an instance of WebDriver\r\n\tif browser.lower() == 'firefox':\r\n            driver = webdriver.Firefox()\r\n\telif browser.lower() == 'chrome':\r\n            driver = webdriver.Chrome()\r\n \r\n\t#Create variables to keep count of pass\/fail\r\n\tpass_check_counter = 0\r\n\ttotal_checks = 0\r\n\r\n        #Visit the tutorial page\r\n\tdriver.get('http:\/\/qxf2.com\/selenium-tutorial-main') \r\n        #Check 1: Is the page title correct?\r\n        if(driver.title==\"Qxf2 Services: Selenium training main\"):\r\n            print (\"Success: Title of the Qxf2 Tutorial page is correct\")\r\n            pass_check_counter += 1\r\n        else:\r\n            print (\"Failed: Qxf2 Tutorial page Title is incorrect\")\r\n\ttotal_checks += 1\r\n\r\n\t#Fill name, email and phone in the example form\r\n\tname_field = driver.find_element_by_xpath(\"\/\/input[@type='name']\")\r\n        name_field.send_keys('Shivahari')\r\n\temail_field = driver.find_element_by_xpath(\"\/\/input[@type='email']\")\r\n        email_field.send_keys('test@qxf2.com')\r\n\tphone_field = driver.find_element_by_xpath(\"\/\/input[@type='phone']\")\r\n        phone_field.send_keys('9999999999')\r\n\tsubmit_button = driver.find_element_by_xpath(\"\/\/button[@type='submit']\") #Click on the Click me button\r\n        submit_button.click()\r\n        time.sleep(5)\r\n        #Check 2: Is the page title correct?\r\n        if(driver.title==\"Qxf2 Services: Selenium training redirect\"):\r\n            print (\"Success: The example form was submitted\")\r\n            pass_check_counter += 1\r\n        else:\r\n            print (\"Failed: The example form was not submitted. Automation is not on the redirect page\")\r\n\ttotal_checks += 1\r\n\t#Quit the browser window\r\n\tdriver.quit() \r\n\r\n\t#Assert if the pass and fail check counters are equal\r\n\tassert total_checks == pass_check_counter \r\n \r\n#---START OF SCRIPT\r\nif __name__=='__main__':\r\n    browser = sys.argv[1] #Note:using sys.argv to keep this example short. We use OptionParser with all our clients \r\n    fill_example_form(browser)\r\n<\/pre>\n<p>Sure, this is a contrived example, but we feel like its simplicity allows us to illustrate our point clearly. <\/p>\n<hr>\n<h3>Setup<\/h3>\n<p>To install the pytest module use:<\/p>\n<pre lang='python'>pip install -U pytest<\/pre>\n<p>To check the pytest version installed use:<\/p>\n<pre lang='python'>py.test --version<\/pre>\n<hr>\n<h3>A brief detour: how pytest gathers tests<\/h3>\n<p>Before you begin making changes to your existing tests, it is worth understanding the convention pytest uses to magically discover your tests and execute them. pytest runs all the files in the current directory and it&#8217;s sub-directories, if the files follow these conventions:<br \/>\na. The test files start with <code>test_*.py<\/code> or end with <code> *_test.py<\/code><br \/>\nb. Test classes prefixed with <code>Test<\/code> that have no __init__ method<br \/>\nc. Test functions or methods prefixed with <code>test_<\/code>  <\/p>\n<hr>\n<h3>Modify the existing test<\/h3>\n<p>Modifying tests to run with pytest is very easy. Just make sure:<br \/>\n1. Your tests follow the naming conventions in the previous section<br \/>\n2. If you don&#8217;t have assert statements, add one at the end of each test method<br \/>\n3. Move your command line options to a separate <code>conftest.py<\/code> file<br \/>\n4. Add empty __init__.py file in the sub-directories that contain tests in them<\/p>\n<h5>Step 1. Modify the naming patterns<\/h5>\n<p>In the previous test,<br \/>\na. change the name of the test file to <code>test_example_form.py<\/code><br \/>\nb. change the method name from <code>fill_example_form<\/code> to <code>test_example_form<\/code><\/p>\n<h5>Step 2. Add an assert statement<\/h5>\n<p>pytest uses the assert statement to figure out if a test Passed or Failed.  If no assert statement is used then the test result shows Passed irrespective of the test outcome<\/p>\n<h5>Step 3. Move your command line options to a separate conftest.py file<\/h5>\n<p>To support command line dependencies for a test, create a <code>conftest.py<\/code> file in the root directory of your code repository. Add the command line options you use to conftest.py. Then create <code>fixtures<\/code> to fetch the value from the config file every time the test is run. Your conftest.py should look something like this:<\/p>\n<pre lang='python'>\r\n#Contents of conftest.py\r\nimport pytest\r\n\r\n#Command line options:\r\n#Example of allowing pytest to accept a command line option\r\ndef pytest_addoption(parser):\r\n    parser.addoption(\"-B\",\"--browser\",\r\n                      dest=\"browser\",\r\n                      default=\"firefox\",\r\n                      help=\"Browser. Valid options are firefox or chrome\")\r\n\r\n#Test arguments:\r\n#Example of populating the argument 'browser' for a test \r\n@pytest.fixture\r\ndef browser():\r\n    \"pytest fixture for browser\"\r\n    return pytest.config.getoption(\"-B\") \r\n<\/pre>\n<p>In our example, our test accepts one command line option: the browser. So let pytest know it should accept a command line option for browser too. Our test method took one argument called <code>browser<\/code>. So create a fixture called <code>browser()<\/code> that to read the value for browser. <\/p>\n<p><b>Note:<\/b> There is a lot more you could do with pytest fixtures. A description of the uses of fixtures would be detailed in future posts.<\/p>\n<h5>4. Add empty __init__.py file in the sub-directories that contain tests in them<\/h5>\n<p>Add empty files called __init__.py to every directory that you want pytest to look for tests in. This is actually optional depending upon your folder structure and what arguments you plan on running pytest with. But if you are starting off with pytest, just perform this step. You can remove the empty __init__.py files once you get a hang of pytest. <\/p>\n<h3>Putting it all together<\/h3>\n<p>For the sake of completeness, this is how our modified test, now named <code>test_example_form.py<\/code> looks like.<\/p>\n<pre lang='python'>\r\n\"\"\"\r\nQxf2 Services: Utility script to test example form\r\nNOTE: This is a contrived example that was written up to make this blog post clear\r\nWe do not use this coding pattern at our clients\r\n\"\"\"\r\n\r\nfrom selenium import webdriver\r\nimport sys,time\r\n \r\ndef test_example_form(browser):\r\n    \"Test example form\"\r\n \r\n    #Create an instance of WebDriver\r\n    if browser.lower() == 'firefox':\r\n            driver = webdriver.Firefox()\r\n    elif browser.lower() == 'chrome':\r\n            driver = webdriver.Chrome()\r\n \r\n    #Create variables to keep count of pass\/fail\r\n    pass_check_counter = 0\r\n    total_checks = 0\r\n\r\n    #Visit the tutorial page\r\n    driver.get('http:\/\/qxf2.com\/selenium-tutorial-main') \r\n    #Check 1: Is the page title correct?\r\n    if(driver.title==\"Qxf2 Services: Selenium training main\"):\r\n        print (\"Success: Title of the Qxf2 Tutorial page is correct\")\r\n        pass_check_counter += 1\r\n    else:\r\n        print (\"Failed: Qxf2 Tutorial page Title is incorrect\")\r\n    total_checks += 1\r\n\r\n    #Fill name, email and phone in the example form\r\n    name_field = driver.find_element_by_xpath(\"\/\/input[@type='name']\")\r\n    name_field.send_keys('Shivahari')\r\n    email_field = driver.find_element_by_xpath(\"\/\/input[@type='email']\")\r\n    email_field.send_keys('test@qxf2.com')\r\n    phone_field = driver.find_element_by_xpath(\"\/\/input[@type='phone']\")\r\n    phone_field.send_keys('9999999999')\r\n    submit_button = driver.find_element_by_xpath(\"\/\/button[@type='submit']\") #Click on the Click me button\r\n    submit_button.click()\r\n    time.sleep(5)\r\n    #Check 2: Is the page title correct?\r\n    if(driver.title==\"Qxf2 Services: Selenium training redirect\"):\r\n        print (\"Success: The example form was submitted\")\r\n        pass_check_counter += 1\r\n    else:\r\n        print (\"Failed: The example form was not submitted. Automation is not on the redirect page\")\r\n    total_checks += 1\r\n    #Quit the browser window\r\n    driver.quit() \r\n\r\n    #Assert if the pass and fail check counters are equal\r\n    assert total_checks == pass_check_counter \r\n\r\n#---START OF SCRIPT\r\nif __name__=='__main__':\r\n    browser = sys.argv[1] #Note:using sys.argv to keep this example short. We use OptionParser with all our clients \r\n    text_example_form(browser)\r\n<\/pre>\n<hr>\n<h3>How to run tests:<\/h3>\n<p>Pull up a terminal, go to the root directory of your code repository and run the py.test command. The simplest way to run all the tests use the command <code>py.test<\/code>. We&#8217;ll show you an example of running the test in verbose mode: <code>py.test -v<\/code><br \/>\n<a href=\"https:\/\/qxf2.com\/blog\/wp-content\/uploads\/2016\/07\/verbose1.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\/2016\/07\/verbose1.png\" alt=\"verbose\" width=\"696\" height=\"128\" class=\"aligncenter size-full wp-image-4328\" srcset=\"https:\/\/qxf2.com\/blog\/wp-content\/uploads\/2016\/07\/verbose1.png 696w, https:\/\/qxf2.com\/blog\/wp-content\/uploads\/2016\/07\/verbose1-300x55.png 300w\" sizes=\"auto, (max-width: 696px) 100vw, 696px\" \/><\/a><br \/>\nThere are many more default options that can be used in pytest. Use <code>py.test -h<\/code> to list the default options, custom options,ini-options and environment variables.<\/p>\n<hr>\n<p>That&#8217;s it! Your tests have been modified to run with pytest. We plan on covering a lot more ground with pytest &#8211; but if you are itching to pick up something specific, do let us know in the comments below.<\/p>\n<p><strong>If you liked this article, learn more <a href=\"https:\/\/qxf2.com\/blog\/about-qxf2\/\">about Qxf2&#8217;s<\/a> testing services for startups.<\/strong><\/p>\n<hr>\n","protected":false},"excerpt":{"rendered":"<p>One of our clients recently introduced us to a really nice, Pythonic test runner called pytest. Thank you Vachan Wodeyar of Kahuna, Inc. for introducing and helping us get started with pytest! &nbsp; Our GUI automation framework lacked a good test runner. So it was tough to run all our tests with just one command. It also made reporting results [&hellip;]<\/p>\n","protected":false},"author":9,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[38,107,18],"tags":[],"class_list":["post-4259","post","type-post","status-publish","format-standard","hentry","category-automation","category-pytest","category-python"],"_links":{"self":[{"href":"https:\/\/qxf2.com\/blog\/wp-json\/wp\/v2\/posts\/4259","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=4259"}],"version-history":[{"count":50,"href":"https:\/\/qxf2.com\/blog\/wp-json\/wp\/v2\/posts\/4259\/revisions"}],"predecessor-version":[{"id":15571,"href":"https:\/\/qxf2.com\/blog\/wp-json\/wp\/v2\/posts\/4259\/revisions\/15571"}],"wp:attachment":[{"href":"https:\/\/qxf2.com\/blog\/wp-json\/wp\/v2\/media?parent=4259"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/qxf2.com\/blog\/wp-json\/wp\/v2\/categories?post=4259"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/qxf2.com\/blog\/wp-json\/wp\/v2\/tags?post=4259"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}