Selenium y QA Automation: tests


En mi anterior post sobre Selenium y QA Automation comenzamos a ver cómo utilizar Selenium/Webdriver para la automatización de tareas web. En esta ocasión lo adaptaremos para su uso más habitual: la creación de pruebas de aceptación.

Mediante ejemplos en Python adaptaremos nuestro ejercicio anterior para ejecutarlo como tests. Así mismo, refactorizaremos para obtener un entorno de pruebas sencillo, extensible y fácil de utilizar.

Tras este artículo, no debería ser difícil utilizar un sistema de integración contínua para la automatización de pruebas de aceptación.

Testing

Sin pan ni na, ahí va nuestro ejemplo anterior, pero con forma de test:

#!/usr/bin/env python
# FILE: exampleTest.py
import unittest
from selenium import webdriver
from selenium.common.exceptions import NoSuchElementException

class AutomationTest(unittest.TestCase):
    def test_the_title_is_set(self):
        driver = webdriver.Remote('http://localhost:4444/wd/hub', capabilities={'browserName': 'firefox'})
        try:
            driver.get('http://localhost:8000')
            self.assertEquals("Directory listing for /", driver.title)
        finally:
            driver.close()

Fácil, ¿eh? Y ya podemos ejecutarlo (acordáos del servidor de Selenium y de nuestro servidor web, explicados en el artículo anterior):

$ nosetests
.
----------------------------------------------------------------------
Ran 1 test in 15.626s

OK

Refactorizando

Pero claro... Si tenemos que escribir todo eso en cada test, puede resultar un poco tedioso y difícil de leer. Añadamos un nuevo test y refactoricemos:

#!/usr/bin/env python
# FILE: exampleTest.py
import unittest
from selenium import webdriver
from selenium.common.exceptions import NoSuchElementException

class AutomationTest(unittest.TestCase):
    def setUp(self):
        self.driver = webdriver.Remote('http://localhost:4444/wd/hub', capabilities={'browserName': 'firefox'})

    def tearDown(self):
        self.driver.close()

    def test_the_title_is_set(self):
        self.driver.get('http://localhost:8000')
        self.assertEquals("Directory listing for /", self.driver.title)

    def test_there_are_links(self):
        self.driver.get('http://localhost:8000')
        links = self.driver.find_elements_by_css_selector('a')
        self.assertTrue(len(links) > 0)

¡Ey, eso ya es otra cosa! Ahora sí se queda más limpio.

Seguramente os preguntaréis: ¿Es necesario obtener una nueva ventana para cada test? La respuesta es bastante compleja. Si queréis aseguraros de que tenéis un entorno limpio, entonces sí, es necesario. Si no lo hacéis así es posible que tengáis efectos secundarios poco recomendables. El problema es que, al tener que estar abriendo y cerrando navegadores, las pruebas son muy lentas.

Por esa razón conviene usar este tipo de pruebas sólo para lo que no podamos probar de ninguna otra manera ("características" de cada navegador) y para tests de aceptación, que quizá sólo tengamos que ejecutar un par de veces al día.

Una posible solución es utilizar phantomjs para acelerarlos y así tener, exclusivamente, pruebas de aceptación, ya que no podríamos probar esos corner-cases propios de cada navegador. Estos corner-cases pueden leerse, a menudo, como "Internet Explorer".

Pero podemos dar otra vuelta de tuerca.

Sacando factor común

Imagináos que tenemos muchas clases y tenemos que repetirnos muchas veces... A mí no me gusta repetirme. Así que saco de la clase lo que no le pertenece y la mejoro un poquito más:

#!/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 = 'http://localhost:8000'
    SELENIUM_URL = 'http://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(capabilities={'browserName': self.BROWSER_NAME})
        self._driver.get(self.BASE_URL + address)
        return self._driver


class AutomationTest(BrowserTest):
    def test_the_title_is_set(self):
        browser = self.getBrowser('/')
        self.assertEquals("Directory listing for /", browser.title)

    def test_there_are_links(self):
        browser = self.getBrowser('/')
        links = browser.find_elements_by_css_selector('a')
        self.assertTrue(len(links) > 0)

 if __name__ == '__main__':
    unittest.main()

Como veréis, no es exactamente lo mismo. En el caso anterior obtenía el driver de Selenium en el setUp y ahora lo hago en una función a parte. Esto tiene dos ventajas: por una parte, no se obtendrá para todos los tests, sino sólo allá donde se necesite, y la segunda es que así aprovecho y obtengo el navegador directamente en la página necesitada.

Además, he aprovechado para sacar las constantes como constantes que son, quedando un código mucho más limpio.

Aquí os dejo el vídeo de cómo funciona (para que veáis que es muy lento):

Ventajas e inconvenientes

Y con esto tenemos un pequeño framework de a penas 21 líneas sobre el que comenzar a trabajar. Sin embargo, este framework tiene dos problemas.

El primero es que, si en nuestra clase de pruebas sobreescribimos los métodos setUp o tearDown, podemos tener efectos secundarios poco recomendables, como no poder obtener el browser o que éste no se cierre.

El segundo es una mejora: me gustaría obtener un screenshot cada vez que el test falle. Resulta que no hay manera de saber el estado del test en el método tearDown. Eso ha supuesto que me estudie la API de testing de Python y me he quedado muy sorprendido. Tanto como para crearme mi propio *framework* completo. Sin embargo, temo que eso quedará para la semana que viene, ya que el artículo de hoy ya es un poco largo.

De todas maneras, todo lo visto hasta aquí, en python, puede replicarse en otros lenguajes orientados a objetos, como Ruby, Java, PHP, etc. No hay nada específico del lenguaje.

Existe una pequeña mejora, que utiliza una característica propia de la librería de tests de python. Consiste en evitar el método tearDown, mediante el uso del método addCleanup. Sin embargo, como digo, no es una gran ventaja y no merece la pena ni añadir el código aquí.

Y en el futuro...

Aquí hay un framework simple. Es tan simple, que nos podemos permitir el copiarlo de proyecto en proyecto.

Sin embargo, escribiendo este artículo he descubierto ciertas capacidades de la API de test de python que me han abierto la puerta a crear un framework aún mejor, también muy pequeño, pero no tan obvio.

Este nuevo framework lo comentaré en un próximo post.


Comentarios

Comments powered by Disqus