Laravel Behat and Selenium

Posted: 2015-02-02 19:51:32

Laracasts has some great videos and libraries for Laravel 5 and Behat integration.

Examples

https://github.com/laracasts/Behat-Laravel-Extension

and

https://laracasts.com/lessons/laravel-5-and-behat-bffs

Two things that I still need and get from this though that I do not think I can get from those are

  • Laravel 4.2 support which obviously is not going to work with the above L5 libraries :)
  • Mocking APIs when running under APP_ENV=local or testing

Also I think with the libraries above only goutte drivers work for the APP_ENV setting.

Dealing with APIs

We use a lot of APIs. One for example is Github so make a provider like this

The Provider

I register an API Provider like this

<?php

namespace BehatEditor\Services;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\ServiceProvider;
class GitApiServiceProvider extends ServiceProvider {
    /**
     * Register the service provider.
     *
     * @return void
     */
    public function register()
    {
        if(App::environment() == 'testing' || Config::get('app.mock') == true)
        {
            $this->app->singleton('GithubClientInstance', function($app){
                $git = new GithubApiMockService();
                $username    = getenv('GIT_USERNAME');
                $token       = getenv('GIT_TOKEN');
                $git->setUsername($username);
                $git->setToken($token);
                $git->setLogger($app['log']);
                $git->setLogging(true);
                $git->authenticate();
                return $git;
            });
        } else
        {
            $this->app->singleton('GithubClientInstance', function($app){
                $git = new GithubApiService();
                $username    = getenv('GIT_USERNAME');
                $token       = getenv('GIT_TOKEN');
                $git->setUsername($username);
                $git->setToken($token);
                $git->setLogger($app['log']);
                $git->setLogging(true);
                $git->authenticate();
                return $git;
            });
        }
    }
}

The "app.mock" I set in two places

config/local/app.php

and

config/testing/app.php
<?php

return array(

    'chat_on' => false,
    'mock'  => true,
    'debug' => true,
    'providers' => append_config(array(
        'GuilhermeGuitte\BehatLaravel\BehatLaravelServiceProvider'
    ))
);

Testing works well for Codeship.

Then if we have mock as true for local and I run

php artisan behat:run --profile=als_local_ui --stop-on-failure ui/people_ui.feature

I can test all my Angular ui for People

Here is the behat.yml for that, keep in mind I run this from inside of Vagrant (Homestead) and Selenium is running on my Mac thanks to "webdriver-manager" and brew install chromedriver you can see more on that here

default:
    filters:
      tags: "~@wip"
    formatter:
            name:                       pretty
            parameters:
                decorated:              true
                verbose:                false
                time:                   true
                language:               en
                output_path:            null
                multiline_arguments:    true
    paths:
        features: app/tests/acceptance/features
        bootstrap: app/tests/acceptance/contexts
    context:
      parameters:
        base_url: http://behat.dev
        asset_path: '/tmp/'

als_local_ui:
  extensions:
    Behat\MinkExtension\Extension:
      default_session: selenium2
      goutte:
        guzzle_parameters:
          curl.options:
            CURLOPT_SSL_VERIFYPEER: false
            CURLOPT_CERTINFO: false
            CURLOPT_TIMEOUT: 120
          ssl.certificate_authority: false
      selenium2:
        wd_host: "http://192.168.33.1:4444/wd/hub"
      base_url: 'https://admin:foo@behat.dev:44300'
      browser_name: chrome

The Mock Class

The mock class just extends the real class but takes over

If mock is on it looks for a matching fixture file and uses that, else it makes one real call, saves the fixture and then uses that next time.

<?php
namespace BehatEditor\Services;


use AlfredNutileInc\Fixturizer\FixturizerReader;
use BehatEditor\Exceptions\ModelException;
use BehatEditor\Helpers\BuildFileObject;
use BehatEditor\Helpers\ThrowAndLogErrors;
use BehatEditor\Providers\GithubClientInterface;
use Github\Client;
use Github\ResultPager;
use BehatEditor\Interfaces\BehatUIInterface;
use BehatEditor\Repositories\ProjectsRepository;
use Illuminate\Support\Facades\Log;

class GithubApiMockService extends GithubApiService implements GithubClientInterface {

    public $sha;
    protected $application;

    /**
     * @var \Github\Client
     */
    public $client;
    protected $username;
    protected $token;
    protected $branch;
    protected $parent_file;
    protected $reponame;
    protected $folder;
    protected $logging = false;
    protected $logger;

    /**
     * @var RepoSettingRepository
     */
    private $repoSettingRepository;

    public function __construct(Client $client)
    {
        $this->client       = $client;
        $this->path = base_path() . '/tests/fixtures/';
    }

    public function seeIfRepoHasCustomSteps()
    {
        $this->logMock('repo_has_custom_steps');
        $results = FixturizerReader::getFixture('git_show_repo_custom_steps.yml', $this->path);

        return $results;
    }

I am using this library to quickly make fixtures https://packagist.org/packages/alfred-nutile-inc/fixturizer

That makes our tests super fast since we are never hitting out APIs like Github, Pusher, etc.

I cover it Mocking Queue Service for faster Behat Testing as well.

API Testing

We use Behat to test our API endpoints as seen in the book Build APIs You Won't Hate

For hitting the API we use basic.once

#filter.php
Route::filter('basic.once', function()
{

        if(Auth::guest())
        {
            /**
             * First authenticate as normal
             */
            if ($results = Auth::onceBasic() )
            {
                return $results;
            }

        }
});

And the route would be

Route::group(['prefix' => 'api/v1', 'before' => 'basic.once|auth'], function() {
///routes
}

This allows our Angular app which happens to live inside the same codebase of the API to login using a standar Laravel Form but also allows other apps to access the API (Oauth coming soon)

Reseeding the DB

This step helps with that

   /**
     * @Given /^I reseed the database$/
     */
    public function iReseedTheDatabase()
    {
        $env = getenv('APP_ENV');
        if(getenv('APP_ENV') != 'production')
        {
            try
            {
                if(getenv('APP_ENV') == 'testing')
                {
                    copy(__DIR__ . '/../../../../app/database/stubdb.sqlite', __DIR__ . '/../../../../app/database/testing.sqlite');
                }
                else
                {
                    exec("php artisan migrate:refresh --seed -n --env=$env");
                }
            }
            catch(\Exception $e)
            {
                throw new \Exception(sprintf("Error seeding the database %s", $e->getMessage()));
            }
        } else {
            throw new \Exception(sprintf("You can not seed production"));
        }
    }

I cover more on that PHP quick fixture data for phpunit testing

Loading APP

FeatureContext has a BaseContext that has these methods

    public function setApp()
    {
        $app = new Illuminate\Foundation\Application;
        $env = $app->detectEnvironment(
            function()
            {
                if(!getenv('APP_ENV'))
                {
                    Dotenv::load(__DIR__ .'/../../../../');
                }
                return getenv('APP_ENV');
            }
        );
        $app->bindInstallPaths(require __DIR__ . '/../../../../bootstrap/paths.php');
        $framework = $app['path.base'].
            '/vendor/laravel/framework/src';
        require $framework.'/Illuminate/Foundation/start.php';
        $this->app = $app;
        $this->app->boot();
        $this->env = $env;
    }

    public function getApp()
    {
        return $this->app;
    }

On the __construct it does

    public function __construct(array $parameters) {


        $config = isset($parameters['guzzle']) && is_array($parameters['guzzle']) ? $parameters['guzzle'] : [];

        $config['base_url'] = (isset($parameters['base_url'])) ? $parameters['base_url'] : false;

        $this->parameters = $parameters;

        $this->client   = new Client($config);

        $this->iSetCredentials();

        Factory::$factoriesPath = 'app/tests/factories';

        $this->setApp();
    }

Laracast TestDummy / Factories

One example of using factories is a step like this

    /**
     * @Given /^I create person fixture with "([^"]*)" id$/
     */
    public function iCreatePersonFixtureWithId($arg1)
    {
        Factory::create('TheHub\Profile\User', [ 'id' => $arg1 ]);
    }

Using the Laracast TestDummy library I can quickly stub out data for the test.