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 02 : formulari buit

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

02-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 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: