Escritura de las pruebas de IU

Completado

En esta sección, ayudará a Andy y Amita a escribir pruebas de Selenium que comprueben los comportamientos de la interfaz de usuario que ha descrito Amita.

Normalmente, Amita ejecuta las pruebas en Chrome, Firefox y Microsoft Edge. Aquí hará lo mismo. El agente hospedado por Microsoft que va a usar está preconfigurado para funcionar con cada uno de estos exploradores.

Captura de la rama de GitHub

En esta sección, capturará la rama selenium desde GitHub. Después, la extraeremos del repositorio, o cambiaremos a esa rama. El contenido de la rama le ayudará a seguir las pruebas que escriben Andy y Amita.

Esta rama contiene el proyecto Space Game con el que ya ha trabajado en los módulos anteriores. También contiene una configuración de Azure Pipelines con la que empezar.

  1. En Visual Studio Code, abra el terminal integrado.

  2. Para descargar una rama denominada selenium desde el repositorio de Microsoft y cambiar a esa rama, ejecute los comandos git fetch y git checkout siguientes:

    git fetch upstream selenium
    git checkout -B selenium upstream/selenium
    

    Sugerencia

    Si ha seguido la prueba manual de Amita en la unidad anterior, es posible que ya haya ejecutado estos comandos. Si ya los ejecutó en la unidad anterior, puede volver a ejecutarlos ahora.

    Recuerde que upstream (ascendente) hace referencia al repositorio de GitHub de Microsoft. La configuración de Git del proyecto entiende el repositorio remoto ascendente porque se ha configurado esa relación al bifurcar el proyecto desde el repositorio de Microsoft y clonarlo localmente.

    En resumen, va a insertar esta rama en el repositorio de GitHub, conocido como origin.

  3. Opcionalmente, en Visual Studio Code, abra el archivo azure-pipelines.yml. Familiarícese con la configuración inicial.

    La configuración es similar a la que ha creado en los módulos anteriores de esta ruta de aprendizaje. Solo compila la configuración de versión de la aplicación. Por motivos de brevedad, también omite los desencadenadores, las aprobaciones manuales y las pruebas que ha configurado en módulos anteriores.

    Nota:

    Una configuración más sólida podría especificar las ramas que participan en el proceso de compilación. Por ejemplo, para facilitar la comprobación de la calidad del código, podríamos ejecutar pruebas unitarias cada vez que insertemos un cambio en cualquier rama. También podríamos implementar la aplicación en un entorno que realice pruebas más exhaustivas. Pero esta implementación solo se realiza cuando se tiene una solicitud de incorporación de cambios, cuando se tiene una versión candidata para lanzamiento o cuando se combina código en la rama principal.

    Para obtener más información, vea Implementación de un flujo de trabajo de código en la canalización de compilación con Git y GitHub y Desencadenadores de canalización de compilación.

Escritura del código de las pruebas unitarias

Amita tiene muchas ganas de aprender a escribir código que controle el explorador web.

Ella y Andy trabajarán juntos para escribir las pruebas de Selenium. Andy ya ha configurado un proyecto de NUnit vacío. A lo largo del proceso, hacen referencia a la documentación de Selenium, a algunos tutoriales en línea y a las notas que tomaron cuando Amita realizó las pruebas manualmente. Al final de este módulo, encontrará más recursos que le ayudarán en el proceso.

Vamos a revisar el proceso que usan Andy y Amita para escribir sus pruebas. Puede seguirlo abriendo el archivo HomePageTest.cs en el directorio Tailspin.SpaceGame.Web.UITests en Visual Studio Code.

Definición de la clase HomePageTest

Andy: Lo primero que debemos hacer es definir nuestra clase de prueba. Podemos optar por seguir una de las convenciones de nomenclatura. Vamos a llamar a nuestra clase HomePageTest. En esta clase, incluiremos todas las pruebas relacionadas con la página principal.

Andy agrega este código a HomePageTest.cs:

public class HomePageTest
{
}

Andy: Necesitamos marcar esta clase como public para que esté disponible para el marco de NUnit.

Adición de la variable miembro IWebDriver

Andy: Ahora necesitamos una variable miembro IWebDriver. IWebDriver es la interfaz de programación que se usa para iniciar un explorador web e interactuar con el contenido de la página web.

Amita: He oído hablar de las interfaces en programación. ¿Me puedes explicar más cosas de ellas?

Andy: Piensa en una interfaz como una especificación o un plano técnico del comportamiento esperado de un componente. Una interfaz proporciona los métodos o comportamientos de dicho componente, pero no proporciona ninguno de los detalles subyacentes. Una persona crea una o varias clases concretas que implementan esa interfaz y Selenium proporciona las clases concretas que necesitamos.

En este diagrama se muestra la interfaz IWebDriver y algunas de las clases que implementa esta interfaz:

Diagram of the IWebDriver interface, its methods, and concrete classes.

En el diagrama se muestran tres métodos que proporciona IWebDriver: Navigate, FindElement y Close.

Las tres clases que se muestran aquí, ChromeDriver, FirefoxDriver y EdgeDriver, implementan IWebDriver y sus métodos. Hay otras clases, como SafariDriver, que también implementan IWebDriver. Cada clase de controlador puede controlar el explorador web que representa.

Andy agrega una variable miembro denominada driver a la clase HomePageTest, como el siguiente código:

public class HomePageTest
{
    private IWebDriver driver;
}

Definición de los accesorios de prueba

Andy: Queremos ejecutar todo el conjunto de pruebas en Chrome, Firefox y Edge. En NUnit, podemos usar los accesorios de prueba para ejecutar todo el conjunto de pruebas varias veces, una vez para cada explorador en el que queremos llevar a cabo las pruebas.

En NUnit, se usa el atributo TestFixture para definir los accesorios de prueba. Andy agrega tres accesorios de prueba a la clase HomePageTest:

[TestFixture("Chrome")]
[TestFixture("Firefox")]
[TestFixture("Edge")]
public class HomePageTest
{
    private IWebDriver driver;
}

Andy: Después, tenemos que definir un constructor para nuestra clase de prueba. Se llama al constructor cuando NUnit crea una instancia de esta clase. Como argumento, el constructor toma la cadena que hemos adjuntado a nuestros accesorios de prueba. Este es el aspecto del código:

[TestFixture("Chrome")]
[TestFixture("Firefox")]
[TestFixture("Edge")]
public class HomePageTest
{
    private string browser;
    private IWebDriver driver;

    public HomePageTest(string browser)
    {
        this.browser = browser;
    }
}

Andy: Agregamos la variable miembro browser para poder usar el nombre real del explorador en nuestro código de configuración. Ahora vamos a escribir el código de configuración.

Definición del método Setup

Andy: Necesitamos asignar nuestra variable miembro IWebDriver a una instancia de clase que implemente esta interfaz para el explorador en el que estamos realizando las pruebas. Las clases ChromeDriver, FirefoxDriver y EdgeDriver implementan esta interfaz para Chrome, Firefox y Edge, respectivamente.

Vamos a crear un método denominado Setup que establece la variable driver. Usamos el atributo OneTimeSetUp para indicar a NUnit que ejecute este método una sola vez por cada accesorio de prueba.

[OneTimeSetUp]
public void Setup()
{
}

En el método Setup podemos usar una instrucción switch para asignar la variable miembro driver a la implementación concreta adecuada en función del nombre del explorador. Vamos a agregar el código.

// Create the driver for the current browser.
switch(browser)
{
    case "Chrome":
    driver = new ChromeDriver(
        Environment.GetEnvironmentVariable("ChromeWebDriver")
    );
    break;
    case "Firefox":
    driver = new FirefoxDriver(
        Environment.GetEnvironmentVariable("GeckoWebDriver")
    );
    break;
    case "Edge":
    driver = new EdgeDriver(
        Environment.GetEnvironmentVariable("EdgeWebDriver"),
        new EdgeOptions
        {
            UseChromium = true
        }
    );
    break;
    default:
    throw new ArgumentException($"'{browser}': Unknown browser");
}

El constructor de cada clase de controlador toma una ruta de acceso opcional al software de controlador que necesita Selenium para controlar el explorador web. Más adelante analizaremos el rol de las variables de entorno que se muestran aquí.

En este ejemplo, el constructor EdgeDriver también requiere opciones adicionales para especificar que queremos usar la versión Chromium de Edge.

Definición de métodos auxiliares

Andy: Sé que tendremos que repetir dos acciones a lo largo de las pruebas:

  • Buscar elementos en la página, como los vínculos en los que hacemos clic y las ventanas modales que esperamos que aparezcan.
  • Hacer clic en los elementos de la página, como los vínculos que revelan las ventanas modales y el botón que cierra cada ventana modal.

Vamos a escribir dos métodos auxiliares, uno para cada acción. Comenzaremos con el método que busca un elemento en la página.

Escritura del método auxiliar FindElement

Cuando se encuentra un elemento en la página, suele ser en respuesta a algún otro evento, como la carga de la página o la escritura de información por parte del usuario. Selenium proporciona la clase WebDriverWait, la cual le permite esperar a que se cumpla una condición. Si la condición no se cumple en el período de tiempo especificado, WebDriverWait produce una excepción o un error. Podemos usar la clase WebDriverWait para esperar a que se muestre un elemento determinado y para prepararnos para recibir la entrada del usuario.

Para buscar un elemento en la página, se usa la clase By. La clase By proporciona métodos que le permiten buscar un elemento por su nombre, por su nombre de clase CSS, por su etiqueta HTML o, en nuestro caso, por su atributo id.

Andy y Amita escriben el código del método auxiliar FindElement. Su aspecto es similar a este código:

private IWebElement FindElement(By locator, IWebElement parent = null, int timeoutSeconds = 10)
{
    // WebDriverWait enables us to wait for the specified condition to be true
    // within a given time period.
    return new WebDriverWait(driver, TimeSpan.FromSeconds(timeoutSeconds))
        .Until(c => {
            IWebElement element = null;
            // If a parent was provided, find its child element.
            if (parent != null)
            {
                element = parent.FindElement(locator);
            }
            // Otherwise, locate the element from the root of the DOM.
            else
            {
                element = driver.FindElement(locator);
            }
            // Return true after the element is displayed and is able to receive user input.
            return (element != null && element.Displayed && element.Enabled) ? element : null;
        });
}

Escritura del método auxiliar ClickElement

Andy: Ahora vamos a escribir un método auxiliar que haga clic en los vínculos. Selenium proporciona varias maneras para escribir este método. Una de ellas es la interfaz IJavaScriptExecutor. Con ella, podemos hacer clic en vínculos mediante programación con JavaScript. Este enfoque funciona bien porque puede hacer clic en los vínculos sin desplazarse hasta ellos para que se muestren en la vista.

ChromeDriver, FirefoxDriver y EdgeDriver implementan IJavaScriptExecutor. Necesitamos convertir el controlador a esta interfaz y, después, llamar a ExecuteScript para ejecutar el método click() de JavaScript en el objeto HTML subyacente.

Andy y Amita escriben el código del método auxiliar ClickElement. Su aspecto es similar a este código:

private void ClickElement(IWebElement element)
{
    // We expect the driver to implement IJavaScriptExecutor.
    // IJavaScriptExecutor enables us to execute JavaScript code during the tests.
    IJavaScriptExecutor js = driver as IJavaScriptExecutor;

    // Through JavaScript, run the click() method on the underlying HTML object.
    js.ExecuteScript("arguments[0].click();", element);
}

Amita: Me gusta la idea de agregar estos métodos auxiliares. Parecen lo suficientemente generales como para usarlos en casi cualquier prueba. Podemos agregar más métodos auxiliares más adelante a medida que los necesitemos.

Definición del método de prueba

Andy: Ahora estamos listos para definir el método de prueba. Basándonos en las pruebas manuales que hemos ejecutado anteriormente, vamos a llamar a este método ClickLinkById_ShouldDisplayModalById. Es recomendable proporcionar a los métodos de prueba nombres descriptivos que definan exactamente lo que hace la prueba. Aquí queremos hacer clic en un vínculo, definido por su atributo id. A continuación, queremos comprobar que se muestra la ventana modal adecuada, también usando su atributo id.

Andy agrega código de inicio para el método de prueba:

public void ClickLinkById_ShouldDisplayModalById(string linkId, string modalId)
{
}

Andy: Antes de agregar más código, vamos a definir qué debe hacer esta prueba.

Amita: Yo puedo encargarme de esta parte. Queremos:

  1. Buscar el vínculo por su atributo id y, después, hacer clic en él.
  2. Localizar la ventana modal resultante.
  3. Cerrar la ventana modal.
  4. Comprobar que la ventana modal se muestra correctamente.

Andy: Genial. También tendremos que abordar otras cosas. Por ejemplo, necesitamos omitir la prueba si no se puede cargar el controlador y solo necesitamos cerrar la ventana modal si esta se muestra correctamente.

Después de rellenar sus tazas de café, Andy y Amita agregan código a su método de prueba. Usan los métodos auxiliares que han escrito anteriormente para buscar elementos de página y hacer clic en vínculos y botones. Este es el resultado:

public void ClickLinkById_ShouldDisplayModalById(string linkId, string modalId)
{
    // Skip the test if the driver could not be loaded.
    // This happens when the underlying browser is not installed.
    if (driver == null)
    {
        Assert.Ignore();
        return;
    }

    // Locate the link by its ID and then click the link.
    ClickElement(FindElement(By.Id(linkId)));

    // Locate the resulting modal.
    IWebElement modal = FindElement(By.Id(modalId));

    // Record whether the modal was successfully displayed.
    bool modalWasDisplayed = (modal != null && modal.Displayed);

    // Close the modal if it was displayed.
    if (modalWasDisplayed)
    {
        // Click the close button that's part of the modal.
        ClickElement(FindElement(By.ClassName("close"), modal));

        // Wait for the modal to close and for the main page to again be clickable.
        FindElement(By.TagName("body"));
    }

    // Assert that the modal was displayed successfully.
    // If it wasn't, this test will be recorded as failed.
    Assert.That(modalWasDisplayed, Is.True);
}

Amita: El código tiene muy buena pinta hasta ahora. Pero ¿cómo conectamos esta prueba a los atributos id que hemos recopilado antes?

Andy: Buena pregunta. Vamos a centrarnos en eso.

Definición de los datos del caso de prueba

Andy: En NUnit, se pueden proporcionar datos a las pruebas de varias maneras. Aquí, vamos a usar el atributo TestCase. Este atributo toma argumentos y posteriormente los pasa al método de prueba cuando se ejecuta. Podemos tener varios atributos TestCase, cada uno de los cuales prueba una característica diferente de la aplicación. Cada atributo TestCase genera un caso de prueba, el cual se incluye en el informe que aparece al final de la ejecución de una canalización.

Andy agrega estos atributos TestCase al método de prueba. Estos atributos describen el botón Download game (Descargar juego), una de las pantallas de juego y el jugador en primera posición en la tabla de clasificación. Cada atributo especifica dos atributos id: uno para el vínculo en el que se va a hacer clic y otro para la ventana modal correspondiente.

// Download game
[TestCase("download-btn", "pretend-modal")]
// Screen image
[TestCase("screen-01", "screen-modal")]
// // Top player on the leaderboard
[TestCase("profile-1", "profile-modal-1")]
public void ClickLinkById_ShouldDisplayModalById(string linkId, string modalId)
{

...

Andy: Para cada atributo TestCase, el primer parámetro es el atributo id del vínculo en el que se debe hacer clic. El segundo parámetro es el atributo id de la ventana modal que se espera que aparezca. Se puede ver cómo estos parámetros corresponden a los dos argumentos de cadena en nuestro método de prueba.

Amita: Sí, lo veo. Con un poco de práctica, creo que puedo agregar mis propias pruebas. ¿Cuándo podemos ver la ejecución de estas pruebas en nuestra canalización?

Andy: Antes de enviar los cambios a la canalización, vamos a comprobar que el código se compila y se ejecuta de forma local. Después de comprobar que todo funciona correctamente, confirmaremos e insertaremos los cambios en GitHub y veremos cómo se mueven a lo largo de la canalización. Ahora vamos a ejecutar las pruebas localmente.