Chapter 6. Testing the API – PHPBrowser to the Rescue

We are now going to delve into functional testing. In the previous chapter, we created the initial steps that deal with the user model, but now we will be creating the REST interface that deals with the user.

Before we even start to worry about the REST interface and its tests, we will be analyzing what's already available in the Yii basic app and later expand on the topic to create more awesome stuff.

This chapter is hence divided into three sections with an increasing level of difficulty, so keep your eyes peeled and feel free to revisit it multiple times until you understand each section which are:

  • Functional tests in Yii 2
  • Functional tests for REST interfaces
  • Creating a RESTful web service with Yii 2

Functional tests in Yii 2

As you saw in Chapter 3, Entering Codeception, we have some basic functional tests preloaded in our basic application.

Let's start digging into that and once you acquire the required knowledge, we're going to move on to the tests for the REST interface.

As you know, the basic application is composed of a few pages, a login system, and a contact form.

The functional tests cover almost everything, so let's start to see what files we have and what's their content.

Understanding and improving the available CEPTs

The tests contained in codeception/functional/HomeCept.php are quite straightforward to understand. Thanks to the syntax used by Codeception, you can easily understand what the intention of the test is, so let's break it down and see what each bit does:

$I = new FunctionalTester($scenario);

You would start by initializing the actor under which the tests will be performed. Yii uses a slightly different naming than the one officially used in the documentation and guide of Codeception, which is TestGuy, so keep that in mind when you're confronted with documentation outside of Yii's.

Tip

Remember that you can name the actors whatever you want, and their configuration is found in the suite YAML file, which for functional tests is tests/codeception/functional.suite.yml.

This class is located within the same folder as that of the other functional tests and is generated automatically by running codecept build:

$I->wantTo('ensure that home page works'),

The very first step is to declare the scope of the test in a compact but detailed way; this will help you and non-technical people to understand what went wrong and if the test is effectively doing what it is meant to be doing in a strong and comprehensive way. The method wantTo() should be called only once, as any following invocations will override what has been set previously:

$I->amOnPage(Yii::$app->homeUrl);

Our tests need a starting point; the method amOnPage() does nothing but load the given URL where our actual test will take place:

$I->see('My Company'),
$I->seeLink('About'),

In Codeception, assertions are performed through see* and dontSee* actions, ensuring a particular portion of text or link is present/absent in the page.

These actions can be as descriptive as needed, and in the preceding example with see('My Company'), we are just checking that the text is present somewhere in the markup rather than in a particular tag while seeLink('About') would be the same as writing see('About', 'a'). We will shortly see that we could pass a second parameter to seeLink(), which will allow us to check the URL where the link should point to.

Interaction with the page in the form of triggering, clicking links with click(), filling fields with fillField(), checkOption(), submitForm(), and so on is all you can do with Codeception functional tests. Anything more complicated must be re-evaluated carefully, as you might actually need to move it into acceptance tests instead:

$I->click('About'),
$I->see('This is the About page.'),

In the preceding lines, we are triggering the link of the "About" page and expecting that the resulting page has a specific copy in it. This specific test just makes a point in using links to navigate through our application, as it could have been done as described earlier by using seeLink('About', '/about') and to leave any assertion with the About page within its own test.

We might as well extend the test a bit more and make it more relevant to what we're trying to test; what are the functionality parts that we want to make sure exist, without which we can consider the page "non-functional"? In our instance, we are talking about the title of the page (as it's already been done), the menu, and any other links we always want to have there:

$I = new FunctionalTester($scenario);
$I->wantTo('ensure that home page works'),
$I->amOnPage(Yii::$app->homeUrl);

The beginning is the same, but then we ensure that the title for the page contains what we expect it to be:

$I->expect('the title to be set correctly'),
$I->seeInTitle('My Yii Application'),

The next section instead makes sure that the menu contains all the required links to the various pages:

$I->expectTo('see all the links of the menu'),
$I->seeLink('Home', '/'),
$I->seeLink('About', '/about'),
$I->seeLink('Login', '/login'),
$I->seeLink('Contact', '/contact'),

You have to keep in mind that the links are not strictly checked; this means that if you have $I->seeLink('Something', '/something'), it will match any link that contains Something; for example, it can be Something Else and any href attribute like /something/else, or even http://something.com.

In our case, it clearly renders the check for the link to the home page a bit irrelevant, so we might well grab the current URL and check against it in the following way:

$url = $I->grabFromCurrentUrl();
$I->seeLink('Home', $url);

There are different ways to grab content to be reused dynamically in the rest of the tests, such as grabAttributeFrom(), grabCookie(), grabResponse(), and so on. Once again, your FunctionalTester class will contain the details of these methods in case your IDE does not support code hinting.

We can do the same for any other link that is pointing to the homepage:

$I->expectTo('see a self-referencing link to my company homepage'),
$I->seeLink('My Company', $url);

For the rest of the links, it might also be useful to check that our routes are well configured; for instance, you need to check if the name of the controller doesn't show up:

$I->dontSeeLink('About', 'site/about'),
$I->dontSeeLink('Login', 'site/login'),
$I->dontSeeLink('About', 'site/contact'),

The last bit we want to make sure of is that the Home link is marked as selected.

For this test, we need to use a very prescriptive selector as the active class that identifies the status of our link is in the parent of the actual anchor, and as there's no way to assert that in a simple way, so making use of XPath expressions comes particularly handy:

$I->expectTo('see the link of the homepage as selected'),
$I->seeElement('//li[@class="active"]/a[contains(.,"Home")]'),

Most of the methods available that require a context selector such as click(), see(), and, seeElement() can accept this parameter in various formats, mostly as CSS selectors, XPath queries or Locators, which are specific objects made available by Codeception.

In its simplest form, selectors can be just a simple word or sentence, which means "find me the first context where this word/sentence appears". As you saw earlier, see("Something") will return the first element that contains Something as its value (for example, Something Else).

CSS selectors are probably the ones you might be more comfortable with, but for more complex stuff, XPath is generally the winner.

In the preceding example, the XPath query //li[@class="active"]/a[contains(.,"Home")], can be read as shown here:

  • Find me all the li nodes at any level (//li)
  • Filter them by a specific class attribute ([@class="active"]);—mind that is literal and case-sensitive
  • Within those find me the direct descendant a nodes (/a)
  • Filter them if they contain a specific text ([contains(.,"Home")])

Note

XPath 2.0 has been a W3C recommendation since December 2010, and you can read more about it at http://www.w3.org/TR/xpath20/.

Locators can ease the process of writing even more complex queries in your DOM and let you combine CSS and XPath queries via OR:

use CodeceptionUtilLocator;

$I->see('Title', Locator::combine('h1','h2','h3'));

With the preceding statement, we can check the presence of the Title string in any h1, h2, or h3 tag.

Another possibly useful feature is a method available in Locator that you can use to browse the page via tabIndex:

<?php
use CodeceptionUtilLocator;

$I->fillField(Locator::tabIndex(1), 'davert'),
$I->fillField(Locator::tabIndex(2) , 'qwerty'),
$I->click('Login'),

Note

The preceding example has been deliberately taken from the documentation page of Locator, available at http://codeception.com/docs/reference/Locator.

Writing reusable page interactions

Testing forms is probably one of the most strenuous tasks any developer and tester has probably ever done. You can feel the pain if you think of forms as questionnaires of several single and multiple choice questions, spread across several pages.

You can clearly see the direct benefit of automating using functional tests.

The two examples already available, LoginCept.php and ContactCept.php, are a good starting point. Let's have a closer look at LoginCept.php; if you scan through the content of the test, you will immediately notice that the fillField() method is never called, and in its place we have the following command:

$loginPage = LoginPage::openBy($I);

$I->see('Login', 'h1'),
$I->amGoingTo('try to login with empty credentials'),
$loginPage->login('', ''),

Pages are, in fact, one of the easiest ways to reuse components across tests. The sequence of actions that are repeated several times in the same test are likely to be taken and put into a page like the one used in our test:

namespace testscodeception\_pages;

use yiicodeceptionBasePage;

/**
 * Represents login page
 * @property AcceptanceTester|FunctionalTester $actor
 */
class LoginPage extends BasePage
{
    public $route = 'site/login';

    /**
     * @param string $username
     * @param string $password
     */
    public function login($username, $password)
    {
        $this->actor->fillField(
            'input[name="LoginForm[username]"]', $username
        );
        $this->actor->fillField(
            'input[name="LoginForm[password]"]', $password
        );
        $this->actor->click('login-button'),
    }
}

The only thing needed is the route associated to it and then you can implement as many methods as you need to achieve whatever you need, which is the login process in the preceding case.

Within the Page class, $this->actor is a reference to the actor that is currently in use in the test.

You have two ways to use pages; the first is by opening the page immediately and associate it with the current actor, as seen earlier with LoginPage::openBy($I), otherwise, you can simply call its constructor and load the page (also with different parameters) when needed:

$loginPage = new LoginPage($I);
$loginPage->getUrl();

Now, as you saw while working with unit tests, being able to keep the content of the database under a controlled state is very useful. And, once again, fixtures come to our help, even here.

Implementing fixtures

In Chapter 4, Isolated Component Testing with PHPUnit, you saw how to implement a fixture. In functional tests the same classes can be used; the only difference is that Codeception's PHPBrowser and its underlying infrastructure doesn't know how to load fixtures, so each framework using Codeception, like what Yii does, needs to provide the bridging to fill in this gap.

The advanced app provides the implementation for FixtureHelper that implements the Codeception Module class and imports the methods from FixtureTrait:

<?php

namespace testscodeception\_support;

use testscodeceptionfixturesUserFixture;
use CodeceptionModule;
use yii	estFixtureTrait;

/**
 * This helper is used to populate database with needed 
 * fixtures before any tests should be run.
 * For example - populate database with demo login user 
 * that should be used in acceptance and functional tests.
 * All fixtures will be loaded before suite will be
 * started and unloaded after it.
 */
class FixtureHelper extends Module
{

    /**
     * Redeclare visibility because Codeception includes
     * all public methods that not starts from "_"
     * and not excluded by module settings, in actor class.
     */
    use FixtureTrait {
        loadFixtures as protected;
        fixtures as protected;
        globalFixtures as protected;
        unloadFixtures as protected;
        getFixtures as protected;
        getFixture as public;
    }

    /**
     * Method called before any suite tests run. 
     * Loads User fixture login user
     * to use in acceptance and functional tests.
     * @param array $settings
     */
    public function _beforeSuite($settings = [])
    {
        $this->loadFixtures();
    }

    /**
     * Method is called after all suite tests run
     */
    public function _afterSuite()
    {
        $this->unloadFixtures();
    }

    /**
     * @inheritdoc
     */
    public function fixtures()
    {
        return [
            'user' => [
                'class' => UserFixture::className(),
                'dataFile' => '@tests/codeception/fixtures/data/init_login.php',
            ],
        ];
    }
}

The preceding code is quite simple, and the only important bit is that in the FixtureHelper, we implement the fixtures() method that returns the list of models handled and their data files that contain all the rows we want in the database. The only difference with the original code that is found in the advanced app is the import of the getFixture() method as public, and we'll later see why this is so.

The following code is for the init_login.php file:

<?php

return [
    'basic' => [
        'username' => 'user',
        'authkey' => uniqid(),
        'password' => Yii::$app->security->generatePasswordHash(
            'something'
        ),
    ],
];

As we imported the trait getFixture() as public, we can access the fixture through $I->getFixture('user') in a similar way to what we did in our unit tests.

Tip

If you need to load additional fixtures, you can similarly expose the loadFixtures() method from the FixtureTrait trait and use it directly in your tests.

The last step is about loading the module in Codeception configuration:

# tests/codeception/functional.suite.yml

modules:
    enabled:
      - ...
      - testscodeception\_supportFixtureHelper

And after running codecept build, the fixture will be automatically loaded when running the tests in the _beforeSuite() and _afterSuite() methods.

Pitfalls of functional tests

A word of advice is that there's plenty of information on functional tests, as well as what cannot be tested, in the official documentation.

The most important thing to grab there is all about the underlying technology that is used to perform tests; PHPBrowser is in fact a powerful tool, but as the whole functional test does not rely on the presence of a web server like you would have in a normal client-server situation, your application and functional tests will be running in the same memory space.

Tip

Normally the memory is cleaned during the _after() method execution, but remember that if you see any of your tests failing, remember to execute the test file separately, before starting to doubt your sanity.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset