Selenium: Primer contacto
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"))
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:
- Python
- Selenium WebDriver para Python
- Firefox
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:
- Para Firefox:
geckodriver
(el que se menciona en el error). - Para Chrome:
chromedriver
.
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
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"))
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:
- Se abre el navegador Ungoogled Chromium en modo de automatización.
-
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). - Se hace una búsqueda en Google del término cheese.
-
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
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"))
Como se ve en el Video 2, ahora el guión ejecuta todos los pasos satisfactoriamente:
- Se abre el navegador en modo de automatización.
- Se navega a la dirección
https://duckduckgo.com/
. - Se hace una búsqueda del término universe.
-
Se localiza el elemento con selector CSS
.zcm__link.is-active
. - 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: