This is the P2PU Archive. If you want the current site, go to www.p2pu.org!

Learning Web UI Automation

Week 6 - Writing Maintainable Tests

David Burns's picture
Wed, 2011-03-09 20:55

In this final lecture we are going to have a look at how we can start making maintainable. This is extremely important when we start writing lots of tests as it abstracts away Selenium from the tests. By abstracting it away from the tests we can start writing extremely readable tests.

The upside to making extremely readable tests, as in making extremely readable code, is that you start removing duplication making your test code DRY. DRY stands for Don't Repeat Yourself. The other benefit, if done properly, is that you can start creating a Domain Specific Langauge (DSL) that both technical and non-technical people can read. Getting non-technical people writing automated tests means that everyone within a business can start adding testing value.

So what is the most common approach to doing this?

Page Object Model

There is a design pattern called Page Object Model which says that we should be developing our tests using good OO development techniques. The main idea is to have 1 object for every page that you work against. When you transition from one page to another you should get a new object. When you move from each object to the other you should always pass the Selenium, or any other browser framework, to the new place.

So how would that look in python?

If we have site.py as the module for our site we could have 2 classes.


class Login(object):

    def __init__(self, selenium):
        self.selenium = selenium

class PageAfterLogin(object):

    def __init__(self, selenium):
        self.selenium = selenium

    
The __init__ method is a constructor. In Java/.NET it would be a method with the same name as the object. We can see that it accepts a Selenium object, that we create in our tests, and then we can do things within our newly created objects.

So let's see an example of a page object in action.

The code below is our test code

test_login_details.py

import unittest
import site

class LoginTests(unittest.TestCase):

    def setUp(self):
        self.selenium = selenium("localhost", 4444,
                                "*firefox", "http://mysite")
        self.selenium.start()
        self.selenium.window_maximize()

    def tearDown(self):
        self.selenium.stop()

    def test_that_we_login(self):
        login = site.Login(self.selenium)
        second_page = login.as(username, password)

As you can see we have started making our code readable by making methods called as().

What about our locators?

Well, if we apply DRY to our Page Objects we will see that placing our locators in a variable and then using that variable will make our code cleaner. It also means that if a locator changes we can update it in one place and everywhere that a test relied on that locator, which is probably failing at that point, will suddenly pass.

Asserts

Tests rely on their assert methods so the question that we need to answer is do we put the asserts in the Page Objects or in the tests themselves?

The answer is to always put them in the tests themselves and never in the Page Objects. The reasoning behind this is that if a test fails we can see the stacktrace that is normally shown, starting at the assert and then showing us the lines that got us there.

Putting them in the Page Objects creates a bit of a smoke screen. When running our tests it will suddenly fail and the test runner will not be able put any meaning info to the screen. Adding them to the tests also shows intent in the tests making it more readable for any one else coming to have a look at the test.

import unittest
import site

class LoginTests(unittest.TestCase):

    def setUp(self):
        self.selenium = selenium("localhost", 4444,
                                "*firefox", "http://mysite")
        self.selenium.start()
        self.selenium.window_maximize()

    def tearDown(self):
        self.selenium.stop()

    def test_that_we_login(self):
        login = site.Login(self.selenium)
        second_page = login.as(username, password)
        second_page.search_for("my marbles")
        self.assertTrue(second_page.has_text("my marbles")

There is no real right way to do things but you can easily spot if you are not doing something right if it hampers the readability of your tests.

I am also a .NET dev in my spare time and have created this example.

    [Test]
    public void ShouldLoadHomeThenGoToXpathTutorial()
    {
        Home home = new Home(selenium);
        SeleniumTutorials seleniumTutorials = home.ClickSelenium();
        SeleniumXPathTutorial seleniumXPathTutorial = seleniumTutorials.ClickXpathTutorial();
        Assert.True(seleniumXPathTutorial.
                    IsInputOnScreen(SeleniumXPathTutorial.FirstInput));
        Assert.True(seleniumXPathTutorial
                    .IsInputOnScreen(SeleniumXPathTutorial.SecondInput));
        Assert.True(seleniumXPathTutorial
                    .IsInputOnScreen(SeleniumXPathTutorial.Total));
    }

We can see that, without having to know C#, we can easily read this test.

Task

This weeks task is to create a test, ideally one from https://litmus.mozilla.org/show_test.cgi?searchType=by_category&product_... that doesnt require any logging in, that shows how you would do it.

As a bonus, for me and you, if you want to have your test used by Mozilla I suggest forking https://github.com/AutomatedTester/Addon-Tests and then doing a pull request so that we can use it. Your name can be added to the contributors section of the files you work on or create.

As always please email me your answers. If you need help please feel free to email me or get me on irc at irc.mozilla.org in the #mozwebqa channel or on irc.freenode.net in the #Selenium channel!