Modulo 23: Selenium WebDriver
Objetivos del modulo
- Entender que es Selenium y cuando usarlo.
- Crear una solucion E2E con NUnit, Selenium WebDriver y una web local.
- Automatizar navegacion, formularios, validaciones, selects, checkboxes, radios, pestanas y alertas.
- Usar esperas explicitas para evitar pruebas fragiles.
- Aplicar Page Object Model.
- Preparar datos por API antes de una prueba de navegador.
- Practicar tambien contra una web publica de entrenamiento,
https://demoqa.com/, diferenciando los riesgos de probar una web externa. - Ejecutar pruebas desde Visual Studio Community, terminal y modo headless.
1. Iniciacion
Concepto: Selenium WebDriver permite controlar un navegador real desde codigo. Se usa para probar una aplicacion web como la usaria una persona: abrir paginas, hacer clic, escribir datos y comprobar resultados.
Selenium no sustituye a NUnit o xUnit. Normalmente se usa junto a un framework de testing:
| Tipo de prueba | Que prueba | Herramienta tipica |
|---|---|---|
| Unitaria | Una clase aislada | NUnit, xUnit |
| Integracion | Varias piezas juntas | NUnit, xUnit |
| End-to-end | Flujo real de usuario | Selenium, Playwright |
En este modulo construiremos Tienda.Selenium, una solucion con una web local para poder probar todos los ejemplos sin depender de paginas externas.
2. Proyecto que vamos a construir
Tienda.Selenium/
Tienda.Selenium.sln
src/
Tienda.Core/
Producto.cs
Tienda.Api/
Program.cs
tests/
Tienda.E2E.Tests/
Pages/
LoginPage.cs
Support/
LocalApiServer.cs
SeleniumTestBase.cs
TestDataApi.cs
TestSettings.cs
Tests/
PrimerTest.cs
LoginTests.cs
ProductosTests.cs
VentanasYAlertasTests.cs
La API local contiene estas paginas:
| Ruta | Para practicar |
|---|---|
/ | Navegacion y titulo |
/login | Formularios y Page Object |
/dashboard | Esperar cambio de URL |
/productos | Listados y busqueda |
/productos/nuevo | Formularios y validacion |
/controles | Select, checkbox y radio |
/ventanas | Nueva pestana |
/ayuda | Pagina abierta en pestana nueva |
/alertas | Confirmaciones JavaScript |
/api/test/productos | Preparar datos para pruebas E2E |
3. Crear la solucion en Visual Studio Community
- Crea una solucion vacia llamada
Tienda.Selenium. - Agrega un proyecto Class Library llamado
Tienda.Core. - Agrega un proyecto ASP.NET Core Empty llamado
Tienda.Api. - Agrega un proyecto NUnit Test Project llamado
Tienda.E2E.Tests. - En
Tienda.Api, agrega referencia aTienda.Core. - En
Tienda.E2E.Tests, agrega referencias aTienda.ApiyTienda.Core. - En
Tienda.E2E.Tests, instala:Selenium.WebDriverSelenium.Support
- Crea las carpetas
Pages,SupportyTests.
Por terminal:
dotnet new sln -n Tienda.Selenium -f sln
dotnet new classlib -n Tienda.Core -o src/Tienda.Core --framework net10.0
dotnet new web -n Tienda.Api -o src/Tienda.Api --framework net10.0
dotnet new nunit -n Tienda.E2E.Tests -o tests/Tienda.E2E.Tests --framework net10.0
dotnet sln Tienda.Selenium.sln add src/Tienda.Core/Tienda.Core.csproj src/Tienda.Api/Tienda.Api.csproj tests/Tienda.E2E.Tests/Tienda.E2E.Tests.csproj
dotnet add src/Tienda.Api/Tienda.Api.csproj reference src/Tienda.Core/Tienda.Core.csproj
dotnet add tests/Tienda.E2E.Tests/Tienda.E2E.Tests.csproj reference src/Tienda.Api/Tienda.Api.csproj src/Tienda.Core/Tienda.Core.csproj
dotnet add tests/Tienda.E2E.Tests/Tienda.E2E.Tests.csproj package Selenium.WebDriver
dotnet add tests/Tienda.E2E.Tests/Tienda.E2E.Tests.csproj package Selenium.Support
Selenium 4 incluye Selenium Manager, que localiza o descarga el driver compatible con Chrome en la mayoria de escenarios.
4. Crear la web local de pruebas
Crea Producto.cs:
namespace Tienda.Core;
public record Producto
{
public int Id { get; init; }
public string Nombre { get; init; } = string.Empty;
public string Categoria { get; init; } = string.Empty;
public decimal Precio { get; init; }
public int Stock { get; init; }
}
En Tienda.Api/Program.cs, extrae la creacion de la aplicacion a ApiHost.Create(args):
WebApplication app = ApiHost.Create(args);
await app.RunAsync();
public static class ApiHost
{
public static WebApplication Create(string[] args)
{
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
WebApplication app = builder.Build();
app.MapGet("/", () => Results.Content(Page("Inicio", "<h1>Dashboard</h1>"), "text/html"));
app.MapGet("/login", () => Results.Content(LoginPage(), "text/html"));
app.MapGet("/productos", () => Results.Content(ProductosPage(), "text/html"));
app.MapGet("/productos/nuevo", () => Results.Content(ProductoForm(), "text/html"));
app.MapGet("/controles", () => Results.Content(ControlesPage(), "text/html"));
app.MapGet("/ventanas", () => Results.Content(VentanasPage(), "text/html"));
app.MapGet("/alertas", () => Results.Content(AlertasPage(), "text/html"));
return app;
}
}
La idea importante no es memorizar el HTML, sino tener paginas controladas para practicar cada tipo de interaccion.
5. Configuracion de entorno y servidor local
Crea Support/TestSettings.cs:
namespace Tienda.E2E.Tests.Support;
public static class TestSettings
{
public static string BaseUrl { get; set; } =
Environment.GetEnvironmentVariable("E2E_BASE_URL")
?? "http://127.0.0.1:5123";
}
Crea Support/LocalApiServer.cs para arrancar la web durante las pruebas:
using System.Net;
using System.Net.Sockets;
using Microsoft.AspNetCore.Builder;
public sealed class LocalApiServer : IAsyncDisposable
{
private readonly WebApplication _app;
private LocalApiServer(WebApplication app, string url)
{
_app = app;
Url = url;
}
public string Url { get; }
public static async Task<LocalApiServer> StartAsync()
{
int port = GetFreePort();
string url = $"http://127.0.0.1:{port}";
WebApplication app = ApiHost.Create(["--urls", url]);
await app.StartAsync();
return new LocalApiServer(app, url);
}
public async ValueTask DisposeAsync()
{
await _app.StopAsync();
await _app.DisposeAsync();
}
private static int GetFreePort()
{
TcpListener listener = new(IPAddress.Loopback, 0);
listener.Start();
int port = ((IPEndPoint)listener.LocalEndpoint).Port;
listener.Stop();
return port;
}
}
Con este enfoque, el alumno no necesita arrancar la API manualmente antes de ejecutar tests.
6. Clase base para Selenium
Crea Support/SeleniumTestBase.cs:
using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
using OpenQA.Selenium.Support.UI;
public abstract class SeleniumTestBase
{
private LocalApiServer? _server;
protected IWebDriver Driver { get; private set; } = null!;
protected WebDriverWait Wait { get; private set; } = null!;
[OneTimeSetUp]
public async Task OneTimeSetUp()
{
if (Environment.GetEnvironmentVariable("E2E_BASE_URL") is null)
{
_server = await LocalApiServer.StartAsync();
TestSettings.BaseUrl = _server.Url;
}
}
[SetUp]
public void SetUp()
{
ChromeOptions options = new();
if (Environment.GetEnvironmentVariable("CI") == "true")
options.AddArgument("--headless=new");
options.AddArgument("--window-size=1920,1080");
Driver = new ChromeDriver(options);
Wait = new WebDriverWait(Driver, TimeSpan.FromSeconds(10));
}
[TearDown]
public void TearDown()
{
Driver.Quit();
Driver.Dispose();
}
}
7. Primer test con navegador
Crea Tests/PrimerTest.cs:
[TestFixture]
public class PrimerTest : SeleniumTestBase
{
[Test]
public void AbrirPaginaPrincipal_MuestraTitulo()
{
Driver.Navigate().GoToUrl(TestSettings.BaseUrl);
Assert.That(Driver.Title, Does.Contain("Inicio"));
}
}
Ejecuta solo este test primero. Si falla, revisa Chrome, Selenium Manager y que la API local arranque correctamente.
8. Localizadores
Los localizadores indican como encontrar elementos en la pagina:
IWebElement porId = Driver.FindElement(By.Id("email"));
IWebElement porCss = Driver.FindElement(By.CssSelector("button[type='submit']"));
IWebElement porXPath = Driver.FindElement(By.XPath("//*[contains(text(), 'Teclado')]"));
IWebElement porTag = Driver.FindElement(By.TagName("h1"));
| Localizador | Uso |
|---|---|
By.Id | Muy estable si el id es unico |
By.CssSelector | Flexible y util para data-testid |
By.TagName | Elementos simples como h1 |
By.LinkText | Enlaces con texto claro |
By.XPath | Potente, pero puede ser fragil |
9. Page Object para login
Crea Pages/LoginPage.cs:
public class LoginPage
{
private readonly IWebDriver _driver;
private readonly WebDriverWait _wait;
public LoginPage(IWebDriver driver)
{
_driver = driver;
_wait = new WebDriverWait(driver, TimeSpan.FromSeconds(10));
}
public void Abrir()
{
_driver.Navigate().GoToUrl($"{TestSettings.BaseUrl}/login");
}
public void IniciarSesion(string email, string password)
{
_driver.FindElement(By.Id("email")).SendKeys(email);
_driver.FindElement(By.Id("password")).SendKeys(password);
_driver.FindElement(By.CssSelector("button[type='submit']")).Click();
}
public bool EstaEnDashboard()
{
return _wait.Until(driver => driver.Url.Contains("/dashboard"));
}
}
Prueba:
[Test]
public void Login_ConCredencialesValidas_EntraEnDashboard()
{
LoginPage login = new(Driver);
login.Abrir();
login.IniciarSesion("ana@ejemplo.com", "Password123!");
Assert.That(login.EstaEnDashboard(), Is.True);
}
10. Formularios y validaciones
Prueba crear producto:
[Test]
public void CrearProducto_DatosValidos_MuestraProductoEnListado()
{
Driver.Navigate().GoToUrl($"{TestSettings.BaseUrl}/productos/nuevo");
Driver.FindElement(By.Id("nombre")).SendKeys("Teclado mecanico");
Driver.FindElement(By.Id("categoria")).SendKeys("Perifericos");
Driver.FindElement(By.Id("precio")).Clear();
Driver.FindElement(By.Id("precio")).SendKeys("89.99");
Driver.FindElement(By.Id("stock")).Clear();
Driver.FindElement(By.Id("stock")).SendKeys("12");
Driver.FindElement(By.CssSelector("[data-testid='guardar-producto']")).Click();
Wait.Until(d => d.Url.EndsWith("/productos", StringComparison.OrdinalIgnoreCase));
string listado = Driver.FindElement(By.CssSelector("main")).Text;
Assert.That(listado, Does.Contain("Teclado mecanico"));
}
Prueba validacion:
[Test]
public void CrearProducto_SinNombre_MuestraError()
{
Driver.Navigate().GoToUrl($"{TestSettings.BaseUrl}/productos/nuevo");
Driver.FindElement(By.CssSelector("[data-testid='guardar-producto']")).Click();
IWebElement error = Wait.Until(d => d.FindElement(By.CssSelector(".validation-error")));
Assert.That(error.Text, Does.Contain("El nombre es obligatorio"));
}
11. Selects, checkboxes y radio buttons
La pagina /controles existe para practicar controles HTML:
[Test]
public void SelectCheckboxYRadio_EjemploDeLocalizadores()
{
Driver.Navigate().GoToUrl($"{TestSettings.BaseUrl}/controles");
SelectElement categoria = new(Driver.FindElement(By.Id("categoria")));
categoria.SelectByText("Perifericos");
IWebElement activo = Driver.FindElement(By.Id("activo"));
if (!activo.Selected)
activo.Click();
Driver.FindElement(By.CssSelector("input[name='envio'][value='urgente']")).Click();
Driver.FindElement(By.CssSelector("[data-testid='guardar-controles']")).Click();
IWebElement resultado = Wait.Until(d =>
d.FindElement(By.CssSelector("[data-testid='resultado-controles']")));
Assert.That(resultado.Text, Is.EqualTo("Controles guardados"));
}
12. Ventanas, pestanas y alertas
Nueva pestana:
[Test]
public void AbrirAyuda_EnNuevaPestana_MuestraTitulo()
{
Driver.Navigate().GoToUrl($"{TestSettings.BaseUrl}/ventanas");
string original = Driver.CurrentWindowHandle;
Driver.FindElement(By.Id("abrir-ayuda")).Click();
Wait.Until(d => d.WindowHandles.Count == 2);
string nueva = Driver.WindowHandles.Single(handle => handle != original);
Driver.SwitchTo().Window(nueva);
Assert.That(Driver.Title, Does.Contain("Ayuda"));
Driver.Close();
Driver.SwitchTo().Window(original);
}
Alerta:
[Test]
public void Eliminar_ConfirmaAlerta_MuestraResultado()
{
Driver.Navigate().GoToUrl($"{TestSettings.BaseUrl}/alertas");
Driver.FindElement(By.Id("eliminar")).Click();
IAlert alert = Driver.SwitchTo().Alert();
Assert.That(alert.Text, Does.Contain("Seguro?"));
alert.Accept();
}
13. Preparar datos por API
Crea Support/TestDataApi.cs:
public static class TestDataApi
{
public static async Task CrearProductoAsync(string nombre)
{
using HttpClient client = new()
{
BaseAddress = new Uri(TestSettings.BaseUrl)
};
HttpResponseMessage response = await client.PostAsJsonAsync("/api/test/productos", new
{
nombre,
categoria = "Perifericos",
precio = 89.99m,
stock = 10
});
response.EnsureSuccessStatusCode();
}
}
Uso:
[Test]
public async Task BuscarProducto_ProductoExistente_ApareceEnResultados()
{
await TestDataApi.CrearProductoAsync("Teclado E2E");
Driver.Navigate().GoToUrl($"{TestSettings.BaseUrl}/productos");
Driver.FindElement(By.Id("buscar")).SendKeys("Teclado E2E");
IWebElement resultado = Wait.Until(d =>
d.FindElement(By.XPath("//*[contains(text(), 'Teclado E2E')]")));
Assert.That(resultado.Displayed, Is.True);
}
14. Ejecucion
Desde Visual Studio:
- Compila la solucion.
- Abre Test > Test Explorer.
- Ejecuta primero
PrimerTest. - Ejecuta despues todo el proyecto.
Desde terminal:
dotnet test
dotnet test --filter "FullyQualifiedName~Productos"
$env:CI="true"; dotnet test
Contra una web ya arrancada:
$env:E2E_BASE_URL="http://localhost:7001"
dotnet test
15. Plan docente recomendado
Selenium suele necesitar mas tiempo practico que los frameworks unitarios, porque intervienen navegador, HTML, tiempos de carga y datos. Una distribucion para 4 horas:
| Tiempo | Contenido | Resultado esperado |
|---|---|---|
| 20 min | Que es E2E y cuando usarlo | El alumno entiende coste y valor |
| 30 min | Crear solucion y web local | API local arrancando desde tests |
| 30 min | Primer ChromeDriver y primer test | PrimerTest en verde |
| 35 min | Localizadores y formularios | Crear producto y validar errores |
| 30 min | Esperas explicitas | Evitar Thread.Sleep |
| 35 min | Page Object de login | LoginPage reutilizable |
| 30 min | Select, checkbox, radio | /controles probado |
| 25 min | Pestanas y alertas | /ventanas y /alertas probadas |
| 25 min | Datos por API y headless | Tests repetibles y preparados para CI |
16. Ruta de construccion guiada
Etapa 1: test minimo contra la home
Objetivo: comprobar que el navegador abre la web local.
[Test]
public void Home_CargaCorrectamente()
{
Driver.Navigate().GoToUrl(TestSettings.BaseUrl);
Assert.That(Driver.Title, Does.Contain("Inicio"));
}
Si falla:
- Revisa que Chrome este instalado.
- Revisa que
LocalApiServeractualiceTestSettings.BaseUrl. - Revisa que
Driver.Quit()se ejecute siempre enTearDown.
Etapa 2: localizar elementos visibles
[Test]
public void Productos_MuestraBuscador()
{
Driver.Navigate().GoToUrl($"{TestSettings.BaseUrl}/productos");
IWebElement buscador = Driver.FindElement(By.Id("buscar"));
Assert.Multiple(() =>
{
Assert.That(buscador.Displayed, Is.True);
Assert.That(buscador.GetAttribute("placeholder"), Is.EqualTo("Buscar productos"));
});
}
Explicacion:
Displayedverifica que el elemento se muestra.GetAttributesirve para leer valores de HTML.- No todo se comprueba con
.Text.
Etapa 3: formulario con datos validos
Primero se prueba el camino feliz: rellenar y guardar.
Driver.FindElement(By.Id("nombre")).SendKeys("Teclado mecanico");
Driver.FindElement(By.Id("categoria")).SendKeys("Perifericos");
Driver.FindElement(By.Id("precio")).Clear();
Driver.FindElement(By.Id("precio")).SendKeys("89.99");
Driver.FindElement(By.Id("stock")).Clear();
Driver.FindElement(By.Id("stock")).SendKeys("12");
Driver.FindElement(By.CssSelector("[data-testid='guardar-producto']")).Click();
Despues se espera a que la navegacion termine:
Wait.Until(d => d.Url.EndsWith("/productos", StringComparison.OrdinalIgnoreCase));
Etapa 4: validacion
El caso de error es igual de importante:
[Test]
public void CrearProducto_SinNombre_MuestraError()
{
Driver.Navigate().GoToUrl($"{TestSettings.BaseUrl}/productos/nuevo");
Driver.FindElement(By.CssSelector("[data-testid='guardar-producto']")).Click();
IWebElement error = Wait.Until(d => d.FindElement(By.CssSelector(".validation-error")));
Assert.That(error.Text, Does.Contain("El nombre es obligatorio"));
}
Etapa 5: extraer Page Object
Cuando un flujo crece, crea una clase de pagina:
public class ProductoFormPage
{
private readonly IWebDriver _driver;
private readonly WebDriverWait _wait;
public ProductoFormPage(IWebDriver driver)
{
_driver = driver;
_wait = new WebDriverWait(driver, TimeSpan.FromSeconds(10));
}
public void Abrir()
{
_driver.Navigate().GoToUrl($"{TestSettings.BaseUrl}/productos/nuevo");
}
public void Rellenar(string nombre, string categoria, string precio, string stock)
{
_driver.FindElement(By.Id("nombre")).SendKeys(nombre);
_driver.FindElement(By.Id("categoria")).SendKeys(categoria);
_driver.FindElement(By.Id("precio")).Clear();
_driver.FindElement(By.Id("precio")).SendKeys(precio);
_driver.FindElement(By.Id("stock")).Clear();
_driver.FindElement(By.Id("stock")).SendKeys(stock);
}
public void Guardar()
{
_driver.FindElement(By.CssSelector("[data-testid='guardar-producto']")).Click();
}
public void EsperarListado()
{
_wait.Until(d => d.Url.EndsWith("/productos", StringComparison.OrdinalIgnoreCase));
}
}
Uso:
[Test]
public void CrearProducto_ConPageObject_ApareceEnListado()
{
ProductoFormPage form = new(Driver);
form.Abrir();
form.Rellenar("Webcam", "Perifericos", "49.99", "20");
form.Guardar();
form.EsperarListado();
Assert.That(Driver.FindElement(By.CssSelector("main")).Text, Does.Contain("Webcam"));
}
17. Banco de ejemplos Selenium
17.1 Espera explicita reutilizable
Puedes crear helpers para condiciones frecuentes:
protected IWebElement EsperarVisible(By by)
{
return Wait.Until(driver =>
{
IWebElement element = driver.FindElement(by);
return element.Displayed ? element : null;
});
}
Uso:
IWebElement titulo = EsperarVisible(By.TagName("h1"));
Assert.That(titulo.Text, Is.EqualTo("Productos"));
17.2 Esperar texto
[Test]
public void GuardarControles_MuestraMensaje()
{
Driver.Navigate().GoToUrl($"{TestSettings.BaseUrl}/controles");
Driver.FindElement(By.CssSelector("[data-testid='guardar-controles']")).Click();
Wait.Until(d =>
d.FindElement(By.CssSelector("[data-testid='resultado-controles']")).Text == "Controles guardados");
Assert.Pass();
}
17.3 Evitar Thread.Sleep
Evita:
Thread.Sleep(3000);
Mejor:
Wait.Until(d => d.FindElement(By.CssSelector(".validation-error")).Displayed);
Explicacion:
Thread.Sleepsiempre espera el mismo tiempo aunque la pagina este lista.- Si la pagina tarda mas, la prueba falla.
- Si tarda menos, pierdes tiempo.
17.4 Localizadores robustos
Preferible:
Driver.FindElement(By.CssSelector("[data-testid='guardar-producto']")).Click();
Mas fragil:
Driver.FindElement(By.CssSelector("form > button")).Click();
Razon: el primer selector expresa intencion de prueba; el segundo depende de la estructura visual del HTML.
17.5 Comprobar URL
[Test]
public void Login_DespuesDeEntrar_RedireccionaADashboard()
{
LoginPage login = new(Driver);
login.Abrir();
login.IniciarSesion("ana@ejemplo.com", "Password123!");
Wait.Until(d => d.Url.Contains("/dashboard"));
Assert.That(Driver.Url, Does.Contain("/dashboard"));
}
17.6 Comprobar atributos
[Test]
public void Login_Email_ConservaValorEscrito()
{
Driver.Navigate().GoToUrl($"{TestSettings.BaseUrl}/login");
IWebElement email = Driver.FindElement(By.Id("email"));
email.SendKeys("ana@ejemplo.com");
Assert.That(email.GetAttribute("value"), Is.EqualTo("ana@ejemplo.com"));
}
17.7 Comprobar tabla o listado
Aunque el ejemplo usa article, la idea es la misma para tablas:
[Test]
public async Task Productos_ListadoContieneProductoCreadoPorApi()
{
await TestDataApi.CrearProductoAsync("Producto listado");
Driver.Navigate().GoToUrl($"{TestSettings.BaseUrl}/productos");
IReadOnlyCollection<IWebElement> productos = Driver.FindElements(By.CssSelector("[data-testid='producto']"));
Assert.That(productos.Any(p => p.Text.Contains("Producto listado")), Is.True);
}
17.8 Select completo
[Test]
public void Categoria_Select_PermiteElegirPantallas()
{
Driver.Navigate().GoToUrl($"{TestSettings.BaseUrl}/controles");
SelectElement categoria = new(Driver.FindElement(By.Id("categoria")));
categoria.SelectByValue("pantallas");
Assert.That(categoria.SelectedOption.Text, Is.EqualTo("Pantallas"));
}
17.9 Checkbox
[Test]
public void Activo_Checkbox_PuedeMarcarseYDesmarcarse()
{
Driver.Navigate().GoToUrl($"{TestSettings.BaseUrl}/controles");
IWebElement activo = Driver.FindElement(By.Id("activo"));
if (!activo.Selected)
activo.Click();
Assert.That(activo.Selected, Is.True);
activo.Click();
Assert.That(activo.Selected, Is.False);
}
17.10 Radio button
[Test]
public void Envio_Urgente_PuedeSeleccionarse()
{
Driver.Navigate().GoToUrl($"{TestSettings.BaseUrl}/controles");
IWebElement urgente = Driver.FindElement(By.CssSelector("input[name='envio'][value='urgente']"));
urgente.Click();
Assert.That(urgente.Selected, Is.True);
}
17.11 Captura manual
[Test]
public void Productos_GuardaCapturaDelListado()
{
Driver.Navigate().GoToUrl($"{TestSettings.BaseUrl}/productos");
GuardarCaptura("listado-productos");
Assert.Pass();
}
Helper:
protected void GuardarCaptura(string nombre)
{
Screenshot screenshot = ((ITakesScreenshot)Driver).GetScreenshot();
Directory.CreateDirectory("screenshots");
string path = Path.Combine("screenshots", $"{nombre}-{DateTime.UtcNow:yyyyMMddHHmmss}.png");
screenshot.SaveAsFile(path);
}
17.12 Ejecutar contra Chrome visible o headless
Visible para clase:
ChromeOptions options = new();
options.AddArgument("--window-size=1920,1080");
Headless para CI:
if (Environment.GetEnvironmentVariable("CI") == "true")
options.AddArgument("--headless=new");
Comando:
$env:CI="true"
dotnet test
17.13 Errores habituales
| Error | Causa habitual | Solucion |
|---|---|---|
NoSuchElementException | El elemento aun no existe | Usa WebDriverWait |
ElementClickInterceptedException | Otro elemento tapa el boton | Espera visibilidad o revisa UI |
StaleElementReferenceException | El DOM se actualizo | Busca de nuevo el elemento |
| Chrome no arranca | Driver/navegador incompatible | Actualiza Chrome o Selenium |
| Test pasa solo a veces | Esperas incorrectas o datos compartidos | Aisla datos y espera condiciones |
18. Practica adicional con DemoQA
Ademas de la web local de Tienda.Selenium, puedes usar https://demoqa.com/ como pagina publica de entrenamiento. DemoQA contiene pantallas preparadas para practicar formularios, botones, tablas, alertas, ventanas, frames, selects, carga de archivos e interacciones.
La web local sigue siendo la base principal del modulo porque controlamos el codigo HTML, los datos y el servidor. DemoQA es util para una segunda fase: el alumno ya conoce Selenium y se enfrenta a una pagina externa, con HTML real, componentes React, anuncios, scroll, esperas y pequenos problemas que aparecen en proyectos reales.
Paginas consultadas para esta seccion:
| Pagina | Uso en el curso |
|---|---|
https://demoqa.com/text-box | Inputs, textareas, submit y comprobacion de salida |
https://demoqa.com/automation-practice-form | Formulario largo, radios, checkboxes, autocompletado, carga de archivo y modal |
https://demoqa.com/webtables | Alta, busqueda, edicion y borrado en tabla |
https://demoqa.com/buttons | Doble clic, clic derecho y clic normal |
https://demoqa.com/alerts | Alert, confirm, prompt y alerta con espera |
https://demoqa.com/select-menu | Select HTML normal y select multiple |
https://demoqa.com/frames | Cambio a iframe y vuelta al documento principal |
https://demoqa.com/dynamic-properties | Esperas sobre elementos que cambian con el tiempo |
Referencias oficiales de Selenium usadas como apoyo:
| Documentacion | Uso en los ejemplos |
|---|---|
https://www.selenium.dev/documentation/webdriver/waits/ | Esperas explicitas con WebDriverWait |
https://www.selenium.dev/documentation/webdriver/actions_api/ | Interacciones complejas con Actions |
https://www.selenium.dev/documentation/webdriver/interactions/alerts/ | Manejo de alertas, confirms y prompts |
https://www.selenium.dev/documentation/webdriver/support_features/select_lists/ | Uso de SelectElement |
Importante: las pruebas contra DemoQA dependen de Internet y de una web que no controlamos. Sirven para practicar, pero no deben mezclarse con el pipeline obligatorio de una aplicacion real. En el curso las marcaremos con
[Category("DemoQA")].
18.1 Crear una base independiente para DemoQA
No vamos a reutilizar SeleniumTestBase directamente porque esa clase arranca la API local de la tienda. Para DemoQA nos interesa abrir solo Chrome y navegar a una web externa.
Crea Support/DemoQaTestBase.cs:
using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
using OpenQA.Selenium.Support.UI;
namespace Tienda.E2E.Tests.Support;
public abstract class DemoQaTestBase
{
protected const string DemoQaBaseUrl = "https://demoqa.com";
protected IWebDriver Driver { get; private set; } = null!;
protected WebDriverWait Wait { get; private set; } = null!;
[SetUp]
public void SetUp()
{
ChromeOptions options = new();
if (Environment.GetEnvironmentVariable("CI") == "true")
options.AddArgument("--headless=new");
options.AddArgument("--window-size=1920,1080");
options.AddArgument("--disable-notifications");
Driver = new ChromeDriver(options);
Wait = new WebDriverWait(Driver, TimeSpan.FromSeconds(12));
}
[TearDown]
public void TearDown()
{
if (TestContext.CurrentContext.Result.Outcome.Status == NUnit.Framework.Interfaces.TestStatus.Failed)
GuardarCaptura(TestContext.CurrentContext.Test.Name);
Driver.Quit();
Driver.Dispose();
}
protected void IrADemoQa(string ruta)
{
Driver.Navigate().GoToUrl($"{DemoQaBaseUrl}{ruta}");
CerrarElementosQueTapanLaPagina();
}
protected IWebElement BuscarVisible(By locator)
{
return Wait.Until(driver =>
{
try
{
IWebElement element = driver.FindElement(locator);
return element.Displayed ? element : null;
}
catch (NoSuchElementException)
{
return null;
}
catch (StaleElementReferenceException)
{
return null;
}
})!;
}
protected IWebElement BuscarDisponible(By locator)
{
return Wait.Until(driver =>
{
try
{
IWebElement element = driver.FindElement(locator);
return element.Displayed && element.Enabled ? element : null;
}
catch (NoSuchElementException)
{
return null;
}
catch (StaleElementReferenceException)
{
return null;
}
})!;
}
protected void Escribir(By locator, string texto)
{
IWebElement element = BuscarDisponible(locator);
ScrollHasta(element);
element.Clear();
element.SendKeys(texto);
}
protected void ClickSeguro(By locator)
{
CerrarElementosQueTapanLaPagina();
IWebElement element = BuscarDisponible(locator);
ClickSeguro(element);
}
protected void ClickSeguro(IWebElement element)
{
CerrarElementosQueTapanLaPagina();
ScrollHasta(element);
element.Click();
}
protected void ScrollHasta(IWebElement element)
{
((IJavaScriptExecutor)Driver).ExecuteScript(
"arguments[0].scrollIntoView({ block: 'center', inline: 'nearest' });",
element);
}
protected IAlert EsperarAlerta()
{
return Wait.Until(driver =>
{
try
{
return driver.SwitchTo().Alert();
}
catch (NoAlertPresentException)
{
return null;
}
})!;
}
protected void GuardarCaptura(string nombre)
{
Screenshot screenshot = ((ITakesScreenshot)Driver).GetScreenshot();
Directory.CreateDirectory("screenshots");
string path = Path.Combine("screenshots", $"{nombre}-{DateTime.UtcNow:yyyyMMddHHmmss}.png");
screenshot.SaveAsFile(path);
}
private void CerrarElementosQueTapanLaPagina()
{
((IJavaScriptExecutor)Driver).ExecuteScript(
"document.querySelectorAll('iframe[id^=\"google_ads\"], iframe[name^=\"google_ads\"], #fixedban, footer').forEach(e => e.remove());");
}
}
Que aporta esta clase:
| Metodo | Para que sirve |
|---|---|
IrADemoQa | Navega a una ruta de DemoQA y limpia elementos fijos que pueden tapar botones |
BuscarVisible | Espera hasta que un elemento exista y sea visible |
BuscarDisponible | Espera hasta que exista, sea visible y este habilitado |
Escribir | Limpia un input y escribe texto |
ClickSeguro | Hace scroll al elemento antes de hacer clic |
EsperarAlerta | Espera hasta que el navegador tenga una alerta activa |
GuardarCaptura | Genera una captura si la prueba falla |
Nota docente: quitar anuncios con JavaScript no es una tecnica para probar una aplicacion propia. Aqui se usa porque DemoQA es una web publica de practica y algunos elementos externos pueden interceptar clics durante la clase.
18.2 Text Box: inputs, textareas y resultado
Crea Tests/DemoQa/DemoQaTextBoxTests.cs:
using OpenQA.Selenium;
using Tienda.E2E.Tests.Support;
namespace Tienda.E2E.Tests.Tests.DemoQa;
[TestFixture]
[Category("DemoQA")]
public class DemoQaTextBoxTests : DemoQaTestBase
{
[Test]
public void TextBox_EnviaFormulario_MuestraLosDatosIntroducidos()
{
IrADemoQa("/text-box");
Escribir(By.Id("userName"), "Ana Garcia");
Escribir(By.Id("userEmail"), "ana.garcia@example.com");
Escribir(By.Id("currentAddress"), "Calle Mayor 1");
Escribir(By.Id("permanentAddress"), "Calle Luna 2");
ClickSeguro(By.Id("submit"));
IWebElement output = BuscarVisible(By.Id("output"));
Assert.Multiple(() =>
{
Assert.That(output.Text, Does.Contain("Ana Garcia"));
Assert.That(output.Text, Does.Contain("ana.garcia@example.com"));
Assert.That(output.Text, Does.Contain("Calle Mayor 1"));
Assert.That(output.Text, Does.Contain("Calle Luna 2"));
});
}
[Test]
public void TextBox_EmailInvalido_MarcaElInputComoError()
{
IrADemoQa("/text-box");
Escribir(By.Id("userName"), "Ana Garcia");
Escribir(By.Id("userEmail"), "email-invalido");
ClickSeguro(By.Id("submit"));
IWebElement email = BuscarVisible(By.Id("userEmail"));
Assert.That(email.GetAttribute("class"), Does.Contain("field-error"));
}
}
Puntos a explicar:
By.Id("userName")es un localizador robusto porque elididentifica directamente el input.output.Textpermite comprobar el resultado visible que ve el usuario.- En una prueba E2E no comprobamos metodos internos, comprobamos comportamiento observable.
- La prueba de email invalido no busca un mensaje de servidor, busca el cambio visual que aplica la pagina.
18.3 Practice Form: formulario largo, radio, checkbox, autocompletado y upload
Esta pantalla es muy util para practicar formularios reales. Tiene inputs normales, radios ocultos detras de etiquetas, checkboxes, campos con autocompletado, seleccion de fecha, subida de archivo y modal final.
Crea Tests/DemoQa/DemoQaPracticeFormTests.cs:
using OpenQA.Selenium;
using Tienda.E2E.Tests.Support;
namespace Tienda.E2E.Tests.Tests.DemoQa;
[TestFixture]
[Category("DemoQA")]
public class DemoQaPracticeFormTests : DemoQaTestBase
{
[Test]
public void PracticeForm_ConDatosValidos_MuestraModalDeConfirmacion()
{
IrADemoQa("/automation-practice-form");
Escribir(By.Id("firstName"), "Laura");
Escribir(By.Id("lastName"), "Perez");
Escribir(By.Id("userEmail"), "laura.perez@example.com");
ClickSeguro(By.CssSelector("label[for='gender-radio-2']"));
Escribir(By.Id("userNumber"), "612345678");
SeleccionarFecha("15 Jan 2000");
SeleccionarSubject("Maths");
ClickSeguro(By.CssSelector("label[for='hobbies-checkbox-2']"));
SubirArchivoDePrueba();
Escribir(By.Id("currentAddress"), "Avenida Central 10");
SeleccionarEstadoYCiudad("NCR", "Delhi");
ClickSeguro(By.Id("submit"));
IWebElement tituloModal = BuscarVisible(By.Id("example-modal-sizes-title"));
IWebElement tablaModal = BuscarVisible(By.CssSelector(".table-responsive"));
Assert.Multiple(() =>
{
Assert.That(tituloModal.Text, Is.EqualTo("Thanks for submitting the form"));
Assert.That(tablaModal.Text, Does.Contain("Laura Perez"));
Assert.That(tablaModal.Text, Does.Contain("laura.perez@example.com"));
Assert.That(tablaModal.Text, Does.Contain("Female"));
Assert.That(tablaModal.Text, Does.Contain("612345678"));
Assert.That(tablaModal.Text, Does.Contain("Maths"));
Assert.That(tablaModal.Text, Does.Contain("Reading"));
Assert.That(tablaModal.Text, Does.Contain("NCR Delhi"));
});
}
private void SeleccionarFecha(string fecha)
{
IWebElement fechaNacimiento = BuscarDisponible(By.Id("dateOfBirthInput"));
ScrollHasta(fechaNacimiento);
fechaNacimiento.SendKeys(Keys.Control + "a");
fechaNacimiento.SendKeys(fecha);
fechaNacimiento.SendKeys(Keys.Enter);
}
private void SeleccionarSubject(string subject)
{
IWebElement subjects = BuscarDisponible(By.Id("subjectsInput"));
ScrollHasta(subjects);
subjects.SendKeys(subject);
subjects.SendKeys(Keys.Enter);
}
private void SeleccionarEstadoYCiudad(string estado, string ciudad)
{
IWebElement state = BuscarDisponible(By.Id("react-select-3-input"));
state.SendKeys(estado);
state.SendKeys(Keys.Enter);
IWebElement city = BuscarDisponible(By.Id("react-select-4-input"));
city.SendKeys(ciudad);
city.SendKeys(Keys.Enter);
}
private void SubirArchivoDePrueba()
{
string path = Path.Combine(TestContext.CurrentContext.WorkDirectory, "demoqa-upload.txt");
File.WriteAllText(path, "Archivo temporal creado por Selenium.");
Driver.FindElement(By.Id("uploadPicture")).SendKeys(path);
}
}
Puntos a explicar:
- Los radios y checkboxes de DemoQA tienen inputs ocultos; por eso hacemos clic en el
label. SendKeys(Keys.Control + "a")selecciona el texto actual de la fecha para reemplazarlo.- El campo
subjectsInputes un componente React: se escribe una opcion y se confirma conEnter. - Para subir archivos no se abre el explorador de Windows. Selenium envia la ruta del fichero al input
type="file". - El modal final es el resultado de negocio de esta pantalla: si aparece y contiene los datos, el flujo funciona.
18.4 Web Tables: alta, busqueda, edicion y borrado
La pantalla webtables permite practicar acciones muy comunes en aplicaciones de gestion: crear un registro, buscarlo, editarlo y eliminarlo.
Crea Tests/DemoQa/DemoQaWebTablesTests.cs:
using OpenQA.Selenium;
using Tienda.E2E.Tests.Support;
namespace Tienda.E2E.Tests.Tests.DemoQa;
[TestFixture]
[Category("DemoQA")]
public class DemoQaWebTablesTests : DemoQaTestBase
{
[Test]
public void WebTables_CrearBuscarEditarYEliminarRegistro()
{
IrADemoQa("/webtables");
string email = $"laura.{Guid.NewGuid():N}@example.com";
CrearRegistro("Laura", "Perez", "34", email, "35000", "QA");
Buscar(email);
IWebElement fila = BuscarFilaPorEmail(email);
Assert.That(fila.Text, Does.Contain("Laura"));
Assert.That(fila.Text, Does.Contain("QA"));
EditarEdadDeFila(fila, "35");
Buscar(email);
IWebElement filaEditada = BuscarFilaPorEmail(email);
Assert.That(filaEditada.Text, Does.Contain("35"));
EliminarFila(filaEditada);
Wait.Until(driver => !driver.FindElement(By.CssSelector(".rt-table")).Text.Contains(email));
Assert.That(Driver.FindElement(By.CssSelector(".rt-table")).Text, Does.Not.Contain(email));
}
private void CrearRegistro(
string firstName,
string lastName,
string age,
string email,
string salary,
string department)
{
ClickSeguro(By.Id("addNewRecordButton"));
Escribir(By.Id("firstName"), firstName);
Escribir(By.Id("lastName"), lastName);
Escribir(By.Id("age"), age);
Escribir(By.Id("userEmail"), email);
Escribir(By.Id("salary"), salary);
Escribir(By.Id("department"), department);
ClickSeguro(By.Id("submit"));
}
private void Buscar(string texto)
{
Escribir(By.Id("searchBox"), texto);
}
private IWebElement BuscarFilaPorEmail(string email)
{
return BuscarVisible(By.XPath($"//div[contains(@class,'rt-tr-group')][.//div[text()='{email}']]"));
}
private void EditarEdadDeFila(IWebElement fila, string nuevaEdad)
{
ClickSeguro(fila.FindElement(By.CssSelector("[title='Edit']")));
IWebElement age = BuscarDisponible(By.Id("age"));
age.SendKeys(Keys.Control + "a");
age.SendKeys(nuevaEdad);
ClickSeguro(By.Id("submit"));
}
private void EliminarFila(IWebElement fila)
{
ClickSeguro(fila.FindElement(By.CssSelector("[title='Delete']")));
}
}
Puntos a explicar:
- El email usa
Guid.NewGuid()para que cada ejecucion tenga un dato unico. - La busqueda reduce la tabla a la fila que nos interesa.
- El XPath busca una fila que contenga una celda con el email exacto.
- Primero hacemos una afirmacion tras crear, despues otra tras editar y finalmente comprobamos que el email ya no aparece.
18.5 Buttons: doble clic, clic derecho y clic normal
La pantalla buttons sirve para introducir Actions, la API de Selenium para interacciones de usuario mas complejas.
Crea Tests/DemoQa/DemoQaButtonsTests.cs:
using OpenQA.Selenium;
using OpenQA.Selenium.Interactions;
using Tienda.E2E.Tests.Support;
namespace Tienda.E2E.Tests.Tests.DemoQa;
[TestFixture]
[Category("DemoQA")]
public class DemoQaButtonsTests : DemoQaTestBase
{
[Test]
public void Buttons_PermiteDobleClickClickDerechoYClickNormal()
{
IrADemoQa("/buttons");
Actions actions = new(Driver);
IWebElement doubleClickButton = BuscarVisible(By.Id("doubleClickBtn"));
ScrollHasta(doubleClickButton);
actions.DoubleClick(doubleClickButton).Perform();
IWebElement rightClickButton = BuscarVisible(By.Id("rightClickBtn"));
ScrollHasta(rightClickButton);
actions.ContextClick(rightClickButton).Perform();
IWebElement clickMeButton = BuscarVisible(By.XPath("//button[normalize-space()='Click Me']"));
ClickSeguro(clickMeButton);
Assert.Multiple(() =>
{
Assert.That(BuscarVisible(By.Id("doubleClickMessage")).Text, Does.Contain("double click"));
Assert.That(BuscarVisible(By.Id("rightClickMessage")).Text, Does.Contain("right click"));
Assert.That(BuscarVisible(By.Id("dynamicClickMessage")).Text, Does.Contain("dynamic click"));
});
}
}
Puntos a explicar:
element.Click()cubre el clic normal.Actions.DoubleClick(element).Perform()simula doble clic real.Actions.ContextClick(element).Perform()simula clic derecho.Perform()ejecuta la accion que se ha construido.
18.6 Alerts: alert, confirm, prompt y alerta diferida
Crea Tests/DemoQa/DemoQaAlertsTests.cs:
using OpenQA.Selenium;
using Tienda.E2E.Tests.Support;
namespace Tienda.E2E.Tests.Tests.DemoQa;
[TestFixture]
[Category("DemoQA")]
public class DemoQaAlertsTests : DemoQaTestBase
{
[Test]
public void Alerts_AlertNormal_SeAcepta()
{
IrADemoQa("/alerts");
ClickSeguro(By.Id("alertButton"));
IAlert alert = EsperarAlerta();
Assert.That(alert.Text, Does.Contain("You clicked a button"));
alert.Accept();
}
[Test]
public void Alerts_AlertaDiferida_EsperaHastaQueAparece()
{
IrADemoQa("/alerts");
ClickSeguro(By.Id("timerAlertButton"));
IAlert alert = EsperarAlerta();
Assert.That(alert.Text, Does.Contain("This alert appeared after 5 seconds"));
alert.Accept();
}
[Test]
public void Alerts_Confirm_SePuedeCancelar()
{
IrADemoQa("/alerts");
ClickSeguro(By.Id("confirmButton"));
IAlert alert = EsperarAlerta();
alert.Dismiss();
Assert.That(BuscarVisible(By.Id("confirmResult")).Text, Does.Contain("Cancel"));
}
[Test]
public void Alerts_Prompt_EnviaTexto()
{
IrADemoQa("/alerts");
ClickSeguro(By.Id("promtButton"));
IAlert alert = EsperarAlerta();
alert.SendKeys("Fran");
alert.Accept();
Assert.That(BuscarVisible(By.Id("promptResult")).Text, Does.Contain("Fran"));
}
}
Puntos a explicar:
- Cuando hay una alerta abierta, Selenium no puede interactuar con la pagina normal hasta aceptarla o cancelarla.
Driver.SwitchTo().Alert()cambia el foco del driver a la alerta.Accept()equivale a aceptar.Dismiss()equivale a cancelar.SendKeys()solo tiene sentido en prompts que aceptan texto.
18.7 Select Menu: selects HTML normales y multiples
DemoQA mezcla selects HTML reales con componentes React. SelectElement solo sirve para etiquetas HTML <select>, no para desplegables dibujados con div.
Crea Tests/DemoQa/DemoQaSelectMenuTests.cs:
using OpenQA.Selenium;
using OpenQA.Selenium.Support.UI;
using Tienda.E2E.Tests.Support;
namespace Tienda.E2E.Tests.Tests.DemoQa;
[TestFixture]
[Category("DemoQA")]
public class DemoQaSelectMenuTests : DemoQaTestBase
{
[Test]
public void SelectMenu_OldStyleSelect_PermiteSeleccionPorTexto()
{
IrADemoQa("/select-menu");
SelectElement oldSelect = new(BuscarVisible(By.Id("oldSelectMenu")));
oldSelect.SelectByText("Blue");
Assert.That(oldSelect.SelectedOption.Text, Is.EqualTo("Blue"));
}
[Test]
public void SelectMenu_MultipleSelect_PermiteSeleccionarVariasOpciones()
{
IrADemoQa("/select-menu");
SelectElement cars = new(BuscarVisible(By.Id("cars")));
cars.SelectByText("Volvo");
cars.SelectByText("Audi");
List<string> seleccionados = cars.AllSelectedOptions
.Select(option => option.Text)
.ToList();
Assert.Multiple(() =>
{
Assert.That(seleccionados, Does.Contain("Volvo"));
Assert.That(seleccionados, Does.Contain("Audi"));
});
}
}
Puntos a explicar:
SelectElementesta enSelenium.Support.SelectByTextselecciona por el texto visible.SelectedOptiondevuelve la opcion activa de un select simple.AllSelectedOptionsdevuelve todas las opciones activas de un select multiple.
18.8 Frames: cambiar al iframe y volver
Crea Tests/DemoQa/DemoQaFramesTests.cs:
using OpenQA.Selenium;
using Tienda.E2E.Tests.Support;
namespace Tienda.E2E.Tests.Tests.DemoQa;
[TestFixture]
[Category("DemoQA")]
public class DemoQaFramesTests : DemoQaTestBase
{
[Test]
public void Frames_CambiaAlFrameYLeeContenido()
{
IrADemoQa("/frames");
Driver.SwitchTo().Frame("frame1");
IWebElement heading = BuscarVisible(By.Id("sampleHeading"));
Assert.That(heading.Text, Is.EqualTo("This is a sample page"));
Driver.SwitchTo().DefaultContent();
Assert.That(BuscarVisible(By.TagName("body")).Text, Does.Contain("Frames"));
}
}
Puntos a explicar:
- Un iframe es un documento HTML dentro de otro documento.
- Selenium no puede encontrar elementos dentro del iframe hasta hacer
SwitchTo().Frame(...). - Para volver a la pagina principal usamos
SwitchTo().DefaultContent().
18.9 Dynamic Properties: esperar cambios en la pagina
Esta pantalla es perfecta para explicar por que no usamos Thread.Sleep. Algunos elementos se habilitan o aparecen tras varios segundos.
Crea Tests/DemoQa/DemoQaDynamicPropertiesTests.cs:
using OpenQA.Selenium;
using Tienda.E2E.Tests.Support;
namespace Tienda.E2E.Tests.Tests.DemoQa;
[TestFixture]
[Category("DemoQA")]
public class DemoQaDynamicPropertiesTests : DemoQaTestBase
{
[Test]
public void DynamicProperties_EsperaHastaQueBotonEsteHabilitado()
{
IrADemoQa("/dynamic-properties");
IWebElement boton = Wait.Until(driver =>
{
IWebElement element = driver.FindElement(By.Id("enableAfter"));
return element.Enabled ? element : null;
})!;
ClickSeguro(boton);
Assert.That(boton.Enabled, Is.True);
}
[Test]
public void DynamicProperties_EsperaHastaQueElementoSeaVisible()
{
IrADemoQa("/dynamic-properties");
IWebElement visibleAfter = BuscarVisible(By.Id("visibleAfter"));
Assert.That(visibleAfter.Displayed, Is.True);
}
}
Puntos a explicar:
WebDriverWaitejecuta la condicion varias veces hasta que se cumple o se agota el tiempo.- La condicion debe devolver el elemento cuando esta listo.
- Si devuelve
null, Selenium sigue esperando. - Esta tecnica hace las pruebas mas estables que
Thread.Sleep.
18.10 Page Object aplicado a DemoQA
Una vez que los alumnos han escrito los tests directos, se puede refactorizar una pantalla a Page Object.
Crea Pages/DemoQaTextBoxPage.cs:
using OpenQA.Selenium;
using OpenQA.Selenium.Support.UI;
namespace Tienda.E2E.Tests.Pages;
public class DemoQaTextBoxPage
{
private readonly IWebDriver _driver;
private readonly WebDriverWait _wait;
public DemoQaTextBoxPage(IWebDriver driver)
{
_driver = driver;
_wait = new WebDriverWait(driver, TimeSpan.FromSeconds(10));
}
public void Completar(
string nombre,
string email,
string direccionActual,
string direccionPermanente)
{
Escribir(By.Id("userName"), nombre);
Escribir(By.Id("userEmail"), email);
Escribir(By.Id("currentAddress"), direccionActual);
Escribir(By.Id("permanentAddress"), direccionPermanente);
}
public void Enviar()
{
IWebElement submit = BuscarVisible(By.Id("submit"));
((IJavaScriptExecutor)_driver).ExecuteScript(
"arguments[0].scrollIntoView({ block: 'center' });",
submit);
submit.Click();
}
public string Resultado()
{
return BuscarVisible(By.Id("output")).Text;
}
private void Escribir(By locator, string texto)
{
IWebElement element = BuscarVisible(locator);
element.Clear();
element.SendKeys(texto);
}
private IWebElement BuscarVisible(By locator)
{
return _wait.Until(driver =>
{
IWebElement element = driver.FindElement(locator);
return element.Displayed ? element : null;
})!;
}
}
Uso del Page Object en una prueba:
using Tienda.E2E.Tests.Pages;
using Tienda.E2E.Tests.Support;
namespace Tienda.E2E.Tests.Tests.DemoQa;
[TestFixture]
[Category("DemoQA")]
public class DemoQaTextBoxPageObjectTests : DemoQaTestBase
{
[Test]
public void TextBox_ConPageObject_MuestraDatos()
{
IrADemoQa("/text-box");
DemoQaTextBoxPage page = new(Driver);
page.Completar(
"Carlos Ruiz",
"carlos.ruiz@example.com",
"Calle Norte 3",
"Calle Sur 4");
page.Enviar();
Assert.That(page.Resultado(), Does.Contain("Carlos Ruiz"));
}
}
Puntos a explicar:
- El test queda mas corto y expresa la intencion de usuario.
- Los selectores quedan dentro del Page Object.
- Si cambia el HTML de la pagina, normalmente se modifica el Page Object y no todas las pruebas.
- No hace falta crear Page Object desde el primer minuto; primero conviene entender el flujo directo.
18.11 Ejecucion de pruebas DemoQA
Para ejecutar solo las pruebas de DemoQA:
dotnet test --filter "TestCategory=DemoQA"
Para ejecutar todo excepto DemoQA:
dotnet test --filter "TestCategory!=DemoQA"
En Visual Studio Community:
- Abre Test Explorer.
- Compila la solucion.
- Filtra por categoria
DemoQA. - Ejecuta primero una prueba visible, sin modo headless.
- Si todo funciona, prueba con:
$env:CI="true"
dotnet test --filter "TestCategory=DemoQA"
18.12 Problemas habituales con DemoQA
| Problema | Causa probable | Solucion didactica |
|---|---|---|
ElementClickInterceptedException | Un anuncio o footer tapa el elemento | Hacer scroll, esperar visibilidad o usar el helper de limpieza |
NoSuchElementException | La pagina aun no ha pintado el componente React | Usar WebDriverWait |
| El radio no se selecciona | El input real esta oculto | Hacer clic en el label[for='...'] |
| El subject no se selecciona | Es un autocomplete React | Escribir el texto y confirmar con Keys.Enter |
| El test falla solo en CI | Tamano de ventana o tiempos distintos | Fijar --window-size y aumentar la espera |
| El sitio externo esta caido | DemoQA no depende de nuestro proyecto | Repetir mas tarde o continuar con la web local |
18.13 Mini plan docente con DemoQA
Una ruta de 90 a 120 minutos:
- Abrir manualmente
https://demoqa.com/text-boxy localizar ids desde DevTools. - Crear
DemoQaTestBase. - Automatizar
Text Boxsin Page Object. - Provocar un fallo cambiando un id y revisar la captura.
- Automatizar
Practice Formpaso a paso. - Explicar por que los radios se pulsan por
label. - Automatizar
Buttonspara introducirActions. - Automatizar
Alertspara practicarSwitchTo().Alert(). - Automatizar
Web Tablespara practicar datos dinamicos. - Refactorizar
Text Boxa Page Object. - Ejecutar solo la categoria
DemoQA. - Comparar con la web local de la tienda: control, estabilidad y mantenimiento.
19. Guion de explicacion en Visual Studio
- Ensena la diferencia entre test unitario y E2E.
- Crea la solucion con API local.
- Abre la API en navegador para ver las paginas.
- Crea
SeleniumTestBase. - Ejecuta
PrimerTestcon Chrome visible. - Rellena formulario de producto.
- Rompe un selector para provocar
NoSuchElementException. - Sustituye busqueda directa por
WebDriverWait. - Extrae
LoginPage. - Prueba
/controles. - Prueba pestanas y alertas.
- Prepara datos por API.
- Ejecuta en headless.
- Revisa capturas en fallo.
- Crea
DemoQaTestBase. - Ejecuta una prueba contra
https://demoqa.com/text-box. - Explica la diferencia entre probar una web propia y una web publica.
- Ejecuta la categoria
DemoQA.
20. Ejercicios
Ejercicio 1: Login
Automatiza login correcto, password incorrecto y usuario vacio.
Ejercicio 2: CRUD de productos
Crea, busca, edita y elimina un producto desde el navegador.
Ejercicio 3: Page Objects
Refactoriza productos usando ProductosPage y ProductoFormPage.
Ejercicio 4: Controles avanzados
Agrega mas opciones a /controles y prueba select multiple, checkbox desmarcado y radio normal.
Ejercicio 5: Capturas
Guarda capturas automaticamente cuando falle una prueba.
Ejercicio 6: DemoQA Text Box
Crea una prueba contra https://demoqa.com/text-box que rellene todos los campos y compruebe que el bloque de salida contiene los datos.
Ejercicio 7: DemoQA Practice Form
Automatiza el formulario de estudiantes con datos validos. Debe seleccionar genero, hobby, subject, estado, ciudad y subir un archivo temporal.
Ejercicio 8: DemoQA Web Tables
Crea un registro, buscalo por email, editalo, valida el cambio y eliminalo.
Ejercicio 9: DemoQA Alerts
Crea cuatro pruebas: alert normal, alert diferida, confirm cancelado y prompt con texto.
Ejercicio 10: DemoQA Page Object
Refactoriza DemoQaPracticeFormTests creando un Page Object para la pantalla completa.
Resumen
| Concepto | Sintaxis |
|---|---|
| Driver Chrome | new ChromeDriver(options) |
| Navegar | Driver.Navigate().GoToUrl(url) |
| Buscar por id | Driver.FindElement(By.Id("id")) |
| Buscar por CSS | Driver.FindElement(By.CssSelector("...")) |
| Escribir | element.SendKeys("texto") |
| Click | element.Click() |
| Espera explicita | new WebDriverWait(driver, timeout) |
| Select | new SelectElement(element) |
| Alerta | Driver.SwitchTo().Alert() |
| Pestana | Driver.SwitchTo().Window(handle) |
| Headless | options.AddArgument("--headless=new") |