Creating acceptance tests

Now that we have seen how the acceptance tests that use Selenium WebDriver are structured, we can start integrating the work done in the previous chapter and begin adding the tests we want.

For these kinds of tests, where normally the definition of the markup is left to whoever implements the layout, you would need to define the functionality of your interface, implement the tests, and then implement the markup and add the JavaScript functionality, if needed. After you've performed these, you will add the specifics of the DOM interaction.

Knowing how many developers leave the frontend functionality definition for the very end, working "tests first" will force you to change your way of working, anticipating with as much detail as needed what lies ahead and discovering immediately any critical aspects of the design.

We will try to implement something that is simple enough to get you started with from scratch and then you can improve upon or extend it later. We know that the HTTP Basic Auth, which we have used, does not permit a stateful login. Therefore, we will have to keep some sort of a session object in JavaScript to simulate it. How this is going to work can be taken from the tests that we have written for the User API. This is practical documentation at its best.

So, our scenario can be described as follows:

I WANT TO TEST MODAL LOGIN

I am on homepage
I see link 'Login'
I don't see link 'Logout' # i.e. I'm not logged
I am going to 'test the login with empty credentials'
.I click 'Login'
I wait for modal to be visible
I submit form with empty credentials
I see 'Error'

I am going to 'test the login with wrong credentials'
I submit form with wrong credentials
I see 'Error'

I am going to 'test the login with valid credentials'
I submit form with valid credentials
I don't see the modal anymore
I see link 'Logout'
I don't see link 'Login'

I am going to 'test logout'
I click 'Logout'
I don't see link 'Logout'
I see link 'Login'

The preceding syntax has been taken explicitly from the generated text version of our tests.

Implementing the modal window

The work of implementing the modal window will be made easy by Bootstrap, which is a default framework bundled with the basic app.

The modal window is composed by an almost pre-determined markup and an additional JS, which provides the interaction part, thereby hooking it to the rest of the interface. I hope that the simplicity of its implementation will let you focus on the aim that lies behind it.

The following code has deliberately been taken from the Bootstrap documentation, which can be found at http://getbootstrap.com/javascript/#modals. Since the modal window can be opened from any part of the website without going to the login page, we will have to add it to the overall layout template:

    <!-- views/layouts/main.ph -->
    </footer>

    <div class="modal fade" id="myModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
        <div class="modal-dialog">
            <div class="modal-content">
                <div class="modal-header">
                    <button type="button" class="close" data-dismiss="modal"><span aria-hidden="true">&times;</span><span class="sr-only">Close</span></button>
                    <h4 class="modal-title">Login</h4>
                </div>
                <div class="modal-body">
                    <?= $this->render('/site/login-modal.php', ['model' => Yii::$app->controller->loginForm]); ?>
                </div>
            </div>
        </div>
    </div>

<?php $this->endBody() ?>
<!-- ... -->

As you can see, we have moved the modal form to a separate template, which will receive the model of the form as a variable, keeping everything self-contained and organized. Please note from where the model takes its value. We're going to discuss it while implementing the controller.

The login-modal.php template is just a rip-off of the original login.php template, which can be found in the same directory without H1 in the title and the "remember me" checkbox. We just need to add a placeholder to show the error that is coming from the API. This is done to inform and debug it.

<!-- views/site/login-modal.php
<!-- ... -->
<div class="alert alert-danger alert-dismissible fade in">
    <button type="button" class="close" data-dismiss="alert"><span aria-hidden="true">×</span><span class="sr-only">Close</span></button>
    <p class="error"></p>
</div>
<!-- ... -->

We can place this snippet right after the first paragraph of the copied markup.

Making the server side work

As we have said before, we want the modal window to be available everywhere. We are going to accomplish this by saving a publicly accessible property in the SiteController, so that we can retrieve it from the view. Remember that if you're coming from Yii 1, then views are now separate objects and not a part of the controller.

Let's use the init() method to do so:

// controllers/SiteController.php

public $loginForm = null;

public function init()
{
    $this->loginForm = new LoginForm();
}

Once this is done, we can load our page without errors.

In the next step, we will add the interaction to JavaScript.

Adding the JavaScript interaction

We will cover a couple of things in this section. We will discuss the basic functional interaction with the modal, the interaction with the form, and then learn how to close everything with the corner cases and error scenarios.

The interaction with the modal will be achieved by reusing the already existing login button, which is at the top right side of the menu. We will disable it, but remember that it will provide a fallback compatibility in case something goes wrong.

The basic open-and-close of the modal window is provided out-of-the-box. We will only trigger it when required, for example upon authentication success.

Let's add the first basic skeleton for the JS module:

// web/js/main.js

var YII = YII || {};

For this part of our application, we will need the module pattern for creating a self-contained application.

YII.main = (function ($) {
    'use strict';

Let's start by caching all the jQuery elements that we are going to need along the way:

    var $navbar = $('.navbar-nav.nav'),
        $modal = $('#myModal'),
        $modalBody = $modal.find('.modal-body'),
        $modalAlertBox = $modal.find('.alert').hide(),
        $modalError = $modalAlertBox.find('.error'),
        $CTALogin = $navbar.find('.login'),

Once we have logged in, we will swap the link with a "fake" logout button:

        $CTALogout = $('<li class="logout"><a href="#">Logout</a></li>'),

We will need some data fields for holding our login information and creating some sort of a session:

        authorization = null,
        username = null,
        userID = null;

Now comes the main part of our script, in which we will initialize our event listeners to the click and submit actions:

    /**
     * initialise all the events in the page.
     */
    (function init() {
        $navbar.append($CTALogout);
        $CTALogout.hide();

Let's start by appending and hiding our logout button; we will show it only when the login succeeds, and define the click action it should have:

        $navbar.on('click', '.logout a', function (e) {
            e.preventDefault();

            // unset the user info
            authorization = null;
            username = null;
            userID = null;

            // restore the login CTA
            $CTALogout.hide();
            $CTALogin.show();
        });

We need to disable the click event for the login button. Otherwise, we will be taken to the login page, instead of opening the modal:

        $navbar.on('click', '.login a', function (e) {
            e.preventDefault();
        });

The modal triggering event is done automatically by modifying the markup of the login button. So, navigate to views/layouts/main.php and then adjust it as follows:

'label' => 'Login',
'url' => ['/site/login'],
'options' => [
    'data-toggle'=>'modal',
    'data-target'=> '#myModal',
    'class' => 'login'
]

Next, we will deal with the form submission:

        $modalBody.on('submit', '#login-form', function (e) {
            e.preventDefault();

After disabling the default submit event, we will need to capture the username and the password, and then save it for future use:

            username = this['loginform-username'].value;
            // we don't care to store the password... sorta
            authorization = btoa(username + ':' + this['loginform-password'].value);

The authorization variable will hold our authorization header that is ready for dispatch:

            $.ajax(
                {
                    method: 'GET',
                    url: '/v1/users/search/' + username,
                    dataType: 'json',
                    async: false,
                    beforeSend: authorize,
                    complete: function (xhr, status) {

                        if (status === 'success') {
                            // save the user ID
                            userID = xhr.responseJSON.id;
                            // set the logout button
                            $CTALogin.hide();
                            $CTALogout.show();
                            // clear the status errors
                            $modalError.html(''),
                            $modalAlertBox.hide();
                            // close the modal window
                            $modal.modal('hide'),
                        }
                        else {
                            // display the error
                            $modalError.html('<strong>Error</strong>: ' + xhr.statusText);
                            $modalAlertBox.show();
                        }
                    }
                }
            );
        });
    })();
})(jQuery);

This code is simple enough. In case of success, we save the user ID for subsequent calls, we hide the login button and display the logout one, clear the error message, and hide the modal window. Otherwise, we just display the error message.

The beforesend option will be initialized by the authorize function, which is defined as follows:

    /**
     * modifies the XHR object to include the authorization headers.
     *
     * @param {jqXHR} xhr the jQuery XHR object, is automatically passed at call time
     */
    function authorize(xhr) {
        xhr.setRequestHeader('Authorization', 'Basic ' + authorization);
    }

After doing this, we won't need anything else to interact with the page. So, let's put everything together.

Tying everything together

At this point, we only have to add our JS to the page and then finalize our tests. In order to add our file to the page, we need to know what assets and asset bundles are.

Dealing with Yii 2 assets bundles

Yii 2 has radically changed the way assets are handled. It has introduced the concept of the asset bundle.

Asset bundles are collections of scripts and style sheets that can have a higher degree of configurability as compared to the past.

This basic app already has a basic implementation. So, let's navigate to /assets/AppAsset.php and see how the content is structured:

<?php // assets/AppAsset.php

namespace appassets;

use yiiwebAssetBundle;

class AppAsset extends AssetBundle
{
    public $basePath = '@webroot';
    public $baseUrl = '@web';
    public $css = [
        'css/site.css',
    ];
    public $js = [];
    public $depends = [
        'yiiwebYiiAsset',
        'yiiootstrapBootstrapAsset',
    ];
}

The AppAsset extends from the yiiwebAssetBundle class and it simply defines a series of public properties.

The first two properties, $basePath and $baseUrl, are the most important ones. $basePath defines where the assets are located on a publicly accessible location, while $baseUrl defines how their resource is linked to the web pages, that is, their URL.

This asset, by using these two properties, defines the so called "published asset". In other words, it defines a bundle of assets, which are available at a publicly accessible location.

You can have "external assets", which are comprised of resources from external locations, and "source assets", which are not comprised of resources from publicly available locations. These assets define only a $sourcePath property and Yii copies them to the publicly accessible assets folder, and names them accordingly.

Source assets are normally provided by libraries and widgets, and for this reason, we won't be covering them here. Published assets are recommended for incorporating assets into the page or pages by putting them somewhere in the web/ folder.

In the example earlier, you saw that we defined the asset dependencies, and in our case, it's done with jQuery and Bootstrap. This is exactly why we've used them for developing the main JavaScript module.

Lastly, we need to see how we can use the asset bundle for our markup. This can be done by looking at the top of the template view. For this, navigate to /views/layouts/main.php. Here, we can see these two lines:

// views/layouts/main.php
use appassetsAppAsset;
AppAsset::register($this);

Remember that the old way of associating any asset with a specific layout, although it's not particularly advisable, hasn't been removed. This works in the same way as it was working in Yii 1, that is, by using registerCssFile() and registerJsFile().

Assets have many other options, such as the ability to compress and compile SASS and LESS files, use Bower or NPM assets, and so on. Go to the documentation page, which is currently in a good shape and is quite comprehensive, at http://www.yiiframework.com/doc-2.0/guide-structure-assets.html.

For our work, we need to slightly adjust the asset bundle provided by adding the JS file and tweak it where it's going to be added to the page, otherwise we will encounter some problems in running it before the page is parsed. Consider the following code snippet:

    public $js = [
        'js/main.js',
    ];
    public $jsOptions = [
        'position' => yiiwebView::POS_END
    ];

Once you've added the preceding lines to the asset bundle, you need to head back to the form template that is included in the modal. This, in fact, will generate some problems because it requires injecting some script into the page in order to make the client-side validation work. This is a major problem; most of the time you will have to override the way ActiveForms works, so you should learn how to do it.

// views/site/login-modal.php

    <?php $form = ActiveForm::begin([
        'id' => 'login-form',
        'options' => ['class' => 'form-horizontal'],
        'enableClientScript' => false,
        'enableClientValidation' => false,
        'fieldConfig' => [
            'template' => "{label}
<div class="col-lg-4">{input}</div>
<div class="col-lg-5">{error}</div>",
            'labelOptions' => ['class' => 'col-lg-offset-1 col-lg-2 control-label'],
        ],
    ]); ?>

The two options shown here will disable both the client-side validation and any additional scripting facility. Disabling only one option won't do the trick.

We can now load the page and no error message will be displayed on the console.

Finalizing the tests

At this point, we just have to convert our scenarios into live code.

Let's start creating the test in the same way as we created unit tests and functional tests:

$ ../vendor/bin/codecept generate:cept acceptance LoginModalCept
Test was created in LoginModalCept.php

Navigate to the file and let's start by asserting the initial statements: where we are and ensure that we are not logged in:

<?php
// tests/codeception/acceptance/LoginModalCept.php
$I = new AcceptanceTester($scenario);
$I->wantTo('test modal login'),

$I->amOnPage(Yii::$app->homeUrl);
$I->seeLink('Login'),
$I->dontSeeLink('Logout'),

Although this might seem like a simplistic way of determining whether the user is logged in, it serves the purpose. If we find that anything more complex is needed, then we can always add it to the mix later:

$I->amGoingTo('test the login with empty credentials'),
$I->click('Login'),
$I->waitForElementVisible('.modal'),
$I->fillField('#loginform-username', ''),
$I->fillField('#loginform-password', ''),
$I->click('#login-form button'),
$I->see('Error', '.alert .error'),

The most important part of this test is the use of an explicit wait, waitForElementVisible(). It does what it says on the tin: waits until the DOM element with class .modal is rendered and visible.

The assertion made at the end does not check for any specific errors. So feel free to add any level of customization here, as I've tried to be as generic as possible.

The same goes for the following test:

$I->amGoingTo('test the login with wrong credentials'),
$I->fillField('#loginform-username', 'admin'),
$I->fillField('#loginform-password', 'wrong password'),
$I->click('#login-form button'),
$I->see('Error', '.alert .error'),

This interesting part of the test comes when we're trying to access using valid credentials. In fact, as we've seen in the script we created previously, the modal window will be dismissed and the login button will be replaced by the logout link:

$I->amGoingTo('test the login with valid credentials'),
$I->fillField('#loginform-username', 'admin'),
$I->fillField('#loginform-password', 'admin'),
$I->click('#login-form button'),
$I->wait(3);
$I->dontSeeElement('.modal'),
$I->seeLink('Logout'),
$I->dontSeeLink('Login'),

In order to do this, we need to add another explicit wait for the AJAX call to complete and then the window will disappear. Using waitForElementNotVisible() might not do the trick because it involves animation. It also depends on the responsiveness of the system you're testing on because it might not work as expected and fail from time to time. So, wait() seems like the simplest solution for the problem. Consider this code snippet:

$I->amGoingTo('test logout'),
$I->click('Logout'),
$I->dontSeeLink('Logout'),
$I->seeLink('Login'),

The last test doesn't need much attention and you should be able to understand it without facing any problems.

Now that we have put together our tests, let's run them:

$ ../vendor/bin/codecept run acceptance
Codeception PHP Testing Framework v2.0.9
Powered by PHPUnit 4.6-dev by Sebastian Bergmann and contributors.

Acceptance Tests (6) ---------------------------------------------
Trying to ensure that about works (AboutCept) Ok
Trying to ensure that contact works (ContactCept) Ok
Trying to ensure that home page works (HomeCept) Ok
Trying to ensure that login works (LoginCept) Ok
Trying to test modal login (LoginModalCept) Ok
------------------------------------------------------------------

Time: 47.33 seconds, Memory: 13.00Mb

OK (5 tests, 32 assertions)

Testing multiple browsers

Version 1.7 onwards, Codeception also provides a method for testing multiple browsers. This is not particularly difficult, and it can ensure that cross-browser compatibility is achieved.

This is normally done by configuring environments in the acceptance.suite.yml file by adding something similar to the following at the bottom of the file:

env:
    chrome39:
        modules:
            config:
                WebDriver:
                    browser: chrome
    firefox34:
        # nothing changed
    ie10:
        modules:
            config:
                WebDriver:
                    host: 192.168.56.102
                    browser: internetexplorer

Each key under the env variable represents a specific browser you want to run the test on, and this is done by overriding the default configuration that we have already defined.

Tip

Within env, you can override just about any other key that was specified in the YAML configuration files.

You can have several machines, each having different versions of the browsers, with Selenium Server listening on them, so you can also perform retro-compatibility tests when deciding which polyfills to use for the new features introduced recently and also depending on your browser support chart.

In order to trigger the various environments, just append the --env <environment> parameter to the run command:

$ ../vendor/bin/codecept run acceptance --env chrome39 --env firefox34 --env ie10

Internet Explorer requires its own driver to be installed on the host machine and a few more steps to be performed to set it up correctly, which is covered in the Selenium documentation, which can be found at https://code.google.com/p/selenium/wiki/InternetExplorerDriver.

Understanding Selenium limits

By now, you have probably seen how powerful Selenium is. By using the browser natively, you can finally interact with the website programmatically. This will save a huge portion of time that is normally spent by human beings on doing repetitive tasks. Repetitiveness is only a cause of problems when it comes into the hands of humans, so this is effectively a good thing.

Unfortunately Selenium can't do everything, and if you have already started looking into it and researching its full use and potential, then you might have noticed that there are some limitations of its use.

Clearly any kind of "pixel-perfect" tests are nearly impossible to recreate with Selenium, although some types of tests on designs can be created, specifically for responsive designs. Other frameworks, such as Galen cover this functionality (http://galenframework.com/).

A few words need to be spent on hover effects, as they might be quite difficult to achieve and you may need to use the moveMouseOver() method for triggering it.

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

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