· Cambiar idioma (Actualmente: Español)

Selenium: Primer contacto

Abril 22, 2021 14:22 -0500

La documentación sobre herramientas para pruebas de aplicaciones web de Django menciona a Selenium desde hace años, pero nunca me puse en la tarea de probarla. Incluso hoy, no siento mucha urgencia de usarla, pero en estos días sí me dio curiosidad de ver el navegador web en modo automático. Esto es lo que aprendí en el ejercicio.

Primero, mi idea de Selenium era más limitada por como interpreté lo que dice sobre la herramienta en la documentación de Django:

Note that the test client is not intended to be a replacement for SELENIUM or other “in-browser” frameworks. Django’s test client has a different focus. In short:

  • Use Django’s test client to establish that the correct template is being rendered and that the template is passed the correct context data.
  • Use in-browser frameworks like SELENIUM to test rendered HTML and the behavior of Web pages, namely JavaScript functionality. Django also provides special support for those frameworks; see the section on LiveServerTestCase for more details.

— Django Documentation, Testing Tools

Tal vez el «namely JavaScript functionality» me llevó a pensar que se trataba de una herramienta especializada en probar la funcionalidad de las interfaces gráficas que dependían de JavaScript. Pero, no. El alcance de Selenium es mucho más amplio. Como dice el mismo sitio web de la herramienta:

Selenium automates browsers. That's it! What you do with that power is entirely up to you.

Primarily it is for automating web applications for testing purposes, but is certainly not limited to just that.

Boring web-based administration tasks can (and should) also be automated as well.

Segundo, Selenium es un conjunto de herramientas: Selenium WebDriver, Selenium IDE y Selenium Grid, además de bibliotecas para programación. Pero da la impresión de ser una sola en las conversaciones sobre pruebas automáticas de software porque generalmente la gente dice «Selenium» para referirse a Selenium WebDriver, que es la parte esencial de todo el conjunto de herramientas que se usa para tomar control de los navegadores web programáticamente.

Tercero, Selenium WebDriver es una implementación de la especificación WebDriver del W3C. Esta especificación describe una interfaz de introspección y control remoto de navegadores web que usa un protocolo de cable (wire protocol) para que programas en procesos independientes puedan enviar instrucciones remotas a los navegadores.

Cuarto, se pueden controlar los navegadores localmente (en la misma máquina donde se ejecuta el código de control) o remotamente (el código de control puede estar en una máquina y los navegadores en otras). Como esta fue mi primera prueba, la hice localmente.

Firefox: Prueba de automatización abortada

La primera prueba no salió bien.

En la primera página de la documentación de Selenium se da un ejemplo corto de automatización para diferentes lenguajes de programación. Yo seleccioné Python y el guión se veía así:

#This example requires Selenium WebDriver 3.13 or newer
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support.expected_conditions import presence_of_element_located


with webdriver.Firefox() as driver:
    wait = WebDriverWait(driver, 10)
    driver.get("https://google.com/ncr")
    driver.find_element(By.NAME, "q").send_keys("cheese" + Keys.RETURN)
    first_result = wait.until(presence_of_element_located((By.CSS_SELECTOR, "h3>div")))
    print(first_result.get_attribute("textContent"))
      
Guión 1. Ejemplo Hola mundo de la documentación de Selenium.

En este punto la documentación no especifica los requisitos de software para poder ejecutar el guión, más allá de los que resultan obvios al leer el código:

Pero tener estos en el entorno no es suficiente. Ejecutar el guión solo con estas dependencias resulta en estos errores:

Traceback (most recent call last):
  File "/home/yo/.config/entorno/environments/selenium/selenium/lib/python3.8/site-packages/selenium/webdriver/common/service.py", line 72, in start
    self.process = subprocess.Popen(cmd, env=self.env,
  File "/gnu/store/cizh7vg0w09izkv07pxdv8csir8p4sdd-python-3.8.2/lib/python3.8/subprocess.py", line 854, in __init__
    self._execute_child(args, executable, preexec_fn, close_fds,
  File "/gnu/store/cizh7vg0w09izkv07pxdv8csir8p4sdd-python-3.8.2/lib/python3.8/subprocess.py", line 1702, in _execute_child
    raise child_exception_type(errno_num, err_msg, err_filename)
FileNotFoundError: [Errno 2] No such file or directory: 'geckodriver'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "selenium-control-firefox.py", line 8, in <module>
    with webdriver.Firefox() as driver:
  File "/home/yo/.config/entorno/environments/selenium/selenium/lib/python3.8/site-packages/selenium/webdriver/firefox/webdriver.py", line 164, in __init__
    self.service.start()
  File "/home/yo/.config/entorno/environments/selenium/selenium/lib/python3.8/site-packages/selenium/webdriver/common/service.py", line 81, in start
    raise WebDriverException(
selenium.common.exceptions.WebDriverException: Message: 'geckodriver' executable needs to be in PATH.
    

Profundizando en la documentación de Selenium, resulta que el WebDriver no controla directamente el navegador sino que lo hace por medio de un controlador (driver) que es específico para cada navegador (ver componentes de automatización). Por ejemplo:

Estos controladores son ejecutables que tienen que estar disponibles en alguno de los directorios definidos en la variable de entorno PATH (sistema operativo GNU de Guix, en mi caso). Además, dependiendo del navegador, es posible que tengan que instalarse de manera independiente.

Efectivamente, revisando los archivos ejecutables que vienen con mi Firefox (IceCat), no encontré el geckodriver:

$ tree /gnu/store/h6w9irpa81yklhy23idsjcqvq5h92r1y-icecat-78.9.0-guix0-preview1/bin/
/gnu/store/h6w9irpa81yklhy23idsjcqvq5h92r1y-icecat-78.9.0-guix0-preview1/bin
└── icecat -> /gnu/store/h6w9irpa81yklhy23idsjcqvq5h92r1y-icecat-78.9.0-guix0-preview1/lib/icecat/icecat

0 directories, 1 file
    

Desafortunadamente, el geckodriver tampoco está disponible para instalarse como paquete independiente en Guix. Entonces paré la prueba y decidí probar con mi Chrome (Ungoogled Chromium), que sí viene con su propio controlador (chromedriver):

$ tree /gnu/store/77qqpbiiwkya5fg8h06fz0r2mns9sjza-ungoogled-chromium-89.0.4389.114-1/bin/
/gnu/store/77qqpbiiwkya5fg8h06fz0r2mns9sjza-ungoogled-chromium-89.0.4389.114-1/bin/
├── chromedriver
└── chromium

0 directories, 2 files
    

Chrome: Prueba de automatización insatisfactoria

Video 1. Grabación de la ejecución del Guión 2.

Para esta prueba modifiqué el guión original como sigue:

#This example requires Selenium WebDriver 3.13 or newer
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support.expected_conditions import presence_of_element_located


with webdriver.Chrome() as driver:
    wait = WebDriverWait(driver, 10)
    driver.get("https://google.com/ncr")
    driver.find_element(By.NAME, "q").send_keys("cheese" + Keys.RETURN)
    first_result = wait.until(presence_of_element_located((By.CSS_SELECTOR, "h3>div")))
    print(first_result.get_attribute("textContent"))
      
Guión 2. Guión original modificado para usar Chrome en vez de Firefox.

O sea, simplemente cambié webdriver.Firefox() por webdriver.Chrome(), nada más.

Como se ve en el Video 1, el guión funciona hasta cierto punto:

  1. Se abre el navegador Ungoogled Chromium en modo de automatización.
  2. Se navega a la dirección https://google.com/ncr (donde creo que NCR significa No Country Redirect, que tal vez se usa en este caso para lograr cierta reproducibilidad de la prueba evitando resultados de búsqueda localizados por región).
  3. Se hace una búsqueda en Google del término cheese.
  4. El guión espera varios segundos tratando de encontrar un elemento HTML al que se aplique el selector CSS h3>div, pero finalmente la ejecución termina anormalmente:

    $ python3 selenium-control-chrome.py
    Traceback (most recent call last):
      File "selenium-control-chrome.py", line 12, in <module>
        first_result = wait.until(presence_of_element_located((By.CSS_SELECTOR, "h3>div")))
      File "/home/yo/.config/entorno/environments/selenium/selenium/lib/python3.8/site-packages/selenium/webdriver/support/wait.py", line 80, in until
        raise TimeoutException(message, screen, stacktrace)
    selenium.common.exceptions.TimeoutException: Message:
            

La última línea del guión, que imprimiría en el terminal el contenido de texto del elemento que se esperaba encontrar, no se alcanza a ejecutar. La prueba termina incompleta.

Pero el problema no parece extraño. Este tipo de prueba está condenada a fallar con el paso del tiempo. El elemento que se esperaba encontrar existía cuando se escribió el guión, pero ya no.

Entonces decidí hacer otra prueba con otro buscador y otro elemento HTML.

Chrome: Prueba de automatización satisfactoria

Video 2. Grabación de la ejecución del Guión 3.

Esta tercera y última prueba es, en esencia, la misma que la anterior, pero usé DuckDuckGo (DDG) para hacer la búsqueda y el elemento que se espera encontrar es una pestaña activa con clase CSS .zcm__link.is-active.

No hubo que hacer muchos cambios en el guión:

#This example requires Selenium WebDriver 3.13 or newer
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support.expected_conditions import presence_of_element_located


with webdriver.Chrome() as driver:
    wait = WebDriverWait(driver, 10)
    driver.get("https://duckduckgo.com/")
    # Find the search box. And enter a search query.
    driver.find_element(By.NAME, "q").send_keys("universe" + Keys.RETURN)
    # Define locator for the HTML element that represents the active
    # results tab in the results page (labeled "All" by default).
    locator = (By.CSS_SELECTOR, ".zcm__link.is-active")
    # Once locator is found, print it's text content.
    first_result = wait.until(presence_of_element_located(locator))
    print(first_result.get_attribute("textContent"))
      
Guión 3. Modificación del Guión 2 para usar DuckDuckGo como buscador, en vez de Google.

Como se ve en el Video 2, ahora el guión ejecuta todos los pasos satisfactoriamente:

  1. Se abre el navegador en modo de automatización.
  2. Se navega a la dirección https://duckduckgo.com/.
  3. Se hace una búsqueda del término universe.
  4. Se localiza el elemento con selector CSS .zcm__link.is-active.
  5. Se imprime el texto del elemento, que en este caso es la etiqueta de la pestaña «Todo» (la que está ubicada debajo del campo de búsqueda en la página de resultados, al lado de las pestañas «Imágenes», «Videos», etc.).

Claro que esta prueba está condenada a sufrir el mismo destino de la prueba original de Selenium, pero ver la prueba completarse calmó mi curiosidad.

Algo que se nota en los videos es que la prueba con Google es mucho más lenta que la prueba hecha con DDG. Las dos pruebas se hicieron estando conectado a una VPN. Sin la VPN, ambas pruebas se demoran menos, pero la de Google sigue siendo más lenta que la de DDG (16 segundos y 6 segundos respectivamente).

Probablemente le encuentre uso a Selenium en mis próximas pruebas de sitios y aplicaciones web (si es que no se acaba el mundo primero).

Temas relacionados: