Il Page Object è un pattern utilizzato nei functional test per astrarre l’interazione con gli elementi della UI.
All’interno di un oggetto vengono incapsulate le interazioni e le funzionalità di una pagina web che vengono esposte tramite api, nascondendo la struttura degli elementi HTML all’esterno.
Per approfondire il concetto rimando all’articolo di Martin Fowler.
Perché usare il Page Object
L’uso del Page Object migliora la leggibilità e la manutenibilità dei test.
Incapsulando le interazioni con le pagine all’interno di oggetti riutilizzabili, si evita la duplicazione del codice.
I cambiamenti della UI saranno facilmente gestibili: basterà aggiornare i riferimenti o la logica nei metodi del Page Object, lasciando invariati i context e le features.
Esempio di utilizzo del Page Object Pattern
Per utilizzare questo pattern con Behat, occorre installare l’estensione BehatPageObjectExtension seguendo le istruzioni presenti nella documentazione.
Scriviamo un semplice functional test per testare il corretto funzionamento del login utente, definito ad esempio dallo scenario:
1 2 3 4 5 |
@ui Scenario: Login utente Given Sono sulla pagina di login When Mi loggo come “nomeutente” e password “password” Then Devo essere reindirizzato al profilo utente |
Nello scenario vengono utilizzate due pagine:
- Pagina di Login
- Pagina del profilo utente
Vediamo come gestire l’interazione con la pagina di Login.
Creiamo il Page Object, LoginPage, che estende la classe
SensioLabs\Behat\PageObjectExtension\PageObject\Page. Questa classe estende
Behat\Mink\Element\DocumentElement , rendendo quindi utilizzabili le funzionalità di navigazione di Mink all’interno del Page Object.
1 2 3 4 5 6 7 8 9 |
namespace NuvolaBehat\Page; use SensioLabs\Behat\PageObjectExtension\PageObject\Page; class LoginPage extends Page { protected $path = '/login'; } |
L’HTML della pagina sarà ad esempio:
1 2 3 4 5 |
<form id="login" method="post"> <input type="text" id="username" name="_username" value="" required="" placeholder="Nome utente"> <input type="password" id="password" name="_password" required="" placeholder="Password"> <button>Login</button> </form> |
Compito del Page Object è di gestire l’interazione con la UI. Andiamo quindi a definire i metodi per interagire con essa:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
/** * @throws \Behat\Mink\Exception\ElementNotFoundException */ public function inserisciNomeUtente(string $nomeUtente): void { $this->fillField('username', $nomeUtente); } /** * @throws \Behat\Mink\Exception\ElementNotFoundException */ public function inserisciPassword(string $password): void { $this->fillField('password', $password); } /** * @throws \Behat\Mink\Exception\ElementNotFoundException */ public function effettuaLogin(): void { $this->pressButton('Login'); } |
Non ci resta che utilizzare la classe appena creata.
È possibile iniettare direttamente nel costruttore del Context i PageObject che ci servono:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
namespace NuvolaBehat\Context\Ui\Login; use Behat\Behat\Context\Context; use NuvolaBehat\Page\LoginPage; use NuvolaBehat\Page\ProfiloPage; class LoginContext implements Context { /** * @var LoginPage */ private $loginPage; /** * @var ProfiloPage */ private $profiloPage; /** * @param LoginPage $loginPage * @param ProfiloPage $profiloPage */ public function __construct(LoginPage $loginPage, ProfiloPage $profiloPage) { $this->loginPage = $loginPage; $this->profiloPage = $profiloPage; } } |
Mappiamo quindi gli scenari con il contesto:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
/** * @Given /^Sono sulla pagina di login$/ */ public function sonoSullaPaginaDiLogin() { $this->loginPage->open(); } /** * @Given /^Mi loggo come "([^"]*)" con password "([^"]*)"$/ */ public function miLoggoComeUtenteConPassword(string $nomeUtente, string $password) { $this->loginPage->inserisciNomeUtente($nomeUtente); $this->loginPage->inserisciPassword($password); $this->loginPage->effettuaLogin(); } /** * @Given /^Devo essere reindirizzato al profilo utente$/ */ public function sonoSulProfiloUtente() { $this->profiloPage->isOpen(); } |
Per approfondire l’argomento vi invito a leggere la documentazione ufficiale.
Conclusioni
Come si può vedere dall’esempio, l’utilizzo del pattern ha permesso di separare la logica di interazione con la pagina, evitando qualsiasi riferimento agli elementi della UI nel Context, ed incapsulandola in un oggetto riutilizzabile.
Happy testing!
Se sei uno sviluppatore appassionato e curioso… scopri come unirti a noi!
P.S. Per rendere i test più solidi, conviene utilizzare come riferimenti degli attributi data-* creati ad hoc. Si eviterà così di dovere aggiornare i test ogni volta che viene cambiata una label, una classe o un name di un elemento. Ad esempio il codice potrebbe essere:
1 2 3 4 5 |
public function inserisciPassword(string $password): void { $selectElement = $this->find('css', '[data-test-element="password"]'); $selectElement->setValue($password); } |