Módulo 10: Automatización UI con Selenium WebDriver

Objetivos del módulo

  • Configurar Selenium WebDriver con C# y .NET
  • Dominar la localización de elementos (selectores)
  • Implementar esperas (waits) correctamente
  • Aplicar el patrón Page Object Model (POM)
  • Ejecutar tests en diferentes navegadores

1. ¿Qué es Selenium?

Selenium WebDriver es una herramienta de automatización que permite controlar un navegador de forma programática.

Componente Descripción
WebDriver API para controlar el navegador
Browser Driver Ejecutable que conecta WebDriver con el navegador (chromedriver, geckodriver)
Selenium Grid Ejecución en paralelo/remota
graph LR
    A[Test C#] --> B[Selenium WebDriver]
    B --> C[ChromeDriver]
    C --> D[Chrome]
    B --> E[GeckoDriver]
    E --> F[Firefox]
    style A fill:#2196F3,color:white
    style D fill:#4CAF50,color:white

2. Configurar el proyecto

dotnet new nunit -n MiApp.UITests
dotnet sln add MiApp.UITests
cd MiApp.UITests

# Paquetes necesarios
dotnet add package Selenium.WebDriver
dotnet add package Selenium.WebDriver.ChromeDriver
dotnet add package Selenium.Support

Primer test

using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;

namespace MiApp.UITests;

[TestFixture]
public class GoogleTests
{
    private IWebDriver driver;

    [SetUp]
    public void Setup()
    {
        var options = new ChromeOptions();
        options.AddArgument("--start-maximized");   // Pantalla completa
        // options.AddArgument("--headless");        // Sin ventana visible
        driver = new ChromeDriver(options);
        driver.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(5);
    }

    [Test]
    public void Google_BuscarSelenium_MuestraResultados()
    {
        // Navegar a Google
        driver.Navigate().GoToUrl("https://www.google.com");

        // Aceptar cookies si aparece el botón
        try
        {
            driver.FindElement(By.Id("L2AGLb")).Click();
        }
        catch (NoSuchElementException) { }

        // Buscar "Selenium WebDriver"
        var searchBox = driver.FindElement(By.Name("q"));
        searchBox.SendKeys("Selenium WebDriver");
        searchBox.SendKeys(Keys.Enter);

        // Verificar que hay resultados
        Assert.That(driver.Title, Does.Contain("Selenium"));
    }

    [TearDown]
    public void TearDown()
    {
        driver.Quit();    // Cerrar navegador
        driver.Dispose();
    }
}

3. Localización de elementos

Estrategias de localización (By)

Selector Sintaxis Ejemplo HTML Código
Id By.Id("id") <input id="email"> By.Id("email")
Name By.Name("name") <input name="q"> By.Name("q")
ClassName By.ClassName("clase") <div class="alert"> By.ClassName("alert")
TagName By.TagName("tag") <h1>Título</h1> By.TagName("h1")
LinkText By.LinkText("texto") <a>Ir a inicio</a> By.LinkText("Ir a inicio")
CSS By.CssSelector("css") <input class="btn primary"> By.CssSelector(".btn.primary")
XPath By.XPath("xpath") Cualquier elemento By.XPath("//input[@type='email']")

Selectores CSS más usados

// Por ID
By.CssSelector("#email")                    // <input id="email">

// Por clase
By.CssSelector(".btn-primary")               // <button class="btn-primary">

// Por atributo
By.CssSelector("input[type='email']")        // <input type="email">
By.CssSelector("[data-testid='login-btn']")  // <button data-testid="login-btn">

// Descendientes
By.CssSelector("form .input-group input")    // input dentro de .input-group dentro de form

// Hijo directo
By.CssSelector("ul > li")                    // li hijo directo de ul

// Nth-child
By.CssSelector("table tr:nth-child(2)")      // Segunda fila de una tabla

XPath más usados

// Por atributo
By.XPath("//input[@id='email']")

// Por texto
By.XPath("//button[text()='Enviar']")
By.XPath("//a[contains(text(), 'Inicio')]")

// Por posición
By.XPath("//table//tr[2]/td[3]")         // 2ª fila, 3ª columna

// Eje padre
By.XPath("//span[@class='error']/..")     // Padre del span

// Combinar condiciones
By.XPath("//input[@type='text' and @name='user']")

💡 Consejo: Prioriza selectores en este orden: Id > data-testid > CSS > XPath. XPath es más potente pero más frágil.


4. Interacciones con elementos

// Encontrar elemento
IWebElement elemento = driver.FindElement(By.Id("email"));

// Acciones comunes
elemento.Click();                          // Hacer clic
elemento.SendKeys("texto");               // Escribir
elemento.Clear();                         // Limpiar campo
elemento.Submit();                        // Enviar formulario

// Obtener información
string texto = elemento.Text;             // Texto visible
string valor = elemento.GetAttribute("value");  // Atributo value
string clase = elemento.GetAttribute("class");  // Atributo class
bool visible = elemento.Displayed;        // ¿Es visible?
bool habilitado = elemento.Enabled;       // ¿Está habilitado?
bool seleccionado = elemento.Selected;    // ¿Está seleccionado? (checkbox)

// Encontrar múltiples elementos
IReadOnlyCollection<IWebElement> items = driver.FindElements(By.CssSelector(".item"));
using OpenQA.Selenium.Support.UI;

var select = new SelectElement(driver.FindElement(By.Id("pais")));
select.SelectByText("España");           // Por texto visible
select.SelectByValue("ES");              // Por atributo value
select.SelectByIndex(2);                 // Por posición

Alertas JavaScript

IAlert alert = driver.SwitchTo().Alert();
string texto = alert.Text;    // Leer mensaje
alert.Accept();                // Aceptar
alert.Dismiss();               // Cancelar
alert.SendKeys("respuesta");   // Escribir en prompt

Frames e iframes

driver.SwitchTo().Frame("nombreFrame");    // Entrar al frame
driver.SwitchTo().DefaultContent();        // Volver al contenido principal

Ventanas y pestañas

string ventanaOriginal = driver.CurrentWindowHandle;

// Abrir nueva pestaña
driver.SwitchTo().NewWindow(WindowType.Tab);

// Cambiar entre ventanas
foreach (var handle in driver.WindowHandles)
{
    driver.SwitchTo().Window(handle);
}

// Volver a la original
driver.SwitchTo().Window(ventanaOriginal);

5. Esperas (Waits)

El problema

El navegador carga elementos de forma asíncrona. Sin esperas, el test puede fallar porque el elemento aún no existe.

Implicit Wait (no recomendado como única estrategia)

driver.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(10);
// Espera hasta 10 segundos al buscar cualquier elemento

Explicit Wait (recomendado)

var wait = new WebDriverWait(driver, TimeSpan.FromSeconds(10));

// Esperar a que un elemento sea visible
IWebElement elemento = wait.Until(
    ExpectedConditions.ElementIsVisible(By.Id("resultado"))
);

// Esperar a que sea clicable
IWebElement boton = wait.Until(
    ExpectedConditions.ElementToBeClickable(By.Id("enviar"))
);

// Esperar a que el título contenga texto
wait.Until(ExpectedConditions.TitleContains("Dashboard"));

// Esperar a que desaparezca un loading
wait.Until(ExpectedConditions.InvisibilityOfElementLocated(
    By.CssSelector(".spinner")
));

// Esperar condición personalizada
wait.Until(d => d.FindElements(By.CssSelector(".item")).Count > 5);

⚠️ Importante: NUNCA uses Thread.Sleep() en tests de Selenium. Es lento, inestable y no se adapta a la velocidad real.


6. Page Object Model (POM)

Concepto

El POM separa la lógica de localización de elementos de la lógica de los tests. Cada página tiene su clase.

graph TD
    T[Tests] --> LP[LoginPage]
    T --> DP[DashboardPage]
    LP --> D[Driver]
    DP --> D
    style T fill:#2196F3,color:white
    style LP fill:#FF9800,color:white
    style DP fill:#FF9800,color:white

Ejemplo: LoginPage

// Pages/LoginPage.cs
public class LoginPage
{
    private readonly IWebDriver _driver;
    private readonly WebDriverWait _wait;

    // Localizadores
    private By EmailInput => By.Id("email");
    private By PasswordInput => By.Id("password");
    private By LoginButton => By.CssSelector("button[type='submit']");
    private By ErrorMessage => By.CssSelector(".alert-danger");

    public LoginPage(IWebDriver driver)
    {
        _driver = driver;
        _wait = new WebDriverWait(driver, TimeSpan.FromSeconds(10));
    }

    public void NavigateTo(string baseUrl)
    {
        _driver.Navigate().GoToUrl($"{baseUrl}/login");
    }

    public DashboardPage LoginAs(string email, string password)
    {
        _wait.Until(ExpectedConditions.ElementIsVisible(EmailInput));
        _driver.FindElement(EmailInput).Clear();
        _driver.FindElement(EmailInput).SendKeys(email);
        _driver.FindElement(PasswordInput).Clear();
        _driver.FindElement(PasswordInput).SendKeys(password);
        _driver.FindElement(LoginButton).Click();

        return new DashboardPage(_driver);
    }

    public string GetErrorMessage()
    {
        var element = _wait.Until(
            ExpectedConditions.ElementIsVisible(ErrorMessage));
        return element.Text;
    }
}

// Pages/DashboardPage.cs
public class DashboardPage
{
    private readonly IWebDriver _driver;
    private By WelcomeMessage => By.CssSelector(".welcome-msg");

    public DashboardPage(IWebDriver driver) => _driver = driver;

    public string GetWelcomeText()
    {
        var wait = new WebDriverWait(_driver, TimeSpan.FromSeconds(10));
        var element = wait.Until(
            ExpectedConditions.ElementIsVisible(WelcomeMessage));
        return element.Text;
    }
}

Tests usando POM

[TestFixture]
public class LoginTests
{
    private IWebDriver _driver;
    private LoginPage _loginPage;
    private const string BaseUrl = "http://localhost:3000";

    [SetUp]
    public void Setup()
    {
        _driver = new ChromeDriver();
        _loginPage = new LoginPage(_driver);
        _loginPage.NavigateTo(BaseUrl);
    }

    [Test]
    public void Login_CredencialesValidas_AccedeAlDashboard()
    {
        var dashboard = _loginPage.LoginAs("admin@test.com", "Pass123!");
        Assert.That(dashboard.GetWelcomeText(), Does.Contain("Bienvenido"));
    }

    [Test]
    public void Login_PasswordIncorrecta_MuestraError()
    {
        _loginPage.LoginAs("admin@test.com", "incorrecta");
        Assert.That(_loginPage.GetErrorMessage(),
            Does.Contain("Credenciales incorrectas"));
    }

    [TearDown]
    public void TearDown()
    {
        _driver.Quit();
        _driver.Dispose();
    }
}

7. Capturas de pantalla

// Captura completa
Screenshot screenshot = ((ITakesScreenshot)driver).GetScreenshot();
screenshot.SaveAsFile("captura.png");

// Captura al fallar un test
[TearDown]
public void TearDown()
{
    if (TestContext.CurrentContext.Result.Outcome.Status ==
        NUnit.Framework.Interfaces.TestStatus.Failed)
    {
        var screenshot = ((ITakesScreenshot)_driver).GetScreenshot();
        var path = Path.Combine(
            TestContext.CurrentContext.TestDirectory,
            $"{TestContext.CurrentContext.Test.Name}_FAIL.png");
        screenshot.SaveAsFile(path);
        TestContext.AddTestAttachment(path);
    }

    _driver.Quit();
}

8. Ejecución en múltiples navegadores

[TestFixture(typeof(ChromeDriver))]
[TestFixture(typeof(FirefoxDriver))]
public class CrossBrowserTests<TDriver> where TDriver : IWebDriver, new()
{
    private IWebDriver _driver;

    [SetUp]
    public void Setup()
    {
        _driver = new TDriver();
    }

    [Test]
    public void PaginaPrincipal_Carga_TituloCorreto()
    {
        _driver.Navigate().GoToUrl("https://example.com");
        Assert.That(_driver.Title, Does.Contain("Example"));
    }

    [TearDown]
    public void TearDown()
    {
        _driver.Quit();
    }
}

9. Ejercicios

Ejercicio 1

Automatiza un test que: abra Wikipedia, busque “Selenium (software)”, y verifique que el artículo contiene “web browser automation”.

Ejercicio 2

Crea un Page Object para la página de búsqueda de Wikipedia con métodos: NavigateTo(), Search(string query), GetArticleTitle(), GetFirstParagraph().

Ejercicio 3

Escribe un test parametrizado que pruebe la búsqueda en Wikipedia con 3 términos diferentes y verifique el título de cada artículo.


Resumen

Concepto Descripción
Selenium WebDriver Controla el navegador programáticamente
By.Id / CssSelector Localizar elementos en la página
Explicit Wait Esperar condiciones antes de actuar
Page Object Model Separar localización de lógica de test
Screenshots Captura al fallar para diagnóstico