Contenido

Selenium y QA Automation: PageObjects

En los artículos anteriores, “Selenium y QA Automation” y “Selenium y QA Automation: Tests” hemos visto cómo utilizar Selenium/Webdriver.

En este caso vamos a ver cómo organizar estos tests. Realmente no es necesario el conocimiento previo de Selenium, ya que puede utilizarse algún otro framework. Sin embargo, sí que resulta interesante ya que lo usaremos para los ejemplos.

Aunque los PageObjects se idearon para pruebas en la Web (de hecho, creo que son una idea de Selenium), este artículo está escrito pensando que los PageObjects pueden usarse tanto en aplicaciones de escritorio como web, por lo que veréis “pantalla” o “página” indistintamente.

Selenium

Creando tests

Cuando uno comienza a escribir tests, comienza haciendo las cosas mal. Es normal. Para hacer las cosas bien hechas es necesario adquirir experiencia.

Lo primero que suele hacerse es escribir toda la inicialización de un test en el propio test. Poco a poco, uno comienza a darse cuenta de que no hace más que repetirse, así que decide pasar al siguiente nivel: crear funciones dentro del propio Test Case, de manera que pueda reutilizarlas.

Sin embargo, se sigue repitiendo código entre distintas Suites. Así que se aumenta al siguiente nivel: Crear archivos aparte que permitan reutilizar toda esta estructura.

PageObjects

Los PageObjects son el siguiente paso. Consisten en encapsular la funcionalidad de nuestra aplicación en objetos. Dicho de otro modo: Consisten en modelar de nuevo nuestra aplicación, de manera que cada pantalla sea un objeto (un PageObject) y cada acción que pueda realizar el usuario sea un método.

Serán los PageObjects quienes interactúen con la aplicación principal, aislando este uso de los tests y consiguiendo así tests más robustos.

Ejemplo

Y llegados a este punto, vamos con un ejemplo completo. Consiste en un formulario que, al pulsar el botón, reemplaza el propio formulario por un saludo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!-- file: index.html -->
<html>
  <head>
    <title>PageObjects Example (By MagMax)</title>

    <script type="text/JavaScript">
      function greet() {
        var myAnchor = document.getElementById("content");
        var mySpan = document.createElement("span");
        mySpan.innerHTML = "Hello, " + document.getElementById("name").value;
        myAnchor.parentNode.replaceChild(mySpan, myAnchor);
      }
    </script>
  </head>
  <body>
    <div id="content">
      <label for="name">Name:</label>
      <input id="name" />
      <input type="submit" onClick="return greet();"/>
    </div>
  </body>
</html>

Y ahora vamos con los tests:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#!/usr/bin/env python
# FILE: exampleTest.py
import unittest
from selenium import webdriver
from selenium.common.exceptions import NoSuchElementException

class BrowserTest(unittest.TestCase):
    BASE_URL = 'https://localhost:8000'
    SELENIUM_URL = 'https://localhost:4444/wd/hub'
    BROWSER_NAME = 'firefox'

    def setUp(self):
        self._driver = None

    def tearDown(self):
        if self._driver:
            self._driver.close()

    def getBrowser(self, address):
        if self._driver is None:
            self._driver = webdriver.Remote(self.SELENIUM_URL, {'browserName': self.BROWSER_NAME})
        self._driver.get(self.BASE_URL + address)
        return self._driver


class PageObject(object):
    def __init__(self, browser):
        self._browser = browser


class GreetingsPageObject(PageObject):
    def writeName(self, name):
        self._browser.find_element_by_id('name').send_keys(name)

    def submit(self):
        self._browser.find_element_by_css_selector('[type=submit]').click()

    def getContent(self):
        return self._browser.find_element_by_css_selector('span').text


class AutomationTest(BrowserTest):
    def test_the_title_is_set(self):
        browser = self.getBrowser('/')
        greetingsPage = GreetingsPageObject(browser)

        greetingsPage.writeName("MagMax")
        greetingsPage.submit()

        self.assertEquals('Hello, MagMax', greetingsPage.getContent())

Es un poquito largo, pero vamos a analizarlo para ver que no es tan complicado.

Por una parte, tenemos la clase BrowserTest, que ya analizamos en el artículo anterior. Como vimos, en esta clase se enmascara el acceso principal a Selenium. Vemos que está configurado para utilizar el servidor. Podéis modificarlo para que utilice webdriver.Firefox en lugar de webdriver.Remote si queréis simplificarlo, pero con este cambio estaréis perdiendo versatilidad. De esto hablaré más en otro artículo.

A continuación creamos un PageObject básico, que lo único que implementa es el constructor. Con el fin de poder acceder a Selenium, necesitamos el driver. La idea es, justamente, usar el driver exclusivamente desde los PageObjects.

Lo siguiente es el PageObject propiamente dicho, en la clase GreetingsPageObject, en la que definimos las acciones que se pueden realizar en la página:

  • writeName: escribe el nombre en el lugar adecuado.
  • submit: envía los datos.
  • getContent: Obtiene el contenido final.

Y finalmente el test en la clase AutomationTest, que define claramente lo que se desea hacer.

Me gustaría hacer incapié en varias cosas que podéis observar en este ejemplo:

  • No se hacen comprobaciones en el PageObject.
  • No se accede directamente a Selenium/Webdriver desde el test. Únicamente desde el PageObject.
  • Estamos creando el objeto directamente desde el Test, pero sería mejor utilizar una Factory.

Problemas

Los PageObjects no están exentos de problemas, pero ya hay quien se ha peleado con éstos y ha aportado algunas soluciones:

Enlaces a otras pantallas

Es normal que, al pulsar un botón, aparezca otra pantalla/página.

La forma de modelar esto es mediante la devolución de otro PageObject que modele la página destino.

1
2
3
4
5
6
class Desktop(PageObject):
    pass

class Login(PageObject):
    def login(self):
        return Desktop(self._browser)

NOTA: Se supone que ambas clases heredan de los PageObjects del ejemplo, por lo que necesitan el browser en su constructor.

Distintos resultados para una misma acción

En ocasiones podemos tener distintos resultados para una misma acción, dependiendo del estado interno. Por ejemplo, en la ventana de Login, el resultado será diferente si el login es válido o inválido.

La solución a este problema es utilizar distintos métodos que modelen cada una de estas posibilidades:

1
2
3
4
5
6
7
class Login(object):
    def login(self):
        # ...
    def login_no_password(self):
        # ...
    def login_invalid(self):
        # ...

Comprobaciones

Todas las comprobaciones deberían hacerse en el propio Test y no en el PageObject. De esta manera se aíslan los Tests del manejo de la aplicación. No deberían realizarse comprobaciones en los PageObjects, ya que esto rome la encapsulación.

Existe una excepción. Es un patrón bastante corriente comprobar que estamos en la página que debemos estar al crear un PageObject. Esto es normal, ya que encontrarnos en una página incorrecta puede dar lugar a errores más difíciles de encontrar.

Por ello, suelen utilizarse los constructores para realizar este tipo de comprobaciones. Hay que evitar realizar estas comprobaciones en otros sitios. Sin embargo, debe tratarse de realizar comprobaciones rápidas, ya que se realizarán muy a menudo.

Páginas muy grandes

En ocasiones vamos a encontrar páginas muy grandes, que suelen dividirse en objetos más pequeños y, a menudo, reutilizados en distintas páginas.

En estos casos debemos crear PageWidgets, que embeban la funcionalidad de todo el componente. Así resultará más sencillo reutilizarlos y organizarlos.

1
2
3
4
5
6
class Login(object):
    def __init__(self):
        self.menu = MenuPageWidget()
        self.header = HeaderPageWidget()
        self.footer = FooterPageWidget()
        # ...

Ventajas

  • Si usamos un framework diferente de Selenium/Webdriver, es suficiente cambiar la implementación de los PageObjects, ya que el funcionamiento de las páginas debería ser el mismo. Los tests seguirán siendo igual de válidos.
  • Es más fácil reutilizar la funcionalidad.
  • Los tests son independientes del acceso a la Web.
  • Los tests se quedan más sencillos.

Referencias

Lo mejor es irse a la propia web de selenium, donde está el artículo sobre Page Objects. Es un artículo bastante conciso, en el que se proponen ejemplos en Java.