Scrivere i test dell'interfaccia utente

Completato

In questa sezione, Andy e Amita verranno aiutati a scrivere i test di Selenium che verificano i comportamenti dell'interfaccia utente descritti da Amita.

In genere, Amita esegue i test su Chrome, Firefox e Microsoft Edge. In questa sezione, si eseguirà la stessa operazione. L'agente ospitato da Microsoft che verrà usato è preconfigurato in modo da poter essere usato con ognuno di questi browser.

Recuperare il ramo da GitHub

In questa sezione si recupererà il ramo selenium da GitHub, quindi si eseguirà il check out o il passaggio a tale ramo. Il contenuto del ramo sarà utile per seguire i test scritti da Andy e Amita.

Questo ramo contiene il progetto Space Game usato nei moduli precedenti. Contiene anche una configurazione di Azure Pipelines per iniziare.

  1. Aprire il terminale integrato in Visual Studio Code.

  2. Per scaricare un ramo denominato selenium dal repository Microsoft, passare a quel ramo ed eseguire i comandi git fetch e git checkout seguenti:

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

    Suggerimento

    Se è stato seguito insieme al test manuale di Amita nell'unità precedente, è possibile che siano già stati eseguiti questi comandi. Se sono già stati eseguiti nell'unità precedente, è comunque possibile eseguirli nuovamente.

    Tenere presente che upstream si riferisce al repository GitHub Microsoft. La configurazione Git del progetto riconosce il repository upstream remoto perché tale relazione è stata configurata quando è stata creata una copia del progetto tramite fork dal repository Microsoft ed è stato clonato in locale.

    In breve, si eseguirà il push di questo ramo al repository GitHub personale, noto come origin.

  3. Facoltativamente, in Visual Studio Code aprire il file azure-pipelines.yml. Acquisire familiarità con la configurazione iniziale.

    La configurazione è simile a quelle create nei moduli precedenti in questo percorso di apprendimento. Esegue la compilazione solo della configurazione Release dell'applicazione. Per brevità, omette anche i trigger, le approvazioni manuali e i test configurati nei moduli precedenti.

    Nota

    Una configurazione più solida potrebbe specificare i rami che fanno parte del processo di compilazione. Ad esempio, per verificare la qualità del codice, è possibile eseguire unit test ogni volta che si esegue il push di una modifica in qualsiasi ramo. È anche possibile distribuire l'applicazione in un ambiente che esegue test più completi. Questa distribuzione viene tuttavia usata solo quando si ha una richiesta pull, quando si ha una versione finale candidata o quando si esegue il merge del codice in main.

    Per altre informazioni, vedere Implementare un flusso di lavoro di codice nella pipeline di compilazione usando Git e GitHub e Trigger della pipeline di compilazione.

Scrivere il codice degli unit test

Amita è entusiasta di imparare a scrivere il codice che controlla il Web browser.

Collaborerà con Andy per scrivere i test di Selenium. Andy ha già configurato un progetto NUnit vuoto. Nel corso del processo, consultano la documentazione di Selenium, alcune esercitazioni online e gli appunti che hanno preso quando Amita ha eseguito manualmente i test. Alla fine del modulo sono disponibili altre risorse utili per completare il processo.

Esaminiamo il processo usato da Andy e Amita per scrivere i test. È possibile seguire la procedura aprendo HomePageTest.cs nella directory Tailspin.SpaceGame.Web.UITests in Visual Studio Code.

Definire la classe HomePageTest

Andy: Prima di tutto dobbiamo definire la classe di test. Possiamo scegliere tra diverse convenzioni di denominazione. Per la nostra classe usiamo il nome HomePageTest. In questa classe verranno inseriti tutti i test correlati alla home page.

Andy aggiunge questo codice a HomePageTest.cs:

public class HomePageTest
{
}

Andy: Dobbiamo contrassegnare questa classe come public, in modo che sia disponibile per il framework NUnit.

Aggiungere la variabile membro IWebDriver

Andy: A questo punto ci serve una variabile membro IWebDriver. IWebDriver è l'interfaccia di programmazione utilizzata per avviare un Web browser e interagire con il contenuto delle pagine Web.

Amita: Ho sentito parlare di interfacce nella programmazione. Mi puoi spiegare meglio?

Andy: Un'interfaccia può essere paragonata a una specifica o a un progetto per il comportamento di un componente. Un'interfaccia fornisce i metodi, o comportamenti, di quel componente. L'interfaccia, però, non fornisce i dettagli sottostanti. Bisognerebbe creare una o più classi concrete che implementano quell'interfaccia. Selenium fornisce le classi concrete necessarie.

Questo diagramma mostra l'interfaccia IWebDriver e alcune delle classi che implementano questa interfaccia:

Diagramma dell'interfaccia IWebDriver, i relativi metodi e le classi concrete.

Il diagramma mostra tre dei metodi forniti da IWebDriver: Navigate, FindElement e Close.

Ognuna delle tre classi illustrate qui, ChromeDriver FirefoxDriver e EdgeDriver, implementa IWebDriver e i relativi metodi. Esistono anche altre classi, ad esempio SafariDriver, che implementano IWebDriver. Ogni classe driver è in grado di controllare il Web browser che rappresenta.

Andy aggiunge una variabile membro denominata driver alla HomePageTest classe, come segue:

public class HomePageTest
{
    private IWebDriver driver;
}

Definire le fixture di test

Andy: Vogliamo eseguire l'intero set di test su Chrome, Firefox e Edge. In NUnit è possibile usare le fixture di test per eseguire l'intero set di test più volte, una per ogni browser su cui si vuole eseguire il test.

In NUnit si usa l'attributo TestFixture per definire le fixture di test. Andy aggiunge queste tre fixture di test alla classe HomePageTest:

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

Andy: A questo punto dobbiamo definire un costruttore per la nostra classe di test. Il costruttore viene chiamato quando NUnit crea un'istanza di questa classe. Come argomento, il costruttore accetta la stringa collegata alle fixture di test. Ecco il codice risultante:

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

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

Andy: Aggiungiamo la variabile membro browser in modo da poter usare il nome del browser corrente nel codice di installazione. Quindi, scriviamo il codice di installazione.

Definire il metodo di installazione

Andy: A questo punto è necessario assegnare la variabile membro IWebDriver a un'istanza della classe che implementa questa interfaccia per il browser su cui viene eseguito il test. Le classi ChromeDriver, FirefoxDriver e EdgeDriver implementano questa interfaccia rispettivamente per Chrome, Firefox ed Edge.

Creiamo un metodo, denominato Setup, che imposta la variabile driver. Usiamo l'attributo OneTimeSetUp per indicare a NUnit di eseguire questo metodo una volta per ogni fixture di test.

[OneTimeSetUp]
public void Setup()
{
}

Nel metodo Setup è possibile usare un'istruzione switch per assegnare la variabile membro driver all'implementazione reale appropriata, in base al nome del browser. Ora si aggiunge il codice:

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

Il costruttore per ogni classe di driver accetta un percorso facoltativo per il software del driver di cui Selenium ha bisogno deve controllare il Web browser. In seguito verrà spiegato il ruolo delle variabili di ambiente mostrate qui.

In questo esempio, il costruttore EdgeDriver richiede anche opzioni aggiuntive per specificare che si vuole usare la versione Chromium di Edge.

Definire i metodi helper

Andy: So che dovremo ripetere due azioni durante i test:

  • Ricerca di elementi nella pagina, ad esempio i collegamenti su cui si fa clic e le finestre modali che si prevede di visualizzare
  • Clic sugli elementi della pagina, ad esempio i collegamenti che aprono le finestre modali e il pulsante che chiude le singole finestre modali

Scriviamo due metodi helper, uno per ogni azione, iniziando da quello che trova un elemento nella pagina.

Scrivere il metodo helper FindElement

Quando si individua un elemento nella pagina, è in genere è il risultato di un altro evento, ad esempio il caricamento della pagina o l'immissione di informazioni da parte dell'utente. Selenium fornisce la classe WebDriverWait, che consente di attendere che una condizione sia vera. Se la condizione non è vera entro il periodo di tempo specificato, WebDriverWait genera un'eccezione o un errore. È possibile usare la classe WebDriverWait per attendere che un determinato elemento venga visualizzato e che sia pronto a ricevere l'input dell'utente.

Per individuare un elemento nella pagina, si usa la classe By. La classe By fornisce metodi che consentono di trovare un elemento in base al suo nome, al nome della sua classe CSS, al suo tag HTML o, in questo caso, in base al suo attributo id.

Andy e Amita codificano il metodo helper FindElement. Sembra questo codice:

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

Scrivere il metodo helper ClickElement

Andy: A questo punto scriviamo un metodo helper che fa clic sui collegamenti. Selenium offre alcuni modi per scrivere questo metodo. Uno di essi è l'interfaccia di IJavaScriptExecutor. Consente di fare clic sui collegamenti a livello di programmazione tramite JavaScript. Questo è un approccio funzionale perché consente di fare clic sui collegamenti senza prima scorrere per visualizzarli.

ChromeDriver, FirefoxDriver e EdgeDriver implementano IJavaScriptExecutor. È necessario eseguire il cast del driver a questa interfaccia, quindi chiamare ExecuteScript per eseguire il metodo JavaScript click() sull'oggetto HTML sottostante.

Andy e Amita codificano il metodo helper ClickElement. Sembra questo codice:

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: Mi piace l'idea di aggiungere questi metodi helper. Sembrano abbastanza generici da poter essere usati in quasi tutti i test. Possiamo aggiungere altri metodi helper in un secondo momento, in base alle necessità.

Definire il metodo di test

Andy: A questo punto possiamo definire il metodo di test. In base ai test manuali eseguiti in precedenza, chiamiamo questo metodo ClickLinkById_ShouldDisplayModalById. È buona norma assegnare ai metodi di test nomi descrittivi che definiscano esattamente ciò che fa il test. Qui vogliamo fare clic su un collegamento, definito in base al relativo attributo id. E vogliamo verificare che venga visualizzata la finestra modale corretta, mediante il relativo attributo id.

Andy aggiunge il codice di avvio per il metodo di test:

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

Andy: Prima di aggiungere altro codice, dobbiamo definire le operazioni che il test deve eseguire.

Amita: Questa parte mi è chiara. Dobbiamo:

  1. Individuare il collegamento in base al relativo attributo id, quindi fare clic sul collegamento.
  2. Individuare la finestra modale risultante.
  3. Chiudere la finestra modale.
  4. Verificare che la finestra modale sia stata visualizzata correttamente.

Andy: Bene. Dovremo anche gestire alcune altre cose. Ad esempio, dobbiamo ignorare il test se non è stato possibile caricare il driver e dobbiamo chiudere la finestra modale solo se è stata visualizzata correttamente.

Andy e Amita si mettono all'opera e aggiungono codice al metodo di test. Usano i metodi helper che hanno scritto per individuare gli elementi della pagina e fare clic su collegamenti e pulsanti. Il risultato è il seguente:

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: Finora va tutto bene con la codifica. Ma come facciamo a connettere questo test agli attributi id raccolti in precedenza?

Andy: Ottima domanda. È il prossimo aspetto di cui ci occuperemo.

Definire i dati del test case

Andy: In NUnit è possibile fornire i dati per i test in diversi modi. Qui usiamo l'attributo TestCase. Questo attributo accetta argomenti che in seguito restituisce al metodo di test durante l'esecuzione. Può avere più attributi TestCase, ognuno dei quali testa una funzionalità diversa dell'app. Ogni attributo TestCase produce un test case che viene incluso nel report visualizzato alla fine di un'esecuzione della pipeline.

Andy aggiunge questi attributi TestCase al metodo di test. Questi attributi descrivono il pulsante Download game (Scarica gioco), una delle schermate di gioco e il giocatore in vetta al tabellone punteggi. Ogni attributo specifica due attributi id: uno per il collegamento su cui fare clic e uno per la finestra modale corrispondente.

// 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: Per ogni attributo TestCase, il primo parametro è l'attributo id relativo al collegamento su cui fare clic. Il secondo parametro è l'attributo id per la finestra modale che dovrebbe comparire. Questi parametri corrispondono ai due argomenti stringa nel metodo di test.

Amita: Lo vedo. Con un po' di pratica, credo di riuscire ad aggiungere i miei test. Quando potremo vedere questi test in esecuzione nella pipeline?

Andy: Prima di eseguire il push delle modifiche nella pipeline, dobbiamo verificare che il codice venga compilato ed eseguito in locale. Eseguiremo il commit e il push delle modifiche in GitHub per visualizzarle nella pipeline solo dopo aver verificato che tutto funzioni. Adesso eseguiamo i test in locale.