April 10, 2022

Writing Tests for Web Apps

Learn the basics around writing tests using pytest and selenium!

Writing Tests for Web Apps

Have you ever used a web app that used to work one way, then it was updated, and now it doesn't? Issues like these are very common for all software, and is something that is typically solved by writing unit and integration tests. Writing these tests usually involves importing your code's methods and functions into a testing framework, (like pytest for example) executing them with mocked data, and then making sure that the expected result happened. This is already a time-consuming process that most developers don't enjoy doing—even for back-end software that is a bit easier to import functions from—Add in web apps, the complexity of JavaScript, and the weirdness of various browsers... Before you know it, your in a hot mess. This post goes over the very simple basics of one potential method for testing the web-apps you create or use.

pytest

The Python package pytest is a simple and easy framework for writing tests in native Python. It's by far not the only testing framework out there, but it is definitely quite popular, and if you are aspiring to be a developer or work more closely with building software from a network automation standpoint, it's definitely a great tool to carry in your belt. Read up more on pytest here if you aren't already familiar with it.

Selenium

Selenium is a chemical elemen—Hold on, wrong Selenium. Selenium is a "suite of tools for automating web browsers." TL;DR, if you aren't familiar with it, it's a pretty neat tool that lets you control your browser of choice entirely programmatically. Now, you may have heard of other tools like BeautifulSoup (also called bs4) that are really helpful for parsing through HTML and XML files. Using requests to pull down an HTML file and then bs4 to parse it works really well when parsing simple static web pages, but bs4 does not implement a full blown web browser/client—What this means is that if you have a web-app that loads some JavaScript and that JavaScript client modifies the HTML DOM based off of API calls and other functions that it runs, you won't really get much value out of bs4. This is where Selenium steps in. Selenium essentially provides "drivers" for interacting with the various popular web browsers. It can run in a semi-interactive mode, or even entirely headless! This tool will be the backbone of the test examples in this post, so read up more on Selenium here if you want more information.

selenium-python

Selenium has bindings for a few popular programming languages, but in our example, we'll just be using Python's bindings since we're using pytest. Installation of these bindings is quite simple and can be installed from PyPi with pip3. This gives us a nice way to drive a web browser using python.

pytest-selenium

On top of the Selenium bindings for Python, there is a neat library called pytest-selenium that makes it easy to integrate these two bits of software.

A Simple Test

Okay, if you're still with me by now, you're probably invested in this, and hopefully, you are comfortable with the basics of pytest and Selenium. If not, this example might help, otherwise, check out their official docs on getting started, they're quite good (and is what I based this example on).

First off, I'm going to create a Python venv, as it always makes life easier:

[I] ┬─[chris@chris-lt01:~/c/selenium_sandbox]─[09:15:23 AM]─[G:main]
╰─>$ python3 -m venv .venv
[I] ┬─[chris@chris-lt01:~/c/selenium_sandbox]─[09:15:32 AM]─[G:main]
╰─>$ source .venv/bin/activate.fish
[I] ┬─[chris@chris-lt01:~/c/selenium_sandbox]─[09:15:33 AM]─[V:.venv]─[G:main]
╰─>$ which python3
/home/chris/code/selenium_sandbox/.venv/bin/python3

Now that I have a nice fresh venv to work from, let's install our pre-reqs for Python:

[I] ┬─[chris@chris-lt01:~/c/selenium_sandbox]─[09:17:11 AM]─[V:.venv]─[G:main]
╰─>$ pip install selenium pytest pytest-selenium ipython

On top of the python bits needed, we'll also download a Chrome from here and put that somewhere on our $PATH. I'm going to do this with my venv activated so that the driver is stored with my venv. We'll also throw in a dash of chmod +x to make sure that this can be executed.

[I] ┬─[chris@chris-lt01:~/c/selenium_sandbox]─[09:22:35 AM]─[V:.venv]─[G:main]
╰─>$ unzip ~/Downloads/chromedriver_linux64.zip
Archive:  /home/chris/Downloads/chromedriver_linux64.zip
  inflating: chromedriver
[I] ┬─[chris@chris-lt01:~/c/selenium_sandbox]─[09:22:38 AM]─[V:.venv]─[G:main]
╰─>$ mv chromedriver ~/.local/bin/
[I] ┬─[chris@chris-lt01:~/c/selenium_sandbox]─[09:22:56 AM]─[V:.venv]─[G:main]
╰─>$ chmod +x ~/.local/bin/chromedriver
[I] ┬─[chris@chris-lt01:~/c/selenium_sandbox]─[09:23:05 AM]─[V:.venv]─[G:main]
╰─>$

Now let's create a new file called test.py and build our first stupidly simple test:

import pytest

@pytest.mark.nondestructive
def test_get_title(selenium):
    selenium.get('https://slash64.tech')
    title = selenium.title
    assert title == "/64"

Easy enough, right? Now, let's pull it all together and run pytest:

[I] ┬─[chris@chris-lt01:~/c/selenium_sandbox]─[09:29:04 AM]─[V:.venv]─[G:main]
╰─>$ pytest test.py --driver Chrome
========================================== test session starts ==========================================
platform linux -- Python 3.8.10, pytest-6.2.5, py-1.11.0, pluggy-1.0.0
driver: Chrome
sensitiveurl: .*
rootdir: /home/chris/code/synth_test_sandbox
plugins: variables-1.9.0, selenium-3.0.0, base-url-1.4.2, metadata-2.0.1, html-3.1.1
collected 1 item                                                                                        

test.py .                                                                                         [100%]

=========================================== 1 passed in 1.64s ===========================================
[I] ┬─[chris@chris-lt01:~/c/selenium_sandbox]─[09:29:20 AM]─[V:.venv]─[G:main]
╰─>$

And just like that, we have written a test that asserts that the title HTML element of this blog is the string /64. Pretty neat! Just to prove that everything's working as intended, let's change line 7 of our test to assert title == "Google":

[I] ┬─[chris@chris-lt01:~/c/selenium_sandbox]─[09:30:06 AM]─[V:.venv]─[G:main]
╰─>$ pytest test.py --driver Chrome
========================================== test session starts ==========================================
platform linux -- Python 3.8.10, pytest-6.2.5, py-1.11.0, pluggy-1.0.0
driver: Chrome
sensitiveurl: .*
rootdir: /home/chris/code/synth_test_sandbox
plugins: variables-1.9.0, selenium-3.0.0, base-url-1.4.2, metadata-2.0.1, html-3.1.1
collected 1 item                                                                                        

test.py F                                                                                         [100%]

=============================================== FAILURES ================================================
____________________________________________ test_get_title _____________________________________________

selenium = <selenium.webdriver.chrome.webdriver.WebDriver (session="7202d2deca09eb2044873f27382fe457")>

    @pytest.mark.nondestructive
    def test_get_title(selenium):
        selenium.get('https://slash64.tech')
        title = selenium.title
>       assert title == "Google"
E       AssertionError: assert '/64' == 'Google'
E         - Google
E         + /64

test.py:7: AssertionError
-------------------------------------------- pytest-selenium --------------------------------------------
Driver log: /tmp/pytest-of-chris/pytest-9/test_get_title0/driver.log
URL: https://slash64.tech/
WARNING: Failed to gather log types: Message: unknown command: Cannot call non W3C standard command while in W3C mode
======================================== short test summary info ========================================
FAILED test.py::test_get_title - AssertionError: assert '/64' == 'Google'
=========================================== 1 failed in 3.63s ===========================================

Very cool! You can see in the pytest output that my website returned the string /64 so when we were expecting it to show up as Google the test failed.

Parting Thoughts

I hope this gives you some ideas around what you can do with pytest and Selenium. I know that this isn't an incredibly practical example, but I hope that it is enough to tickle your brain a bit and get you thinking about how you could use this in your day-to-day work, and hit me up if you have any questions.