{"id":109,"date":"2019-09-27T05:17:05","date_gmt":"2019-09-27T05:17:05","guid":{"rendered":"http:\/\/erikscode.space\/?p=109"},"modified":"2019-09-27T05:17:11","modified_gmt":"2019-09-27T05:17:11","slug":"getting-started-with-browser-automation-testing-in-python","status":"publish","type":"post","link":"https:\/\/erikscode.space\/index.php\/2019\/09\/27\/getting-started-with-browser-automation-testing-in-python\/","title":{"rendered":"Getting Started With Browser Automation Testing in Python"},"content":{"rendered":"\n<p>Developers have unit tests to test atomic functionality and integration tests to test system interoperability. Web developers have to take it a step further and test actual browser behavior. This can be done in many ways, but most often it&#8217;s with some implementation of Selenium webdriver and an xUnit testing framework. In this article, I&#8217;m going to show you how to write a basic framework in Python to get your  tests able to interact with a browser.<\/p>\n\n\n\n<!--more-->\n\n\n\n<h2 class=\"wp-block-heading\">What Exactly are we Doing?<\/h2>\n\n\n\n<p>Before we get started, let&#8217;s make sure we know what we&#8217;re doing and why. Browser tests, or end to end tests are exactly what they sound like. You think about the steps a user would take, and you write code that emulates those actions. This isn&#8217;t abstract like an integration test that sets off a chain of API calls in the order a user might trigger, we are actually going to drive a browser with our code.<\/p>\n\n\n\n<p>In our code, we are going to test out the functionality of a multi-language news broadcaster. (It&#8217;s the web page I used to write a JavaScript observer pattern tutorial, the article for that is <a href=\"https:\/\/dev.to\/erikwhiting88\/observer-design-pattern-tutorial-in-javascript-fun-with-a-language-api-21o3\">here<\/a>).<\/p>\n\n\n\n<p>The code we&#8217;re going to write in this article can be found in <a href=\"https:\/\/github.com\/erik-whiting\/LuluTest\/tree\/blog-example\">this GitHub repository<\/a>. We are using my personal LuluTest Python testing framework, but I will leave this branch unchanged for the purposes of this tutorial.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Overview<\/h2>\n\n\n\n<p>Our test framework will have 2 functional components, and a testing component (like, for testing the framework). The two functional components are<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li><strong>Configuration Module:<\/strong> This will consist of a <code>Config<\/code> class that we&#8217;ll use to set things like<ul><li><em>The web driver<\/em> which means what kind of browser will be doing our tests (Chrome, FireFox, etc.).<\/li><li>The URL including the http prefix, subdomain, and port if necessary.<\/li><\/ul><\/li><li><strong>Page Module:<\/strong> In this module, we&#8217;ll have 2 classes<ul><li>The <code>BasePage<\/code> class will have a <code>Config<\/code> element and methods for going to a URL, closing the browser, and selecting elements within a page.<\/li><li>The <code>BaseElement<\/code> class represents elements on the web page we&#8217;re testing and contains helper methods for interacting with the different kinds of elements we could encounter.<\/li><\/ul><\/li><\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Set Up<\/h2>\n\n\n\n<p>To get started, you need a few things. First of all, I&#8217;m using Python 3.6 but I&#8217;ve seen it work with earlier versions. I can almost guarantee it won&#8217;t work with 2.7, but I don&#8217;t know that for sure.<\/p>\n\n\n\n<p>You also need Selenium. Go on over to your terminal and run a <code>pip install selenium<\/code> or do it within the virtual environment once you&#8217;ve started the project.<\/p>\n\n\n\n<p>Finally, we need a webdriver. I&#8217;ll be using Chrome for this tutorial. You can download Chrome webdriver <a href=\"https:\/\/chromedriver.chromium.org\/downloads\">here<\/a> and you need to put the driver&#8217;s exe file in your system&#8217;s path variable. Alternatively, you can pass the path of the driver but you will need to do so any time you see my code say something like <code>webdriver.Chrome()<\/code> or <code>webdriver.Chrome(chrome_options=chrome_options)<\/code>. Just pass the path like another parameter.<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\"><p>Full disclosure regarding passing the path of Chrome driver. This definitely used to be the case, but I cannot actually find anything supporting this claim right now. It&#8217;s possible you don&#8217;t have to pass the webdriver&#8217;s .exe path anymore. If these tests work without you passing the path or putting it in your own environment&#8217;s path variable, please let me know!<\/p><\/blockquote>\n\n\n\n<p>And that should be it! Let&#8217;s get coding.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Whet Your Appetite<\/h2>\n\n\n\n<p>This is actually going to be a bit of a long process, so let me first show you what selenium web driver is capable of. Once you&#8217;ve finished setting up, open up a python terminal and do this:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>>>> from selenium import webdriver\n>>> from selenium.webdriver.common.keys import Keys\n>>> driver = webdriver.Chrome()\n# Give it a second, a browser should pop up\n>>> driver.get(\"http:\/\/www.google.com\")\n>>> search_box = driver.find_element_by_name(\"q\")\n>>> search_box.send_keys(\"python\")\n>>> search_box.send_keys(Keys.RETURN)\n>>> driver.close()<\/code><\/pre>\n\n\n\n<p>Isn&#8217;t that cool??? Basically, today we&#8217;re going to make all of this more robust and quicker to write.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Config Class<\/h2>\n\n\n\n<p>Inside your project, make a Python module called Configs and create a <code>Config.py<\/code> file. <\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\"><p> I wanted to write this article TDD style, but for the sake of brevity, we&#8217;re going to skip tests for the tutorial. I don&#8217;t want you to get bogged down in a war-and-peace size article. I will include the test files at the bottom of this article for anyone interested.<\/p><\/blockquote>\n\n\n\n<p>The purpose of the config class is mostly to handle the URL for the page to be tested as well as the kind of driver we&#8217;ll be using. When we make the actual automated tests, we&#8217;ll need to be able to handle any kind of URL. We could have subdomains or ports. We could also have neither, so let&#8217;s make sure we don&#8217;t make a URL function that returns something like .google.com:<\/p>\n\n\n\n<p>Here&#8217;s what I came up with:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>class Config:\n  def __init__(self):\n    # Set your configuration items here\n    self.driver = 'Chrome'\n    self.headless = False\n    self.base_url = ''\n    self.subdomain = ''\n    self.http_prefix = 'http:\/\/'\n    self.port = ''\n\n  def url(self):\n    full_url = self.base_url\n    if self.subdomain:\n      full_url = self.subdomain + '.' + full_url\n\n    if self.port:\n      full_url = full_url + ':' + self.port\n\n    full_url = self.http_prefix + full_url\n\n    return full_url<\/code><\/pre>\n\n\n\n<p>Nothing super fancy going on here, especially nothing related to testing with a web driver, so I won&#8217;t talk about it too much. Do take note, however, that the <code>driver<\/code> attribute is a string. This is because we&#8217;re going to instantiate the actual driver within the <code>Page<\/code> class.<\/p>\n\n\n\n<p>Also note, I&#8217;ve set most of these attributes myself. This is because the project (LuluTest) is a personal project and at the time of writing, it&#8217;s a bit basic. Feel free to add some more configurability if you&#8217;d like.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">BasePage Class<\/h2>\n\n\n\n<p>Finally, we get to some selenium stuff. The <code>BasePage<\/code> class is responsible for instantiating the actual webdriver class, be it Chrome, Safari, or otherwise. Also, this class will be responsible for going to a URL, closing the browser, and finding elements. Let&#8217;s get started with the constructor:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>from selenium import webdriver\nfrom selenium.webdriver.common.by import By\nfrom Page.BaseElement import BaseElement\nfrom selenium.webdriver.chrome.options import Options\n\n\nclass Page:\n\n  def __init__(self, config, url_extension=''):\n    self.driver = config.driver\n    self.headless = config.headless\n    self.page = self.web_driver()\n    if not url_extension:\n      self.url = config.url()\n    else:\n      self.url = config.url() + '\/' + url_extension\n<\/code><\/pre>\n\n\n\n<p>Let&#8217;s start with the last parameter <code>url_extension<\/code> which we default to nothing. The rest of the URL will be configured with attributes from the <code>config<\/code> object we pass, and users will be able to add something like &#8220;path\/to_page\/being_tested&#8221; if needed. The <code>config<\/code> object also provides the <code>headless<\/code> boolean and <code>driver<\/code> string which is used to configure the <code>web_driver<\/code> attribute, which we call <code>page<\/code>. Here&#8217;s the code for that bit:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>def web_driver(self):\n  if self.driver == 'Chrome':\n    chrome_options = Options()\n    if self.headless:\n      chrome_options.add_argument(\"--headless\")\n    return webdriver.Chrome(chrome_options=chrome_options)\n  elif self.driver == 'Safari':\n    return webdriver.Safari()<\/code><\/pre>\n\n\n\n<p>To set the <code>page<\/code> attribute, we first instantiate an <code>Options<\/code> object, which is passed to the webdriver class constructor and contains the headless option, among other things.<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\"><p>Notice here that the code only does this for Chrome driver because I have never tested this with any other browser. Like I said earlier, this is an early stage project. Feel free to add options for Safari or other browsers, but keep in mind you might have to do some of your own research to keep up with this tutorial.<\/p><\/blockquote>\n\n\n\n<p>The <code>page<\/code> attribute is the <em>true<\/em> webdriver in this class and lets us actually interact with the browser. As such, our <code>go<\/code> and <code>close<\/code> methods will use it.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">def go(self):<br>   self.page.get(self.url)<br><br>def close(self):<br>   self.page.close()<\/pre>\n\n\n\n<p>Nothing special here, but now you know: to make a webdriver object go to a web page, you use <code>webdriver.get(url_of_page)<\/code>. We&#8217;ll see this in action soon. Also, the <code>close<\/code> method closes the browser (really??).<\/p>\n\n\n\n<p>The next thing our <code>BasePage<\/code> class needs is a way to grab elements. But before we get to that, we&#8217;re going to make the <code>BaseElement<\/code> class and then come back to this one.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">BaseElement Class<\/h2>\n\n\n\n<p>This is my favorite part. The <code>BaseElement<\/code> well represent elements on a page such as buttons, input boxes, and other such things. It will also allow us to manipulate those objects, which is obviously a necessity when testing browser features.<\/p>\n\n\n\n<p>Once we&#8217;re on a page, we only need two things to select an element: the property we&#8217;re going to select it by, and the value of that thing. For example, if we want to enter text into an input box with an id of &#8220;username&#8221;, the <code>by<\/code> is &#8220;id,&#8221; and the <code>value<\/code> is &#8220;username&#8221;.<\/p>\n\n\n\n<p>Usually, we do this by writing something like <code>web_driver.find_element(By.ID, \"username\")<\/code> but there&#8217;s one thing we have to worry about.<\/p>\n\n\n\n<p>Webdrivers are <strong><em>fast<\/em><\/strong> as heck. Sometimes our tests will fail because the test is looking for an element that has not yet rendered. The element could load only a split second after the test script tries to get it, but by then it&#8217;s too late, and the test will fail because it couldn&#8217;t find the element. <\/p>\n\n\n\n<p>To combat this, sometimes developers will put in <code>sleep<\/code> commands. Every time you want to do that, I want you to slap yourself in the face with a newspaper and yell  &#8220;no! bad developer!&#8221; <\/p>\n\n\n\n<p>We don&#8217;t use sleeps for two reasons. One, if you have 200 tests that interact with an average of 3 elements per test, and each time you select an element, you sleep for 2 seconds, your test can never run faster than 20 minutes. Two, you actually don&#8217;t know if your sleep is long enough. Maybe that element is having a hard day and takes 4 seconds to render but you only waited for 2. Luckily, there is a solution, so let&#8217;s just jump into the code.<\/p>\n\n\n\n<p>We&#8217;ll start with the constructor and the method that returns an actual web element object we can manipulate:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>from selenium.webdriver.support import expected_conditions as ec\nfrom selenium.webdriver.common.keys import Keys\nfrom selenium.webdriver.support.wait import WebDriverWait\n\n\nclass BaseElement:\n  def __init__(self, by, value, driver, name=''):\n    self.driver = driver\n    self.by = by\n    self.value = value\n    self.name = name\n    self.locator = (self.by, self.value)\n    self.element = self.web_element()\n\n  def web_element(self):\n    return WebDriverWait(self.driver, 10).until(ec.visibility_of_element_located(locator=self.locator))<\/code><\/pre>\n\n\n\n<p>The solution to the problem of slow rendering web elements is in the <code>web_element<\/code> method. Notice we use a method called <code>WebDriverWait<\/code>. This comes from selenium and takes two parameters: a web driver object and a timeout. The timeout is the only time you&#8217;re allowed to hardcode seconds, and this attribute basically says &#8220;ok, after x amount of seconds, we&#8217;re just goonna assume the element isn&#8217;t rendering.&#8221; You can adjust this as you see fit.<\/p>\n\n\n\n<p>We also call the <code>until<\/code> method which takes in an <code>expected_conditions<\/code> object (which we&#8217;re calling <code>ec<\/code> in the code above) property. In this case, we use the property of <code>visibility_of_element_located<\/code> which basically says &#8220;when it shows up on the page&#8221; and pass it the element&#8217;s locator (which we&#8217;ll talk about in a second).<\/p>\n\n\n\n<p>This is all a fancy way of saying &#8220;give me this element as soon as it shows up on the page; if it takes more than 10 seconds, give up.&#8221;<\/p>\n\n\n\n<p>Also notice in the constructor we are creating a tuple called <code>locator<\/code>. This is going to be populated with values that will come from the <code>BasePage<\/code> object, but let&#8217;s talk about it real quick. The <code>by<\/code> parameter will be translated in the <code>BasePage<\/code> object to return an actual <code>By<\/code> object, which could be Id, Xpath, class name, or a few other things. The &#8220;value&#8221; part of this tuple is the value of the Id, Xpath, class name or whatever of the element we&#8217;re trying to find. To reuse the example of the username input box, the locator tuple would be <code>(\"id\", \"username\")<\/code>.<\/p>\n\n\n\n<p>What good is having an element object if we can&#8217;t do stuff to it? Let&#8217;s finish up this class by adding some fairly self explanatory methods and one decorated method:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>def click(self):\n  self.element.click()\n\ndef input_text(self, text):\n  self.element.send_keys(text)\n\ndef clear(self):\n  self.element.clear()\n\ndef clear_text(self):\n  self.element.send_keys(Keys.CONTROL + 'a')\n  self.element.send_keys(Keys.DELETE)\n\ndef select_drop_down(self, index):\n  self.element.select_by_index(index)\n\n@property\ndef text(self):\n  return self.element.text<\/code><\/pre>\n\n\n\n<p> These methods manipulate the object and let us click, add text, and clear the object. One note about the <code>clear_text<\/code> method: The pure <code>clear<\/code> method does not always work the way you would expect, so this method essentially gets inside the element, types control+a (the keyboard shortcut for selecting all), and presses the delete key.<\/p>\n\n\n\n<p>The <code>text<\/code> method lets us evaluate the text in the element and works for input boxes, paragraph elements, and just about anything else that might have text in it. The <code>@property<\/code> line is called a &#8220;decorator&#8221; and to be honest with you, I have no idea what it&#8217;s for. But this code will not work without it. *shrug*<\/p>\n\n\n\n<p>Now let&#8217;s head back to our <code>BasePage<\/code> class and finish it out. We&#8217;re getting close to being done!<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Back to BasePage<\/h2>\n\n\n\n<p>The last part of the <code>BasePage<\/code> class will be the <code>element_by<\/code> method in which we will take in &#8220;indicator&#8221; and &#8220;location&#8221; as arguments. The indicator will be used to get that <code>By<\/code> object we talked about earlier, and the locator will be the value. Here&#8217;s the code:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>def element_by(self, indicator, locator):\n  indicator = indicator.lower()\n  indicator_converter = {\n      \"id\": By.ID,\n      \"xpath\": By.XPATH,\n      \"selector\": By.CSS_SELECTOR,\n      \"class\": By.CLASS_NAME,\n      \"link text\": By.LINK_TEXT,\n      \"name\": By.NAME,\n      \"partial link\": By.PARTIAL_LINK_TEXT,\n      \"tag\": By.TAG_NAME\n  }\n  return BaseElement(indicator_converter.get(indicator), locator, self.page)<\/code><\/pre>\n\n\n\n<p>As you can see, there are quite a few ways to identify an element outside of its Id.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Side Note<\/h3>\n\n\n\n<p>I do want to caution you about one thing regarding selectors. Try to avoid using xpath when you can. The reason for this is because if an element changes places on a screen either by function or because of tweaks in design, your xpath patter has to change, making the test very fragile. Grab by unique IDs when you can.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">One More Thing<\/h2>\n\n\n\n<p>Before we write the test, I want you to write one more thing for the sake of keeping the code readable. Make a package called <code>tests<\/code> and create 2 files, <code>helpers<\/code> and <code>test_feature<\/code>. We&#8217;re going to make one method in the helper class that will keep us from having a six foot wide line of code in our tests later. Here&#8217;s <code>helpers.py<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>from selenium.webdriver.support import expected_conditions as ec\nfrom selenium.webdriver.support.wait import WebDriverWait\n\n\ndef evaluate_element_text(element, text):\n  WebDriverWait(element.driver, 10).until(ec.text_to_be_present_in_element(element.locator, text))\n  return element.text == text\n<\/code><\/pre>\n\n\n\n<p>This is going to take in an element object and some text we expect to have and return true or false. Instead of writing that long line every time we need to evaluate an element&#8217;s text, we&#8217;ll just call this method.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">See it in Action!<\/h2>\n\n\n\n<p>We are officially done building our framework and it&#8217;s time to write our first test script.  Open <code>test_features.py<\/code> and lets get started.<\/p>\n\n\n\n<p>First, we do our imports, then we&#8217;ll do a little set up:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import unittest\nfrom Configs import Config\nfrom Page import BasePage\nfrom tests import helpers as helper\n\n\nclass TestFeature(unittest.TestCase):\n  cf = Config.Config()\n  cf.base_url = 'erikwhiting.com'\n  cf.subdomain = ''\n  cf.base_url += '\/newsOutlet'\n  bp = BasePage.Page(cf)<\/code><\/pre>\n\n\n\n<p>Then, we can write all the tests for that page that we want. For our example, we&#8217;re going to go to the news site, enter &#8220;Hello&#8221; in the input box, click the transmit button, and then make sure the div that gets transmitted to has the word &#8220;Hello&#8221; in it. Behold!<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>def test_write_and_click(self):\n  bp = self.bp\n  bp.go()\n  bp.element_by(\"id\", \"sourceNews\").input_text(\"Hello\")\n  bp.element_by(\"id\", \"transmitter\").click()\n  english_div = helper.evaluate_element_text(bp.element_by(\"id\", \"en1\"), \"Hello\")\n  self.assertTrue(english_div)\n  bp.close()<\/code><\/pre>\n\n\n\n<p>Basically, in the first part we instantiate our <code>Config<\/code> object, and use it to create our <code>BasePage<\/code> object. From there, the test script is pretty easy to write. We <code>go()<\/code> to the URL, get the element with the ID &#8220;sourceNews&#8221; and input &#8220;Hello&#8221; into it. We find the element with the &#8220;transmitter&#8221; ID, and <code>click()<\/code> it. Then we find the element with ID &#8220;en1&#8221;, send it to our helper method along with the text we expect, and then use the <code>unittest<\/code> method <code>assertTrue<\/code> to evaluate it.<\/p>\n\n\n\n<p>In a terminal in your project root, write:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$> python -m unittest tests.test_configs<\/code><\/pre>\n\n\n\n<p>and hit enter. See the browser go!<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Closing Remarks<\/h2>\n\n\n\n<p>Did that seem like a lot of work? Well, it may have been, but there&#8217;s some things to keep in mind. Not only are we getting elements in an efficient non-sleep way, but we are capturing a lot of functionality in just a couple of classes. All the automated tests we write from here on out will be a breeze, and that&#8217;s the real time saver.<\/p>\n\n\n\n<p>One more thing, a bit of a self plug. If you&#8217;re interested in this project, you are more than welcome to make a pull request on its github. The first goal of this project is to be as easy and configurable as possible. The second goal is to then be turned into a testing DSL to allow less technical users to write tests. No pressure though! Just throwing it out there.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The Tests<\/h2>\n\n\n\n<p>Like I said earlier, I really wanted to do this tutorial in a TDD type of way, since I <strong><em>love<\/em><\/strong> TDD, but that would have made this already long article even more so. But, as promised, there are the tests that are included in the GitHub repo:<\/p>\n\n\n\n<p><code>test_config.py<\/code><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import unittest\nfrom Configs import Config\n\n\nclass TestConfigs(unittest.TestCase):\n\n  test_url = 'eriktest.com'\n  test_sub_domain = 'test'\n  test_port = '5000'\n\n  def test_config_returns_basic_url(self):\n    cf = Config.Config()\n    cf.base_url = self.test_url\n    cf.subdomain = ''\n    cf.port = ''\n    self.assertEqual(cf.url(), 'http:\/\/' + self.test_url)\n\ndef test_config_returns_url_with_subdomain(self):\n    cf = Config.Config()\n    cf.base_url = self.test_url\n    cf.subdomain = self.test_sub_domain\n    cf.port = ''\n    self.assertEqual(cf.url(), 'http:\/\/' + self.test_sub_domain + '.' + self.test_url)\n\n  def test_config_returns_url_with_port_only(self):\n    cf = Config.Config()\n    cf.base_url = self.test_url\n\t\tcf.subdomain = ''\n\t\tcf.port = self.test_port\n\t\tself.assertEqual(cf.url(), 'http:\/\/' + self.test_url + ':' + self.test_port)\n\n  def test_config_returns_url_with_port_and_subdomain(self):\n    cf = Config.Config()\n    cf.base_url = self.test_url\n    cf.subdomain = self.test_sub_domain\n    cf.port = self.test_port\n    val_to_test = 'http:\/\/' + self.test_sub_domain + '.' + self.test_url + ':' + self.test_port\n    self.assertEqual(cf.url(), val_to_test)\n<\/code><\/pre>\n\n\n\n<p><code>test_base_page.py<\/code><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import unittest\nfrom Configs import Config\nfrom Page import BasePage\n\n\nclass TestBasePage(unittest.TestCase):\n  cf = Config.Config()\n  cf.driver = 'TestDriver'\n  cf.base_url = 'TestMe.com'\n\ndef test_base_page_returns_config_url(self):\n  bp = BasePage.Page(self.cf)\n  self.assertEqual(bp.url, self.cf.url())\n\ndef test_bast_page_returns_config_url_with_sub_dir(self):\n  bp = BasePage.Page(self.cf, 'about')\n  self.assertEqual(bp.url, self.cf.url() + '\/about')<\/code><\/pre>\n\n\n\n<p><code>test_feature.py<\/code><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import unittest\nfrom Configs import Config\nfrom Page import BasePage\nfrom tests import helpers as helper\n\n\nclass TestFeature(unittest.TestCase):\n  cf = Config.Config()\n  cf.base_url = 'erikwhiting.com'\n  cf.subdomain = ''\n  cf.base_url += '\/newsOutlet'\n  bp = BasePage.Page(cf)\n\ndef test_write_and_click(self):\n  bp = self.bp\n  bp.go()\n  bp.element_by(\"id\", \"sourceNews\").input_text(\"Hello\")\n  bp.element_by(\"id\", \"transmitter\").click()\n  english_div = helper.evaluate_element_text(bp.element_by(\"id\", \"en1\"), \"Hello\")\n  self.assertTrue(english_div)\n  bp.close()\n<\/code><\/pre>\n","protected":false},"excerpt":{"rendered":"<p>Developers have unit tests to test atomic functionality and integration tests to test system interoperability. Web developers have to take it a step further and test actual browser behavior. This can be done in many ways, but most often it&#8217;s with some implementation of Selenium webdriver and an xUnit testing framework. In this article, I&#8217;m&#8230;<\/p>\n","protected":false},"author":1,"featured_media":119,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"nf_dc_page":"","_monsterinsights_skip_tracking":false,"_monsterinsights_sitenote_active":false,"_monsterinsights_sitenote_note":"","_monsterinsights_sitenote_category":0,"jetpack_post_was_ever_published":false,"_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[17],"tags":[18,20,19,10],"class_list":["post-109","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-testing","tag-python","tag-tdd","tag-test-automation","tag-testing"],"aioseo_notices":[],"jetpack_featured_media_url":"https:\/\/i0.wp.com\/erikscode.space\/wp-content\/uploads\/2019\/09\/david-clode-RAfIk-ZKbPk-unsplash.jpg?fit=5886%2C3818&ssl=1","jetpack_sharing_enabled":true,"jetpack_shortlink":"https:\/\/wp.me\/pb90KY-1L","jetpack-related-posts":[{"id":486,"url":"https:\/\/erikscode.space\/index.php\/2021\/09\/28\/improving-browser-automation-tests-how-to-add-microsoft-edge-and-edgedriver-to-linux-ci-systems\/","url_meta":{"origin":109,"position":0},"title":"Improving Browser Automation Tests: How to Add Microsoft Edge and Edgedriver to Linux CI Systems","author":"erik","date":"September 28, 2021","format":false,"excerpt":"Lately, the Microsoft Edge browser has been growing in popularity, recently unseating Firefox as the 3rd most popular web browser and approaching Safari in the number two spot. This means that any web application with a potentially wide user base should include MS Edge tests in its automated test suite.\u2026","rel":"","context":"In &quot;SVC, CI\/CD, and More&quot;","block_context":{"text":"SVC, CI\/CD, and More","link":"https:\/\/erikscode.space\/index.php\/category\/svc-ci-cd-and-more\/"},"img":{"alt_text":"","src":"","width":0,"height":0},"classes":[]},{"id":1559,"url":"https:\/\/erikscode.space\/index.php\/2023\/09\/16\/test-driven-development-with-python-a-primer\/","url_meta":{"origin":109,"position":1},"title":"Test-Driven Development with Python: a Primer","author":"erik","date":"September 16, 2023","format":false,"excerpt":"Making sure the software we build works the way we (and our customers) want it to work is called, unsurprisingly, software testing. Software testing is an enormous topic; indeed, there are entire books, courses, conferences, academic journals, and more about the topic. One can even make a career out of\u2026","rel":"","context":"In &quot;Python&quot;","block_context":{"text":"Python","link":"https:\/\/erikscode.space\/index.php\/category\/language-specific\/python\/"},"img":{"alt_text":"brown python","src":"https:\/\/i0.wp.com\/erikscode.space\/wp-content\/uploads\/2023\/09\/snake-ball-python-python-regius-beauty-53140.jpeg?fit=1200%2C827&ssl=1&resize=350%2C200","width":350,"height":200,"srcset":"https:\/\/i0.wp.com\/erikscode.space\/wp-content\/uploads\/2023\/09\/snake-ball-python-python-regius-beauty-53140.jpeg?fit=1200%2C827&ssl=1&resize=350%2C200 1x, https:\/\/i0.wp.com\/erikscode.space\/wp-content\/uploads\/2023\/09\/snake-ball-python-python-regius-beauty-53140.jpeg?fit=1200%2C827&ssl=1&resize=525%2C300 1.5x, https:\/\/i0.wp.com\/erikscode.space\/wp-content\/uploads\/2023\/09\/snake-ball-python-python-regius-beauty-53140.jpeg?fit=1200%2C827&ssl=1&resize=700%2C400 2x, https:\/\/i0.wp.com\/erikscode.space\/wp-content\/uploads\/2023\/09\/snake-ball-python-python-regius-beauty-53140.jpeg?fit=1200%2C827&ssl=1&resize=1050%2C600 3x"},"classes":[]},{"id":126,"url":"https:\/\/erikscode.space\/index.php\/2019\/10\/14\/example-tdd-workflows\/","url_meta":{"origin":109,"position":2},"title":"Example TDD Workflows","author":"erik","date":"October 14, 2019","format":false,"excerpt":"This article isn't designed to sell you on the benefits of TDD, it is simply a tutorial (in Java and JUnit) to get you acclimated to the typical workflow. TDD can be counter-intuitive, so we'll go slow and keep it simple. Test Driven Development, or TDD, is a development process\u2026","rel":"","context":"In &quot;Testing&quot;","block_context":{"text":"Testing","link":"https:\/\/erikscode.space\/index.php\/category\/testing\/"},"img":{"alt_text":"","src":"https:\/\/i0.wp.com\/erikscode.space\/wp-content\/uploads\/2019\/10\/TDDCycle.jpg?fit=833%2C654&ssl=1&resize=350%2C200","width":350,"height":200,"srcset":"https:\/\/i0.wp.com\/erikscode.space\/wp-content\/uploads\/2019\/10\/TDDCycle.jpg?fit=833%2C654&ssl=1&resize=350%2C200 1x, https:\/\/i0.wp.com\/erikscode.space\/wp-content\/uploads\/2019\/10\/TDDCycle.jpg?fit=833%2C654&ssl=1&resize=525%2C300 1.5x, https:\/\/i0.wp.com\/erikscode.space\/wp-content\/uploads\/2019\/10\/TDDCycle.jpg?fit=833%2C654&ssl=1&resize=700%2C400 2x"},"classes":[]},{"id":232,"url":"https:\/\/erikscode.space\/index.php\/2020\/04\/20\/5-great-programming-books-rarely-mentioned-in-great-programming-books-articles\/","url_meta":{"origin":109,"position":3},"title":"5 Great Programming Books Rarely Mentioned in &#8220;Great Programming Books&#8221; Articles","author":"erik","date":"April 20, 2020","format":false,"excerpt":"Originally posted on dev.to If you search something like \"programming books\" or \"books developers should read\" you will get a lot of articles listing the same 5 or 6 books. In this article, I want to bring to your attention some stellar books on coding that don't get the love\u2026","rel":"","context":"In &quot;Book Reviews and Suggested Reading&quot;","block_context":{"text":"Book Reviews and Suggested Reading","link":"https:\/\/erikscode.space\/index.php\/category\/suggested-reading\/"},"img":{"alt_text":"","src":"","width":0,"height":0},"classes":[]},{"id":1729,"url":"https:\/\/erikscode.space\/index.php\/2023\/09\/25\/how-to-debug-code-with-python-examples\/","url_meta":{"origin":109,"position":4},"title":"How to Debug Code (with Python Examples)","author":"erik","date":"September 25, 2023","format":false,"excerpt":"Often in your programming career, you will inadvertently write flawed code that introduces some fault into your codebase. These faults are called bugs and the activity of fixing bugs is called \u201cdebugging.\u201d Of course, as developers, we try to write correct code every time, but writing bugs is simply a\u2026","rel":"","context":"In &quot;Python&quot;","block_context":{"text":"Python","link":"https:\/\/erikscode.space\/index.php\/category\/language-specific\/python\/"},"img":{"alt_text":"green yellow and red multicolored insect in close up photography","src":"https:\/\/i0.wp.com\/erikscode.space\/wp-content\/uploads\/2023\/09\/canthigaster-cicada-fulgoromorpha-insect-proboscis-60016.jpeg?fit=1200%2C926&ssl=1&resize=350%2C200","width":350,"height":200,"srcset":"https:\/\/i0.wp.com\/erikscode.space\/wp-content\/uploads\/2023\/09\/canthigaster-cicada-fulgoromorpha-insect-proboscis-60016.jpeg?fit=1200%2C926&ssl=1&resize=350%2C200 1x, https:\/\/i0.wp.com\/erikscode.space\/wp-content\/uploads\/2023\/09\/canthigaster-cicada-fulgoromorpha-insect-proboscis-60016.jpeg?fit=1200%2C926&ssl=1&resize=525%2C300 1.5x, https:\/\/i0.wp.com\/erikscode.space\/wp-content\/uploads\/2023\/09\/canthigaster-cicada-fulgoromorpha-insect-proboscis-60016.jpeg?fit=1200%2C926&ssl=1&resize=700%2C400 2x, https:\/\/i0.wp.com\/erikscode.space\/wp-content\/uploads\/2023\/09\/canthigaster-cicada-fulgoromorpha-insect-proboscis-60016.jpeg?fit=1200%2C926&ssl=1&resize=1050%2C600 3x"},"classes":[]},{"id":343,"url":"https:\/\/erikscode.space\/index.php\/2021\/02\/01\/site-update-and-2021-roadmap\/","url_meta":{"origin":109,"position":5},"title":"Site Update and 2021 Roadmap","author":"erik","date":"February 1, 2021","format":false,"excerpt":"Good morning everyone, it's been a while since I've written a blog post. To keep you all informed, I'd like to make a quick post about where I've been and what the plans are for this year. Where Did eriksCodeSpace Go? The year 2020 was a crazy one for me\u2026","rel":"","context":"Similar post","block_context":{"text":"Similar post","link":""},"img":{"alt_text":"","src":"https:\/\/i0.wp.com\/erikscode.space\/wp-content\/uploads\/2021\/02\/map.jpg?fit=1200%2C800&ssl=1&resize=350%2C200","width":350,"height":200,"srcset":"https:\/\/i0.wp.com\/erikscode.space\/wp-content\/uploads\/2021\/02\/map.jpg?fit=1200%2C800&ssl=1&resize=350%2C200 1x, https:\/\/i0.wp.com\/erikscode.space\/wp-content\/uploads\/2021\/02\/map.jpg?fit=1200%2C800&ssl=1&resize=525%2C300 1.5x, https:\/\/i0.wp.com\/erikscode.space\/wp-content\/uploads\/2021\/02\/map.jpg?fit=1200%2C800&ssl=1&resize=700%2C400 2x, https:\/\/i0.wp.com\/erikscode.space\/wp-content\/uploads\/2021\/02\/map.jpg?fit=1200%2C800&ssl=1&resize=1050%2C600 3x"},"classes":[]}],"_links":{"self":[{"href":"https:\/\/erikscode.space\/index.php\/wp-json\/wp\/v2\/posts\/109","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/erikscode.space\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/erikscode.space\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/erikscode.space\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/erikscode.space\/index.php\/wp-json\/wp\/v2\/comments?post=109"}],"version-history":[{"count":0,"href":"https:\/\/erikscode.space\/index.php\/wp-json\/wp\/v2\/posts\/109\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/erikscode.space\/index.php\/wp-json\/wp\/v2\/media\/119"}],"wp:attachment":[{"href":"https:\/\/erikscode.space\/index.php\/wp-json\/wp\/v2\/media?parent=109"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/erikscode.space\/index.php\/wp-json\/wp\/v2\/categories?post=109"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/erikscode.space\/index.php\/wp-json\/wp\/v2\/tags?post=109"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}