Automated Application Testing using Selenium

I was recently onboarded onto a project to automate testing of a web application using Selenium. On my previous projects, I have written many unit tests, but end to end testing was a first-time experience. 

As a software developer, you are always reminded how important it is to thoroughly test your code. That is why I was excited to use automated testing tools and learn of their value. Automating tools such as Selenium allow you to run tests with ease, so you can catch bugs and ensure that your code is reliable. This also means that it is important for the tester to understand the user’s intentions so that tests are written in ways that mimic a user’s path throughout the application. 

The tool I’m using for automating testing is Selenium, a popular open-source testing framework that lets you automate web browsers and user actions within them. Selenium can be used with multiple languages, and I’m using Python. Here are a few things I learned while automating testing for an application using Selenium.  

1. Start With a Solid Foundation

Before you start automating tests using Selenium, it's important to have a solid foundation in place. Walk through the scenarios and think about the processes to automate as well as the assertions you will be making within your tests. This also means making sure you understand your application’s design and structure, analyzing HTML tags, CSS classes, and IDs. This will make it much easier to write tests that are reliable and maintainable. 

2. Don’t Give Up, Be Patient

Automated testing using Selenium can be a bit inconsistent at times. I ran into issues where tests failed unexpectedly many times in my project, or where the test results were incorrect. Whether it was due to timeouts or external services causing errors, it can be frustrating when this happens, but it is important to be patient and persistent. Take the time to understand what and why it went wrong. Most importantly, don’t give up and try alternative approaches until you find a solution that works. This means writing more recovery code such as retry code in case of failure. 

Take for example the code snippet below, a scenario where you wait for iframe to load within a page. I was able to identify that this is a common point of failure due to slow network when running this inside Azure Pipeline so this was a perfect place to add a retry strategy to ensure that the test is less flaky and more consistent. 

tries = 1
while (tries <= 5):
    try:
        iframe = WebDriverWait(driver, 10).until(
            EC.element_to_be_clickable((By.CLASS_NAME, "external-content--iframe")))
        tries = 6
    except TimeoutException:
        tries += 1

Additionally, StackOverflow is a great resource where you could refer to see if any others ran into similar issues!

3. Research the Right Tools

Selenium is a powerful tool. There are many helpful libraries that make writing tests even quicker and easier. For me, using Python as the language for scripting, I used a test runner called pytest to make my tests easier to write and read. This was especially helpful when I had to run my script in Azure Cloud and Azure Devops environments 

@pytest.mark.parametrize('password,server', [('itrellis_password', False), ('server_password', True)])
def test_info_and_access_roadmap(driver, password, server, request):

Using the above code as an example, without pytest, I would have to need to write separate test cases for each type of configurations, however using parameterization and fixtures, I’m able to reuse the same test case for both configs and safely pass in the user secrets.  

I also used the webdriver-manager library to set up a test environment for various browser configurations. Take the time to research and choose the right tools for your use cases, dependencies, and configurations. 

4. Keep Your Tests Modular

As a newcomer in end-to-end testing, I just started to write code without much thought in the beginning. However, as my test suite grew, I noticed patterns in the processes and type of assertions I made that could be grouped together. Then it hit me, just like software development, I could have been making my own code modular so that it is easier to maintain. This was not only for me but for future maintainers. This meant going back to my old code, breaking tests down into smaller, more atomic tests for readability and factoring out common processes into shared methods using good programming practices like encapsulation and abstraction. 

Here is an example of using fixtures to handle ADO login versus Azure Cloud Login that I factored out (Look at the previous example to see how values are passed in) 

def init_roadmap_settings(driver, password, server, request):
    if server is True:
        value = request.getfixturevalue(password)
        BasePage.login_server(driver, value)  # login
    else:
        value = request.getfixturevalue(password)
        BasePage.login(driver, value)  # login
        # click on the x for upgrade if the pop up comes up
        BasePage.upgrade(driver)
    WebDriverWait(driver, 20).until(EC.element_to_be_clickable(
        (By.ID, "__bolt-addRoadmap")))

5. Be Mindful of Timing Issues

As I started to run the tests I wrote, I came to realize that one of the biggest challenges when automating testing using Selenium is dealing with timing issues. Luckily, Selenium provides several useful methods for wait-states and resolving these issues. I needed to be mindful of things like page load times, AJAX requests, and user input delays when dealing with forms. At first, I was using more implicit waits, but I quickly discovered early on that there are better techniques such as using explicit waits (to make the test wait for the application to catch up with commands) or even polling the DOM for web elements to appear. 

Here is a simple example of using implicit wait (Before) and explicit wait (After) 

# Before:
    use_percent_button.click()
    time.sleep(3)
# After:
    use_percent_button.click()
    WebDriverWait(driver, 2).until(EC.visibility_of_element_located(
        (By.XPATH, "//div[@class='summary-text' and descendant::span[contains(text(), '%')]]")))

Using explicit wait, you can ensure the conditions you are seeking are met, and save the time in your tests if the conditions are satisfied.

6. Understand Your Test Plans

Just like user stories, there are priorities when it comes to test cases/plans. Not all tests are critical, and some may be trivial. Some tests might  be more likely to fail than others. I started automating high priority tests first to ensure the critical bugs are caught as soon as possible so when the application is shipped out, I can feel safe knowing core business logic is sound.

7. Keep Tests Updated

As your application grows and new features are added, the tests will need to accommodate them. Some may require small code changes and some may require a rewrite (e.g., UI update). It is important to have effective communication with the development team to know the types of changes you should expect in the application and adapt the code to them. Also make sure to regularly review and change your tests to ensure that they are still providing value. 

Keep these tips in mind, and you will write better tests, and you can assure that the application will be of better quality.

Young-Chan Kim

Young is a Senior Developer at iTrellis in the Digital & App Innovation Practice. He has worked across a variety of projects and technologies, and currently leads automated software testing for our own published software, Portfolio++.

Previous
Previous

Things We Do to Improve Delivery

Next
Next

Infrastructure as Configured