Escrever os testes da interface do usuário

Concluído

Nesta seção, ajude o Paulo e a Marina a escreverem os testes do Selenium que verificam os comportamentos da interface do usuário descritos por Marina.

Marina executa normalmente os testes no Chrome, no Firefox e no Microsoft Edge. Aqui, você fará o mesmo. O agente hospedado pela Microsoft que você usará está pré-configurado para funcionar com cada um desses navegadores.

Buscar o branch do GitHub

Nesta seção, você efetua fetch do branch selenium do GitHub. Em seguida, você fará check-out ou alternará para esse branch. O conteúdo do branch ajudará você a acompanhar os testes que o Paulo e a Marina escreveram.

Esse branch contém o projeto do Space Game com o qual você trabalhou nos módulos anteriores. Ele também contém uma configuração do Azure Pipelines com a qual começar.

  1. No Visual Studio Code, abra o terminal integrado.

  2. Para baixar um branch chamado selenium no repositório da Microsoft, alterne para esse branch e execute os seguintes comandos git fetch e git checkout:

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

    Dica

    Se você seguiu o teste manual da Amita na unidade anterior, talvez já tenha executado esses comandos. Se você já os executou na unidade anterior, ainda poderá executá-los novamente agora.

    Lembre-se de que upstream refere-se ao repositório GitHub da Microsoft. A configuração do Git do seu projeto compreende o repositório remoto de upstream porque você configurou essa relação. Você a configurou ao criar o fork do projeto no repositório da Microsoft e a clonou localmente.

    Em breve, você efetuará push desse branch para seu repositório do GitHub, conhecido como origin.

  3. Opcionalmente, no Visual Studio Code, abra o arquivo azure-pipelines.yml. Familiarize-se com a configuração inicial.

    A configuração é semelhante àquelas que você criou nos módulos anteriores deste roteiro de aprendizagem. Ela compila apenas a configuração de Versão do aplicativo. Para resumir, ela também omite os gatilhos, as aprovações manuais e os testes que você configurou nos módulos anteriores.

    Observação

    Uma configuração mais robusta pode especificar os branches que participam do processo de build. Por exemplo, para ajudar a verificar a qualidade do código, você pode executar testes de unidade cada vez que efetuar push de uma alteração em qualquer branch. Você também pode implantar o aplicativo em um ambiente que executa testes mais exaustivos. Porém, realize essa implantação somente quando você tiver uma solicitação de pull, uma versão Release Candidate ou quando mesclar o código com o principal.

    Para obter mais informações, confira Implement a code workflow in your build pipeline by using Git and GitHub e Gatilhos de pipeline de build.

Escrever o código de teste de unidade

Marina está ansiosa para aprender a escrever o código que controla o navegador da Web.

Ela e Paulo trabalharão em conjunto para escrever os testes do Selenium. Paulo já configurou um projeto NUnit vazio. Durante todo o processo, eles consultam a documentação do Selenium, alguns tutoriais online e as anotações que eles realizaram quando a Marina fez os testes manualmente. No final deste módulo, você encontrará mais recursos para ajudar você a realizar o processo.

Vamos examinar o processo que Paulo e Marina usaram para escrever os testes. Você pode acompanhar abrindo HomePageTest.cs no diretório Tailspin.SpaceGame.Web.UITests no Visual Studio Code.

Definir a classe HomePageTest

Paulo: A primeira coisa que precisamos fazer é definir nossa classe de teste. Podemos optar por seguir uma das várias convenções de nomenclatura. Vamos chamar nossa classe de HomePageTest. Nessa classe, colocaremos todos os nossos testes relacionados à home page.

Paulo adiciona esse código ao HomePageTest.cs:

public class HomePageTest
{
}

Paulo: Precisamos marcar essa classe como public para que ela esteja disponível para a estrutura NUnit.

Adicionar a variável de membro IWebDriver

Paulo: depois, precisamos de uma variável de membro IWebDriver. O IWebDriver é a interface de programação que você usa para iniciar um navegador da Web e interagir com o conteúdo da página da Web.

Marina: Já ouvi falar de interfaces em programação. Você pode me contar mais?

Paulo: Imagine uma interface como uma especificação ou um blueprint de como um componente deve se comportar. Uma interface fornece os métodos (ou comportamentos) desse componente. Mas a interface não fornece nenhum dos detalhes subjacentes. Você ou outra pessoa criam uma ou mais classes concretas que implementam essa interface. O Selenium fornece as classes concretas que precisamos.

Este diagrama mostra a interface IWebDriver e algumas das classes que implementam essa interface:

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

O diagrama mostra três dos métodos que o IWebDriver fornece: Navigate, FindElement e Close.

Nas três classes mostradas aqui, ChromeDriver, FirefoxDriver e EdgeDriver, cada uma implementa o IWebDriver e os métodos dele. Há outras classes, como SafariDriver, que também implementam o IWebDriver. Cada classe de driver pode controlar o navegador da Web que ela representa.

Paulo adiciona uma variável de membro chamada driver à classe HomePageTest, da seguinte maneira:

public class HomePageTest
{
    private IWebDriver driver;
}

Definir os acessórios de teste

Paulo: Queremos executar todo o conjunto de testes no Chrome, no Firefox e no Edge. No NUnit, podemos usar os acessórios de teste para executar todo o conjunto de testes várias vezes, uma vez para cada navegador no qual desejamos testar.

No NUnit, você usa o atributo TestFixture para definir os seus acessórios de teste. O Paulo adiciona esses três acessórios de teste à classe HomePageTest:

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

Paulo: em seguida, precisamos definir um construtor para nossa classe de teste. O construtor é chamado quando o NUnit cria uma instância dessa classe. Como argumento, o construtor usa a cadeia de caracteres que anexamos aos nossos acessórios de teste. Veja abaixo a aparência do código:

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

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

Paulo: adicionamos a variável de membro browser para que possamos usar o nome atual do navegador no nosso código de configuração. Após realizar isso, vamos escrever o código de configuração.

Definir o método Setup

Paulo: em seguida, precisamos atribuir nossa variável de membro IWebDriver a uma instância de classe que implementa essa interface no navegador no qual estamos testando. As classes ChromeDriver, FirefoxDriver e EdgeDriver implementam essa interface no Chrome, no Firefox e no Microsoft Edge, respectivamente.

Vamos criar um método chamado Setup, que define a variável driver. Usamos o atributo OneTimeSetUp para dizer ao NUnit para executar esse método uma vez por acessório de teste.

[OneTimeSetUp]
public void Setup()
{
}

No método Setup, podemos usar uma instrução switch para atribuir a variável de membro driver à implementação concreta apropriada, com base no nome do navegador. Vamos adicionar esse código agora.

// 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");
}

O construtor de cada classe de driver usa um caminho opcional para o software do driver que o Selenium precisa controlar no navegador da Web. Posteriormente, discutiremos a função das variáveis de ambiente mostradas aqui.

Neste exemplo, o construtor EdgeDriver também requer opções adicionais para especificar que desejamos usar a versão Chromium do Edge.

Definir os métodos auxiliares

Paulo: Sei que precisaremos repetir duas ações em todos os testes:

  • Localizar elementos na página, como os links que clicamos e as janelas modais que esperamos que sejam exibidas
  • Clicar em elementos na página, como os links que revelam as janelas modais e o botão que fecha cada janela modal

Vamos escrever dois métodos auxiliares, um para cada ação. Vamos começar com o método que localiza um elemento na página.

Escrever o método auxiliar FindElement

Quando você localiza um elemento na página, ele é normalmente uma resposta a algum outro evento, como o carregamento da página ou a inserção de informações pelo usuário. O Selenium fornece a classe WebDriverWait, que permite que você aguarde até que uma condição seja verdadeira. Se a condição não for verdadeira no período determinado, o WebDriverWait vai gerar uma exceção ou um erro. Podemos usar a classe WebDriverWait para aguardar até que um determinado elemento seja exibido e esteja pronto para receber a entrada de usuário.

Para localizar um elemento na página, use a classe By. A classe By fornece métodos que permitem encontrar um elemento por nome, por nome de classe CSS, por marca HTML ou, no nosso caso, pelo atributo id.

O Paulo e a Marina codificam o método auxiliar FindElement. É semelhante 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;
        });
}

Escrever o método auxiliar ClickElement

Paulo: em seguida, vamos escrever um método auxiliar que clica em links. O Selenium fornece algumas maneiras de escrever esse método. Uma delas é a interface IJavaScriptExecutor. Com ela, podemos clicar em links de modo programático usando o JavaScript. Essa abordagem funciona bem, pois ela pode clicar em links sem primeiro rolá-los até que eles sejam exibidos.

ChromeDriver, FirefoxDriver e EdgeDriver, cada um implementa IJavaScriptExecutor. Precisamos converter o driver para essa interface e chamar ExecuteScript para executar o método JavaScript click() no objeto HTML subjacente.

O Paulo e a Marina codificam o método auxiliar ClickElement. É semelhante 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);
}

Marina: Gosto da ideia de adicionar esses métodos auxiliares. Eles parecem genéricos o suficiente para serem usados em quase todos os testes. Podemos adicionar mais métodos auxiliares posteriormente, conforme precisarmos deles.

Definir o método de teste

Paulo: agora estamos prontos para definir o método de teste. Com base nos testes manuais que executamos anteriormente, vamos chamar esse método de ClickLinkById_ShouldDisplayModalById. É uma prática recomendada dar aos métodos de teste nomes descritivos que definem precisamente o que o teste realiza. Aqui, queremos clicar em um link definido pelo respectivo atributo id. Então, queremos verificar se a janela modal apropriada é exibida, usando também o atributo id.

O Paulo adiciona o código inicial do método de teste:

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

Paulo: Antes de adicionarmos mais código, vamos definir o que esse teste deve fazer.

Marina: Consigo fazer essa parte. Queremos:

  1. Localizar o link pelo atributo id e clicar nele.
  2. Localizar a janela modal resultante.
  3. Fechar a janela.
  4. Verifique se a janela modal foi exibida com êxito.

Paulo: Ótimo. Também precisaremos lidar com algumas outras coisas. Por exemplo, precisaremos ignorar o teste se o driver não puder ser carregado e precisaremos fechar a janela modal somente se ela tiver sido exibida com êxito.

Depois de encherem as canecas de café, Paulo e Marina adicionam um código ao método de teste. Eles usam os métodos auxiliares que escreveram para localizar elementos da página e clicar em links e botões. Eis o 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);
}

Marina: Até agora, a codificação está dando certo. Mas como podemos conectar esse teste aos atributos id que coletamos anteriormente?

Paulo: Ótima pergunta. Faremos isso em seguida.

Definir dados de caso de teste

Paulo: No NUnit, você pode fornecer dados aos seus testes de algumas maneiras. Nesse caso, usamos o atributo TestCase. Esse atributo usa argumentos que depois são passados para o método de teste quando ele é executado. Podemos ter vários atributos TestCase e cada um testar um recurso diferente do nosso aplicativo. Cada atributo TestCase produz um caso de teste que é incluso no relatório exibido no final de uma execução de pipeline.

O Paulo adiciona esses atributos TestCase ao método de teste. Esses atributos descrevem o botão Baixar jogo, uma das telas do jogo e o melhor jogador no placar de líderes. Cada atributo especifica dois atributos id: um para o link a ser clicado e um para a janela modal correspondente.

// 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)
{

...

Paulo: Para cada atributo TestCase, o primeiro parâmetro é o atributo id no qual o link deve clicar. O segundo parâmetro é o atributo id da janela modal que esperamos que seja exibida. Veja como esses parâmetros correspondem aos dois argumentos de cadeia de caracteres no método de teste.

Marina: Entendi. Com alguma prática, acho que consigo adicionar meus próprios testes. Quando poderemos ver esses testes em execução no nosso pipeline?

Paulo: Antes de enviarmos por push as alterações pelo pipeline, primeiro verificaremos se o código é compilado e executado localmente. Vamos fazer commit e enviar as alterações por push para o GitHub e vê-las passar pelo pipeline somente depois de verificarmos se tudo está funcionando. Vamos executar os testes localmente agora.