Qafoo GmbH - passion for software quality ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :Author: Manuel Pichler :Date: Tue, 06 Sep 2016 09:19:41 +0200 :Revision: 4 :Copyright: All rights reserved ============================ Introduction To Page Objects ============================ :Description: A while ago we wrote about writing acceptance tests (end-to-end tests) with Mink and PHPUnit. While this is a great set of tools for various applications such tests tend be susceptible to changes in the frontend. And the way they break is often hard to debug, too. Today I will introduce you to Page Objects which can solve some of these problems. :Abstract: A while ago we wrote about writing acceptance tests (end-to-end tests) with Mink and PHPUnit. While this is a great set of tools for various applications such tests tend be susceptible to changes in the frontend. And the way they break is often hard to debug, too. Today I will introduce you to Page Objects which can solve some of these problems. :Keywords: PHP, Testing, Page Object, Integration Test A while ago we wrote about `writing acceptance tests (end-to-end tests) with Mink and PHPUnit`__. While this is a great set of tools for various applications such tests tend be susceptible to changes in the frontend. And the way they break is often hard to debug, too. Today I will introduce you to `Page Objects`__ which can solve some of these problems. The basic idea behind a Page Object is that you get an object oriented representation of your website. The Page Objects maps the HTML (or JSON) to an object oriented structure you can interact with and assert on. This is more initial work then than writing tests with PHPUnit and Mink directly, but it can be worth the effort. I will introduce you to Page Objects by writing some simple tests for `Tideways – our application performance monitoring platform`__. __ /blog/081_phpunit_mink_functional_tests.html __ http://martinfowler.com/bliki/PageObject.html __ https://tideways.io Groundwork ========== We will again use the awesome Mink__ to simulate browsers and make it easy to interact with a website. Thus we are actually re-using the ``FeatureTest`` base class from the `Using Mink in PHPUnit`__ blog post. We have set up a repository__ where can take a full look at the working example and maybe even try it out yourself. You'll need some tools to set this up – in the mentioned repository it is sufficient to execute ``composer install``. Setting it up in a new projects you'd execute something like:: composer require --dev phpunit/phpunit behat/mink behat/mink-goutte-driver The `FeatureTest base class`__ handles the basic mink interaction and has already been discussed in the last blog post so that we can skip it here. __ http://mink.behat.org/en/latest/ __ /blog/081_phpunit_mink_functional_tests.html __ https://github.com/QafooLabs/PageObjects __ https://github.com/QafooLabs/PageObjects/blob/master/tests/Qafoo/FeatureTest.php A First Test ============ As mentioned we want to test `Tideways`__ and Tideways requires you to login. Thus we start with a simple login test:: class LoginTest extends FeatureTest { public function testLogInWithWrongPassword() { $page = (new Page\Login($this->session))->visit(Page\Login::PATH); $page->setUser(getenv('USER')); $page->setPassword('wrongPassword'); $newPage = $page->login(); $this->assertInstanceOf(Page\Login::class, $newPage); } // … } This test already uses a page object by instantiating the class ``Page\Login``. And by using this one it makes the test very easy to read. You instantiate the page, ``visit()`` it and then interact with it in an obvious way. We set username and password, and then call ``login()``. Since we set a wrong password we expect to stay on the login page. This already is the nice thing with page objects. The test are readable and this is something we want to optimize for, right? One the other hand the logic must be implemented in the Page Object. By implementing it in a Page Object it is re-usable in other tests as you will see later. So let's take a look at this simple Page Object:: use Qafoo\Page; class Login extends Page { const PATH = '/login'; public function setUser($user) { $this->find('input[name="_username"]')->setValue($user); } public function setPassword($password) { $this->find('input[name="_password"]')->setValue($password); } public function login() { $this->find('input[name="_submit"]')->press(); return $this->createFromDocument(); } } Since we use Mink and implement some logic in the ``Qafoo\Page`` base class this still does not look that complex. What you should note is the fact that the method ``setUser()`` (and alike) hide the interaction with the DOM tree. If the name of those form fields change you'll have to change it in one single location. The methods ``find()`` and ``visitPath()`` can be found in the `Page base class`__ and just abstract Mink a little bit and provide better error messages if something fails. The ``login()`` method will execute a HTTP request to some page. If the login failed we will be redirected back to the login page (like in the test above), otherwise we expect to be redirected to the dashboard:: public function testSuccessfulLogIn() { $page = (new Page\Login($this->session))->visit(Page\Login::PATH); $page->setUser(getenv('USER')); $page->setPassword(getenv('PASSWORD')); $newPage = $page->login(); $this->assertInstanceOf(Page\Dashboard::class, $newPage); } We expect the user name and password to be set as environment variables since there are no public logins for Tideways. If you want to run the tests yourself, just create an account and provide them like mentioned in the README__. There is one "magic" method left in the page object shown before – the method ``createFromDocument()``. The method maps the path of the last request back to Page Object. Something like the router in about every framework would do, but we map to a Page Object instead of a controller. This method will get more complex for complex routes but it helps us to make assertions on the resulting page. __ https://tideways.io __ https://github.com/QafooLabs/PageObjects/blob/master/tests/Qafoo/Page.php __ https://github.com/QafooLabs/PageObjects Refactoring The Frontend ======================== We recently migrated the dashboard from being plain HTML rendered using Twig templates on the server into a React.js component. What happens to our page objects in this case? Let's take a look at our dashboard tests first:: class DashboardTest extends FeatureTest { use Helper\User; // … public function testHasDemoOrganization() { $this->logIn(); $page = (new Page\Dashboard($this->session))->visit(Page\Dashboard::PATH); $organizations = $page->getOrganizations(); $this->assertArrayHasKey('demo', $organizations); return $organizations['demo']; } // … } This test again makes assertions on a page object – now ``Page\Dashboard`` which can be instantiated after logging in successfully. The test itself does not reveal in any way if we are asserting on HTML or some JSON data. It is simple and asserts that we find the ``demo`` organization in the current users account (which you might need to enable__). So let's take a look at the dashboard Page Object, where the magic happens:: class Dashboard extends Page { const PATH = '/dashboard'; public function getOrganizations() { $dataElement = $this->find('[data-dashboard]'); $dataUrl = $dataElement->getAttribute('data-dashboard'); $data = json_decode($this->visitPath($dataUrl)->getContent()); \PHPUnit_FrameWork_Assert::assertNotNull($data, "Failed to parse JSON response"); $organizations = array(); foreach ($data->organizations as $organization) { $organizations[$organization->name] = new Dashboard\Organization($organization, $data->applications); } return $organizations; } } We are currently migrating (step by step) from jQuery modules to React.js components and are still using data attributes to trigger loading React.js components in the UI. Instead of asserting on the HTML, what we would have done when still rendering the dashboard on the server side, we check for such a data attribute and load the data from the server. For each organization found on the page we then return another object which represents a partial (organization) on the page. Using this object oriented abstraction of the page allows us to transparently switch between plain HTML rendering and React components while the test will look just like before. The only thing changed is the page object, but this one can still power many tests which can make the effort worth it. On top of those ``Organization`` partials we can then execute additional assertions:: /** * @depends testHasDemoOrganization */ public function testMonthlyRequestLimitReached(Page\Dashboard\Organization $organization) { $this->assertFalse($organization->getMonthlyRequestLimitReached()); } /** * @depends testHasDemoOrganization */ public function testHasDemoApplications(Page\Dashboard\Organization $organization) { $this->assertCount(3, $organization->getApplications()); } // … __ https://app.tideways.io/settings/profile Problems With Page Objects ========================== As you probably can guess providing a full abstraction for your frontend will take some time to write. Those page objects can also get slightly more complex, so that you might even feel like testing them at some point. Since end-to-end tests also will always be slow'ish (compared to unit tests) we advise to only write those tests for critical parts of your application. The tests will execute full HTTP requests which take time – and nobody runs a test suite which takes multiple hours to execute. Also remember that, like with any integration test, you probably need some means to setup and reset the environment the tests run on. In this simple example we run the test on the live system and assume that the user has the demo organization enabled. In a real-world scenario you'd boot up your application (provision a vagrant box or docker container), reset the database, mock some services, prime the database and run the tests against such a reproducible environment. This takes more effort to implement, again. While the tests are immune to UI changes this way (as long as the same data is still available) they are not immune to workflow changes. If your team adds another step to a checkout process, for example, the corresponding Page Object tests will still fail and you'll have to adapt them. .. note:: If you want help finding the correct testing strategy for you and get a kickstart in testing – `book us for an on-site workshop`__. __ /services/workshops/testing.html Conclusion ========== Page Objects can be a good approach to write mid-term stable end-to-end tests even for complex applications. By investing more time in your tests you can get very readable tests which are easy to adapt to most UI changes. .. Local Variables: mode: rst fill-column: 79 End: vim: et syn=rst tw=79 Trackbacks ========== Comments ========