Javascript makes me weep

Every non-trivial site needs blackbox testing to ensure that every new iteration leaves it in a fully functional state. And front-end testing may become really tedious, really fast. That’s where automated front-end testing can save you a lot of resources and streamline project iteration. So, which tools do we use for this task ?

Selenium Webdriver

Selenium Webdriver controls an instance of an actual browser and can receive user input from other programs through Selenium API and send them to the browser. This is exactly the functionality we need to write tests that will imitate user input and validate output for your site. In this post, we will use Python as example, but Selenium Webdriver API is available for most of the programming languages on the market.

Docker

As good as it is, testing with Selenium can be further enhanced with Docker. Docker is a powerful containerization tool available on most platforms which can simultaneously support multiple environments with varying levels of isolation. Docker can help us in several ways. First, it simplifies and speeds up Selenium maintanence. Second, it simplifies and streamlines test enviroment initialization and cleanup between test cases. Third, it lets us easily run tests in parallel on a single machine. Let’s talk about these topics in more detail. With docker, you can pretty much forget about Selenium, always having the latest version becomes even easier than with Selenium installed into the system. Update the docker image, and you’re done. During tests, you can run and stop the containers so your test cases always have a side effect free, clean environment to run in, with a very simple and streamlined procedure for cleanup and initialization. You can also run multiple containers at once, each of them supporting one test case per running container.

Testing

Now let’s try and make a simple test case to demonstrate the whole process.

First, we will need a stupid simple HTML page to play with:

<html lang="en">
<head>
    <script type="text/javascript">
        function hello(){alert(document.getElementById("field1").value);}
        function slow_text(){setTimeout(function(){
            document.getElementById("field2").value = "New text !";}, 1500);}
    </script>
</head>
<body>
    <p>This is a simple html page to showcase Selenium WebDriver</p>
    <form>
        <fieldset>
            <legend>form elements:</legend>
            <label for="field1">Field 1:</label>
            <input type="text" name="field1" id="field1"><br>
            <label for="button1">Button 1:</label>
            <input type="button" name="button1" id="button1"
                   value="Press me" onclick="hello()"><br>
            <label for="field2">Field 2:</label>
            <input type="text" name="field2" id="field2" value="This text will change"><br>
            <label for="button2">Button 2:</label>
            <input type="button" name="button2" id="button2"
                   value="Press me to change text" onclick="slow_text()"><br>
        </fieldset>
    </form>
    <a href="https://www.google.com">Link to find by text</a>
</body>
</html>

Then, we’ll write a simple test in Python 3, to showcase the workflow. Let’s start with setting up an environment for our test.

import subprocess
from time import sleep

import docker
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
from selenium.webdriver.support import expected_conditions
from selenium.webdriver.support.ui import WebDriverWait

docker_client = docker.from_env()
# test setup
container = docker_client.containers.run(
    image='selenium/standalone-chrome-debug:3.11.0-bismuth',
    name='chrome_debug',
    ports={5900: 5900, 4444: 4444, },
    volumes={'/dev/shm': {'bind': '/dev/shm', 'mode': 'rw'}, },
    network_mode='host',
    auto_remove=True,
    detach=True)
sleep(1)  # waiting for selenium to come online
http_serv = subprocess.Popen(['python', '-m', 'http.server', '8800', ])
webdriver = webdriver.Remote('http://127.0.0.1:4444/wd/hub',
                             desired_capabilities=DesiredCapabilities.CHROME)

Here, we connected to docker and told it to launch a container with Selenium. Then, we give Seleniu WebDriver some time to start accepting connections and, run a simple http server to serve our simple html page and connect to our Selenium WebDriver instance.

Now, let’s test how our simple HTML page works by exploring it with Selenium.

try:
    webdriver.get('http://localhost:8800/test_subject.html')
    elem = webdriver.find_element_by_link_text('Link to find by text')
    elem.click()
    WebDriverWait(webdriver, 3).until(expected_conditions.url_contains('google.com'))

In the above block we asked Selenium to send a get request to our http server and got back our HTML page. Now we can start poking at it which we did with finding a hyperlink element by its text and then proceed to click it. Our link leads to an external web site and we need to verify that it was opened properly. But the thing is, actions may take some time to execute in the browser and we need to wait for them to complete before proceeding with our inspections. Instead of guessing how much time we need to wait, we can use Selenium wait objects which will poll the page for specific conditions and resume program execution after they were satisfied, or raise an exception if they were timed out. In the last line of code we initiated a wait for browser to open a new page with “google.com” in its url and have also set a timeout of three seconds. We expect url to change because the link we programmatically clicked earlier had “www.google.com” in its href.

Next, we will go back to our page and try interacting with it.

    webdriver.back()
    WebDriverWait(webdriver, 3).until(expected_conditions.url_changes('localhost'))
    webdriver.find_element_by_css_selector('#button2').click()
    WebDriverWait(webdriver, 3).until(expected_conditions.text_to_be_present_in_element_value(
        (By.ID, 'field2', ), 'New text !'))
    webdriver.find_element_by_id('field1').send_keys('sent from Selenium WebDriver')
    webdriver.find_element_by_xpath('//*[@id="button1"]').click()
    WebDriverWait(webdriver, 1).until(expected_conditions.alert_is_present())
    alert = webdriver.switch_to.alert
    assert alert.text == 'sent from Selenium WebDriver'
    alert.accept()

As you can see, Selenium has many different ways to find elements. You can use element’s ids, css selectors, xpath, classes and tags. Also you can emulate clicks and send key inputs. Wait conditions also have all kinds of conditionals, you can wait for element’s values to change, for alerts to pop, for elements to appear or disappear, for element to come into focus and many more.

Finally, we destroy the environment and take a screenshot of the browser’s content.

finally:
    # test teardown
    webdriver.get_screenshot_as_file('test_output.png')
    container.stop()
    http_serv.terminate()

Of course, this is just an example. But it should provide a good idea on how the whole process works. You can put environment initialization in test case’s set up and environment destruction into teardown to have clean environment for each test case.


Rustam Gafurov

Full-stack Developer, Python expert

bitsouls Linkedin