
flowchart TB
Repo[Push GitLab] --> Yml[.gitlab-ci.yml]
Yml --> Stages["stages: build, test, deploy"]
Stages --> Job[test-selenium]
Job --> Img["image jdk + chrome"]
Img --> Var[variables MAVEN_OPTS]
Var --> Cache[cache .m2 .gradle]
Cache --> Script[mvn test]
Script --> CHL["Chrome --headless=new"]
Script --> Run[Tests Selenium]
Run --> Art["artifacts: reports + screenshots"]
Run --> Junit[reports junit XML]
Junit --> UI[Pipeline visualization]
Job --> Par["parallel:matrix BROWSER"]
Par --> Grid[Selenium Grid services]
Estructura del pipeline y configuración del WebDriver headless
Integrar Selenium en un pipeline de CI permite detectar regresiones funcionales en cada commit, antes de que lleguen a preproducción. La pieza crítica es disponer de un navegador real en el runner de CI, con todas las dependencias del sistema operativo (bibliotecas gráficas, fuentes, codecs) y con una versión de ChromeDriver que coincida exactamente con la de Chrome. La forma más simple de garantizar esa alineación es usar una imagen Docker que ya incluya JDK, Maven y Chrome preinstalados.
La imagen comunitaria markhobson/maven-chrome:jdk-21 es suficiente para proyectos Java 21 con Maven: pública una versión reciente de Google Chrome junto con el ChromeDriver correspondiente y actualiza ambas a la par. Alternativas válidas son la imagen oficial de Selenium (selenium/standalone-chrome:latest) cuando se necesita Selenium Grid embebido, o una imagen propia basada en eclipse-temurin:21-jdk con Chrome añadido por apt-get.
El archivo .gitlab-ci.yml define el pipeline completo. La caché Maven evita descargar el mismo spring-boot-starter docena tras docena de veces, los artefactos conservan los reportes JUnit y la variable MAVEN_OPTS apunta al repositorio local cacheado:
# .gitlab-ci.yml
image: markhobson/maven-chrome:jdk-21
stages:
- build
- test
- report
variables:
MAVEN_OPTS: "-Dmaven.repo.local=$CI_PROJECT_DIR/.m2/repository -Dstyle.color=always"
MAVEN_CLI_OPTS: "--batch-mode --errors --fail-at-end --show-version"
cache:
key:
files:
- pom.xml
paths:
- .m2/repository/
- node_modules/
build:
stage: build
script:
- mvn $MAVEN_CLI_OPTS compile
artifacts:
paths:
- target/
expire_in: 1 hour
selenium_tests:
stage: test
needs: [build]
before_script:
- chromedriver --version
- google-chrome --version
script:
- mvn $MAVEN_CLI_OPTS test
artifacts:
when: always
paths:
- target/screenshots/
- target/surefire-reports/
- target/logs/
reports:
junit: target/surefire-reports/TEST-*.xml
expire_in: 1 week
La directiva when: always es clave: sin ella, los artefactos solo se publican si el job tiene éxito y perdemos precisamente las pruebas del fallo que queremos diagnosticar. El needs: [build] convierte el pipeline en un DAG: el job de tests arranca en cuanto termina el build, sin esperar al resto del stage.
El ChromeDriver debe configurarse con opciones que son imprescindibles dentro de contenedores. --no-sandbox desactiva el sandboxing de Chrome (impracticable cuando el proceso corre como root en Docker); --disable-dev-shm-usage fuerza a Chrome a usar /tmp en lugar de /dev/shm, que en los runners de GitLab SaaS viene limitado a 64 MB y provoca crashes aleatorios; --headless=new activa el modo sin ventana gráfica de Chrome 112 en adelante, notablemente más rápido que el headless clásico:
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import java.time.Duration;
public class WebDriverFactory {
public static WebDriver crear() {
ChromeOptions opciones = new ChromeOptions();
boolean enCI = System.getenv("CI") != null;
if (enCI) {
opciones.addArguments("--headless=new");
opciones.addArguments("--no-sandbox");
opciones.addArguments("--disable-dev-shm-usage");
opciones.addArguments("--disable-gpu");
opciones.addArguments("--window-size=1920,1080");
opciones.addArguments("--lang=es-ES");
}
WebDriver driver = new ChromeDriver(opciones);
driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(5));
driver.manage().timeouts().pageLoadTimeout(Duration.ofSeconds(30));
return driver;
}
}
Usar la variable de entorno CI como switch permite reutilizar el mismo código en desarrollo (con ventana visible) y en el pipeline (headless). GitLab la define automáticamente en todos los jobs, por lo que no requiere configuración adicional.
Capturas de fallos, reportes JUnit y paralelización
El valor real de un pipeline de Selenium está en poder diagnosticar un fallo sin tener que reproducirlo en local. Para ello conviene capturar una screenshot cada vez que una prueba falla y acompañarla de los logs de navegador y el HTML de la página. JUnit 5 lo expone con extensiones:
import org.junit.jupiter.api.extension.*;
import org.openqa.selenium.OutputType;
import org.openqa.selenium.TakesScreenshot;
import org.openqa.selenium.WebDriver;
import java.io.IOException;
import java.nio.file.*;
public class ScreenshotOnFailure implements TestWatcher {
@Override
public void testFailed(ExtensionContext context, Throwable cause) {
WebDriver driver = (WebDriver) context.getStore(
ExtensionContext.Namespace.create("selenium")
).get("driver");
if (driver == null) {
return;
}
try {
byte[] png = ((TakesScreenshot) driver).getScreenshotAs(OutputType.BYTES);
String html = driver.getPageSource();
Path dir = Paths.get("target/screenshots");
Files.createDirectories(dir);
String nombre = context.getRequiredTestClass().getSimpleName()
+ "-" + context.getRequiredTestMethod().getName();
Files.write(dir.resolve(nombre + ".png"), png);
Files.writeString(dir.resolve(nombre + ".html"), html);
} catch (IOException ignored) {
}
}
}
Con esa extensión registrada, cada fallo deja un ClaseTest-nombreMetodo.png en target/screenshots/. El pipeline ya está configurado para subirlo como artefacto y GitLab permite previsualizar imágenes directamente en la interfaz.
Los reportes JUnit (target/surefire-reports/TEST-*.xml) se procesan automáticamente por GitLab cuando se declaran en artifacts.reports.junit. En cada pipeline aparece una pestaña Tests con gráficos de tests superados, fallados y en ejecución, y un diff respecto al commit anterior. Esto permite identificar regresiones introducidas por un merge request específico sin abrir ningún log.
Para suites grandes, ejecutar en paralelo reduce drásticamente los tiempos. GitLab soporta dos estrategias complementarias. La primera es parallel:matrix, que lanza varias instancias del mismo job con variables distintas y reparte las clases de test mediante un plugin como maven-failsafe-plugin con groups:
selenium_tests:
stage: test
needs: [build]
parallel:
matrix:
- TEST_GROUP: [smoke, checkout, search, account, admin]
script:
- mvn $MAVEN_CLI_OPTS test -Dgroups=$TEST_GROUP
artifacts:
when: always
paths:
- target/screenshots/
reports:
junit: target/surefire-reports/TEST-*.xml
flowchart LR
MR[Merge Request] --> P[Pipeline GitLab]
P --> B[build]
B --> S1[selenium smoke]
B --> S2[selenium checkout]
B --> S3[selenium search]
B --> S4[selenium account]
B --> S5[selenium admin]
S1 --> R["reports JUnit + screenshots"]
S2 --> R
S3 --> R
S4 --> R
S5 --> R
La segunda estrategia es levantar un Selenium Grid efímero dentro del propio pipeline mediante services. Cada contenedor Chrome expone un nodo del Grid y las pruebas conectan a través de RemoteWebDriver. Ventaja: permite ejecutar Chrome + Firefox + Edge en paralelo para verificar compatibilidad cross-browser.
selenium_grid:
stage: test
image: maven:3.9-eclipse-temurin-21
services:
- name: selenium/standalone-chrome:latest
alias: selenium-chrome
- name: selenium/standalone-firefox:latest
alias: selenium-firefox
variables:
SELENIUM_CHROME_URL: "http://selenium-chrome:4444/wd/hub"
SELENIUM_FIREFOX_URL: "http://selenium-firefox:4444/wd/hub"
BASE_URL: "http://nginx"
script:
- mvn $MAVEN_CLI_OPTS test -Dtarget=remote
Variables de entorno, limpieza de recursos y operación en producción
Un pipeline limpio nunca hardcodea URLs ni credenciales. GitLab permite definir variables protegidas y enmascaradas a nivel de proyecto o grupo (menú Settings, sección CI/CD, apartado Variables). Las protegidas solo se expone en branches también protegidas (típicamente main y release/*), y las enmascaradas no se imprimen en los logs ni siquiera si el test las hace echo por accidente.
Algunas variables habituales en una suite Selenium madura:
| Variable | Uso |
|----------|-----|
| BASE_URL | URL de la aplicación bajo test (staging, preprod, local) |
| API_TOKEN | Token para crear datos de prueba vía API antes de la UI |
| TEST_USER_EMAIL / TEST_USER_PWD | Usuario prefabricado para el flujo de login |
| BROWSERSTACK_USER / BROWSERSTACK_KEY | Credenciales si se delegan tests en BrowserStack |
| IMPLICIT_WAIT | Timeout de espera implícita en segundos |
El WebDriver debe cerrarse siempre con quit() para que Chrome termine todos sus procesos hijos. Un driver olvidado deja zombies que acaban agotando la memoria del runner. JUnit 5 facilita esta limpieza con @AfterEach:
@AfterEach
void liberarRecursos() {
if (driver != null) {
driver.quit();
driver = null;
}
}
En producción, un pipeline Selenium robusto combina cinco elementos: imagen Docker con navegadores preinstalados, WebDriver configurado en headless con flags de contenedor, captura de screenshots y HTML en cada fallo, reportes JUnit publicados como artefacto y paralelización por grupos de tests o por Selenium Grid. Sin alguno de esos cinco, la suite acaba siendo inestable o lenta y el equipo termina desactivándola, que es el peor resultado posible.
Caso B2B, telco: un operador de banda ancha mantiene un portal de autoservicio con más de cuatrocientos flujos críticos (alta de línea, cambio de tarifa, gestión de averías, facturación electrónica). La suite Selenium cuenta con dos mil seiscientos escenarios escritos en Java 21 con JUnit 5 y el patrón Page Object. Antes de cada release quincenal, el pipeline de GitLab CI lanza los tests repartidos en doce jobs paralelos mediante parallel:matrix sobre ocho regiones geográficas (los redireccionadores CDN son distintos por región). Cada job usa un ChromeDriver en headless dentro de la imagen markhobson/maven-chrome:jdk-21, escribe capturas y HTML de los fallos a un bucket S3 de la propia empresa, y pública JUnit en GitLab. Los equipos de QA revisan cada mañana el dashboard de flaky tests construido sobre los JUnit archivados durante noventa días, detectando regresiones estacionales (noches de fin de mes, picos de portabilidad) antes de que afecten al cliente final.
Fuentes y referencias
Documentación oficial y recursos externos para profundizar en Selenium
Documentación oficial de Selenium
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Selenium es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de Selenium
Explora más contenido relacionado con Selenium y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
- Montar un pipeline de GitLab CI que ejecute pruebas Selenium con Maven
- Configurar ChromeDriver en modo headless sobre contenedores
- Optimizar caché, artefactos y reportes JUnit para análisis posterior
- Paralelizar suites con matrices y Selenium Grid dentro del propio pipeline