Taula de continguts

Testing amb Selenium en Node.js

Selenium es pot executar en diversos llenguatges, inclòs JS amb Node.js.

Ens pot ser molt pràctic per a realitzar tests per a Cordova o el propi NodeJS.

Referències:

, , ,

Basics

Si tenim un projecte NodeJS podem afegir el packages amb:

$ npm install selenium-webdriver assert child-process

Si es tracta d'un projecte en un altre llenguatge podem crear la carpeta .test i posar els tests a dins, independents del projecte:

$ mkdir .test
$ cd .test
$ npm init
$ npm install selenium-webdriver assert child-process

Els tests s'executaran entrant a la carpeta .test i executant-los:

$ cd .test
$ node 01-xxxx.js

Per defecte s'executarà en mode HEADLESS (sense GUI). Si volem veure el browser:

$ HEADLESS=false node 01-xxx.js

Per executar els tests amb Chrome enlloc de Firefox:

$ CHROME_TESTS=chrome node 01-xxx.js


Projecte base

Ens convé tenir els tests independents del codi de l'aplicació. En aquest exemple tenim una estructura amb aquests arxius. Dins src pot haver el codi en PHP o altres llenguatges que ens interessi:

.
├── .gitignore
├── README.md
├── run.sh
├── src
│   ├── index.php
│   ├── register.php
│   └── ...
└── .test
    ├── BaseTest.js
    └── 01-page-h1.js

Cal que afegim la carpeta node_modules/ al .gitignore del nostre projecte, encara que no estiguem treballant en NodeJS:

.gitignore
node_modules/


Desenvolupament

En aquests tests funcionals, els tests estan aïllats del desenvolupament i del llenguatge emprat, pel què podem fer un objecte per testejar qualsevol altre projecte. Només el fitxer run.sh contindrà les i instruccions adients per engegar un o altre projecte.

Arxius de posada en marxa del server

run.sh (o run.bat en Windows) està a l'arrel del projecte, és a dir, al mateix nivell que la carpeta .test

Versió Linux

run.sh per a PHP

run.sh
#!/bin/bash
# directori del script run.sh
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
# entrem a la carpeta del codi font
cd $SCRIPT_DIR/src
# engeguem el PHP server
php -S 0.0.0.0:8000

run.sh per a Cordova

run.sh
#!/bin/bash
# directori del script run.sh
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
# entrem a la carpeta del codi font
cd $SCRIPT_DIR
# engeguem el cordova sense browser
cordova serve
Versió Windows

run.sh per a Cordova

run.bat
cordova serve


Llibreria Base

BaseTest.js
const {Builder, Browser, By, Key, until} = require("selenium-webdriver");
const firefox = require('selenium-webdriver/firefox');
const chrome = require('selenium-webdriver/chrome');
const { spawn } = require("child_process");
const assert = require('assert');
 
 
class BaseTest {
 
    constructor() {
        console.log("Constructing...")
        this.headless = process.env.HEADLESS=="false" ? false : true;
        this.browser = process.env.CHROME_TESTS ? "chrome" : "firefox";
        this.cmd = null;
        this.driver = null;
    }
 
    async setUp() {
        console.log("HEADLESS:"+this.headless);
        console.log("BROWSER:"+this.browser);
 
        // run server and setup driver
        await this.runServer( "../run", [] );
        await this.setupDriver();
        // deixem temps a que el servidor es posi en marxa
        await this.driver.sleep(2000);
    }
 
    async tearDown() {
        console.log("Closing server...");
        // parem server
        await this.stopServer();
        // deixem temps perquè es tanquin els processos
        await this.driver.sleep(2000);
        // tanquem browser
        console.log("Closing Selenium driver...");
        await this.driver.quit();
    }
 
    async run() {
        await this.setUp();
        try {
            await this.test();
        } finally {
            await this.tearDown();
        }
    }
 
    async test() {
        console.log("Empty test!");
    }
 
    async setupDriver() {
        let firefoxOptions = new firefox.Options();
        let chromeOptions = new chrome.Options();
        if( this.headless ) {
            console.log("Running Headless Tests...");
            firefoxOptions = new firefox.Options().addArguments('-headless');
            chromeOptions = new chrome.Options().addArguments('--headless=new');
        }
        if( this.browser=="chrome" ) {
            this.driver = await new Builder()
                .forBrowser(Browser.CHROME)
                .setChromeOptions(chromeOptions)
                .build();
        } else {
            this.driver = await new Builder()
                .forBrowser(Browser.FIREFOX)
                .setFirefoxOptions(firefoxOptions)
                .build();
        }
    }
 
    runServer( command, options ) {
        // Engeguem server amb la APP
        if( process.platform=="win32" ) {
            this.cmd = spawn(command+".bat",options,{shell:true});
        } else {
            // linux, macos (darwin), or other
            this.cmd = spawn(command+".sh",options);
        }
 
        this.cmd.stdout.on("data", data => {
            console.log(`stdout: ${data}`);
        });
        this.cmd.stderr.on("data", data => {
            console.log(`stderr: ${data}`);
        });
        this.cmd.on('error', (error) => {
            console.log(`error: ${error.message}`);
        });
        this.cmd.on("close", code => {
            console.log(`child process exited with code ${code}`);
        });
    }
 
    async stopServer() {
        // tanquem servidor
        if( process.platform=="win32" ) {
            spawn("taskkill", ["/pid", this.cmd.pid, '/f', '/t']);
        } else {
            // Linux, MacOS or other
            await this.cmd.kill("SIGHUP")
        }
    }
 
}
 
// publiquem l'objecte BaseTest
exports.BaseTest = BaseTest;


Test 01 : comprovem header H1

01-header-h1.js
// carreguem les llibreries
const { BaseTest } = require("./BaseTest.js")
const { By, until } = require("selenium-webdriver");
const assert = require('assert');
 
// heredem una classe amb un sol mètode test()
// emprem this.driver per utilitzar Selenium
 
class MyTest extends BaseTest
{
	async test() {
        // testejem H1 a la home page
        //////////////////////////////////////////////////////
        await this.driver.get("http://localhost:8000/browser/www/");
        var currentText = await this.driver.findElement(By.tagName("h1")).getText();
        var expectedText = "Tasklist";
        assert( currentText==expectedText, "Títol H1 de la pàgina principal incorrecte");
 
        console.log("TEST OK");
	}
}
 
// executem el test
 
(async function test_example() {
	const test = new MyTest();
	await test.run();
	console.log("END")
})();


Test 00 : formulari buit

Aquest exemple testeja que si deixem buit el nom, ens surt un alert que ens avisa.

00-nom-buit.js
// carreguem les llibreries
const { BaseTest } = require("./BaseTest.js")
const { By, until } = require("selenium-webdriver");
const assert = require('assert');
 
// heredem una classe amb un sol mètode test()
// emprem this.driver per utilitzar Selenium
 
class MyTest extends BaseTest
{
	async test() {
        // testejem login
        //////////////////////////////////////////////////////
        await this.driver.get("http://localhost:8000/register.php");
        //await this.driver.findElement(By.name("nom")).getText();
 
        // el INPUT name="nom" està buit
 
        await this.driver.findElement(By.xpath("//button[text()='Seguent']")).click();
 
        // comprovem que l'alert message és ERRONI
        await this.driver.wait(until.alertIsPresent(),2000,"ERROR TEST: després del SEGUENT ha d'aparèixer un alert amb el resultat de la validació del NOM.");
        let alert = await this.driver.switchTo().alert();
        let alertText = await alert.getText();
        let assertMessage = "El NOM no pot estar buit.";
        assert(alertText==assertMessage,"ERROR TEST: si el nom està buit, l'alert ha de dir: '"+assertMessage+"'.");
        await alert.accept();
 
        console.log("TEST OK");
	}
}
 
// executem el test
 
(async function test_example() {
	const test = new MyTest();
	await test.run();
	console.log("END")
})();


Test 02 : login ok

Testeja que podem logar-nos en un admin panel de Django.

És molt important no deixar credencials en els arxius de codi per raons òbvies.

Per selventar aquest problema, aquest exemple utilitza la llibreria dotenv de NPM, pel què caldrà que la instal·lis:

$ npm install dotenv

I crear un arxiu .env com aquest:

URL=http://localhost:8000
HEADLESS=false
USUARI=el-meu-username
CONTRASENYA=la-contrasenya-secreta
02-login-ok.js
// carreguem les llibreries
const { BaseTest } = require("./BaseTest.js")
const { By, until } = require("selenium-webdriver");
const assert = require('assert');
 
//.env
require('dotenv').config()
console.log(process.env)
 
class MyTest extends BaseTest
{
    async test() {
        // Login test
        //////////////////////////////////////////////////////
        var site = process.env.URL
        var driver = this.driver
        await driver.get(site+"/admin/login/");
 
        // 1 cercar login box
        let usernameInput = await driver.wait(until.elementLocated(
                By.id('id_username')), 10000);
        let passwordInput = await driver.wait(until.elementLocated(
                By.id('id_password')), 10000);
 
        // 2 posar usuari i pass
        usernameInput.sendKeys(process.env.USUARI)
        passwordInput.sendKeys(process.env.CONTRASENYA)
 
        // 3 boto send .click()
        let sendButton = await driver.wait(until.elementLocated(
                By.css('input[value="Iniciar sessió"]')), 10000);
        sendButton.click()
 
        // 4 comprovem que hem entrat
        let logoutButton = await driver.wait(until.elementLocated(
                By.xpath('//button[@type="submit"]')), 10000);
        var currentLogoutText = await logoutButton.getText();
        var expectedText = "FINALITZAR SESSIÓ";
 
        // assert verifica que es compleixin condicions concretes
        // si no es compleix, llançarà una excepció de test fallit
        assert( currentLogoutText==expectedText,
                "Login fallit.\n\tTEXT TROBAT="+currentLogoutText+"\n\tTEXT ESPERAT="+expectedText);
 
        console.log("TEST OK");
    }
}
 
// executem el test
 
(async function test_example() {
	const test = new MyTest();
	await test.run();
	console.log("END")
})();


Test Cordova : afegir tasca

Aquest exemple inclou l'ús de prompts, que es fa com si fos un alert.

Fixeu-vos que Cordova necessita anar a la web /browser/www.

addtask.js
// carreguem les llibreries
const { BaseTest } = require("./BaseTest.js")
const { By, until } = require("selenium-webdriver");
const assert = require('assert');
 
// heredem una classe amb un sol mètode test()
// emprem this.driver per utilitzar Selenium
 
class AddTaskTest extends BaseTest
{
	async test() {
        // testejem afegir tasca en tasklist de Cordova
        //////////////////////////////////////////////////////
        await this.driver.get("http://localhost:8000/browser/www/");
 
        // cliquem botó "+"
        await this.driver.findElement(By.xpath("//button[text()='+']")).click();
 
        // el prompt pel text de la tasca es tracta igual que un alert en Selenium
        await this.driver.wait(until.alertIsPresent(),2000,"ERROR TEST: el botó '+' d'afegir tasca ha d'obrir un prompt.");
        let prompt = await this.driver.switchTo().alert();
        // afegim el text de la tasca i acceptem
        var taskText = "lalala";
        prompt.sendKeys(taskText);
        await this.driver.sleep(1000);
        await prompt.accept();
        await this.driver.sleep(1000);
 
        // checkejem tasca
        await this.driver.findElement(By.xpath("//li[text()='"+taskText+"']")).click();
 
        console.log("TEST OK");
	}
}
 
// executem el test
 
(async function test_example() {
	const test = new AddTaskTest();
	await test.run();
	console.log("END")
})();

Crea un test DelTaskTest similar a l'anterior que:

Exercicis

Crea els següents tests amb la web que t'ha proporcionat el professor:

Crea un dels següents tests que t'assigni el professor: exercicis_de_test_amb_django_tutorial