Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

JeffreyWay's avatar

Acceptance Tests While Setting the Environment Name

Let's say that you're writing an acceptance test that actually opens a browser and works with the UI. Maybe with something like Codeception or Behat.

Often, you'll want to use a special environment for these tests: maybe like "acceptance," and that'll reference a different database or something.

My question to you all is: how do you go about doing this? In the past, I've simply created a custom url, like http://app.testing/, and then associated a APP_ENV with that particular domain. However, this can be tedious, and I'd like to have an easier and quicker way to handle it in Laravel 5.

I know that the Symfony folks use a multi-front-controller approach. Any other ideas?

0 likes
22 replies
mdwheele's avatar

Is modifying .env from a @BeforeScenario on the table? If you're using something like Selenium, Goutte, or PhantomJS; I've done this before and it works well enough in a pinch. It definitely has downsides and feels wrong, but it is what it is. Just a little background; we didn't do this for a Laravel application. We were approaching a legacy application with tons of code-rot and wanted to establish a baseline of user-story-backed acceptance tests through Behat. The application did have some level of configuration management which we rigged up similar to what I mention above.

Alternatively, you can have the acceptance tests drive their own application object. You can lose some convenience in writing step definitions, depending. Is this what you mean by "multi-front-controller approach"? Dedicated bootstrapped application for acceptance tests?

afrayedknot's avatar

They way I do it is one small change in the environment detection code. Because Codeception etc are called via the command line on your server, you can detect the localcall, and adjust accordingly:

In Laravel 5 I change environment.php from this:

$env = $app->detectEnvironment(function()
{
    return getenv('APP_ENV') ?: 'production';
});

to this

$env = $app->detectEnvironment(function()
{
    if ((gethostname() === 'homestead') && (isset($_SERVER['REMOTE_ADDR'])) && ($_SERVER['REMOTE_ADDR'] === '127.0.0.1'))
    {
        return 'acceptance';
    }

    return getenv('APP_ENV') ?: 'production';
});

Or in Laravel 4 I change the code to be like this

if ((gethostname() === 'homestead') && (isset($_SERVER['REMOTE_ADDR'])) && ($_SERVER['REMOTE_ADDR'] === '127.0.0.1'))
{
    $env = $app->detectEnvironment(['acceptance' => ['homestead']]);
}
else
{
    $env = $app->detectEnvironment(['local' => ['homestead']]);
}

It works flawlessly, only one change per project, and allows you to set specific acceptance test environment, regardless if it is Behat, Codeception etc.

p.s. I put the gethostname() detection in there for security, because you dont want to just trust $_SERVER variables. This way it can only ever be triggered on your local homestead environment.

1 like
afrayedknot's avatar

Actually - I re-read your question. This probably wont work for a browser UI interaction, because it is not coming via the command line curl?

But it will work for other acceptance testing that interacts via curl.

afrayedknot's avatar

What about this? I dont know if it is much better/different to what you do - but only takes a couple of seconds:

In Homestead.yaml

    - map: dev.vm
      to: /home/vagrant/www/dev/public  

    - map: dev.acceptance
      to: /home/vagrant/www/dev/public 

and then in your environment detection

$env = $app->detectEnvironment(function()
{
    if ((gethostname() === 'homestead') && (isset($_SERVER['SERVER_NAME'])) && ($_SERVER['SERVER_NAME'] === 'dev.acceptance'))
    {
        return 'acceptance';
    }

    return getenv('APP_ENV') ?: 'production';
});

Then just run your acceptances tests at http://dev.acceptance?

tzookb's avatar

I used the same env for my acceptance as in my unit.

Sqlite in memory, and laravel module added to codeception.

But now I look for removing the laravel module as it outputs blank screenshots

JohnRivs's avatar

@theshiftexchange you said the Codeception calls are made via command line. Does that mean you can do:

if (App::runningInConsole())

plus some other checking?

afrayedknot's avatar

@JohnRivs - unfortunately not. That command is for when you run it via artisan.

But because you are calling the Codeception calls via curl (from the command line) - you can check for the localhost as above.

FrankPeters's avatar

I had to deal with this issue last weekend when trying to get Behat working on L5. See my topic here: https://laracasts.com/discuss/channels/general-discussion/l5-behat-testing-environment

Basically I have a .test url for my app, check for that in the environment detection but also reset the DB via the @beforeSuite by using the Dotenv::setEnvironmentVariable() method as it kept picking up a local environment there. But any suggestions for improvements are appreciated.

1 like
carvefx's avatar

I also use a project.testing domain which is then handled in the detectEnvironment() function. It feels totally dirty and as a bonus it's a PITA to set up with something like a CI server.

afrayedknot's avatar

@mthomas - you just put it all into \config\acceptance\* - since you are forcing the environment to be run in acceptance mode.

So in my projects I have local, testing and acceptance environments.

MThomas's avatar

@theshiftexchange but that will put your files (unless you add them to your gitignore file) to github/bitbucket. And I don't want to do that with my Mandrill test key for example. For those third party services I added the sensitive data to a .env file.

Update Ah wait! I can put test env vars in the .env file but prepend them with TEST_ for example. And add those keys to config/testing/.

erikthedeveloper's avatar

I ran into a similar question recently (not w/ testing UI but API). Basically what I am trying to accomplish is:

  • Test a RESTful json API using Behat (currently using Guzzle to issue HTTP requests to it)
  • For each scenario/context have a fresh database by using either an in memory database or running migrate:refresh
  • Be able to contextually seed data per scenario/suite

Ran into a weird issue in trying to use an in memory sqlite database where I would try and set the ENV to testing in my Behat FunctionalFeatureContext and use a uri based ENV -> testing detection on the API.

// bootstrap/start.php
$env = $app->detectEnvironment( function () {
    // Detect testing environment for HTTP requests
    if (isset($_SERVER['HTTP_HOST']) && $_SERVER['HTTP_HOST'] == 'api.my-great-app.test')
        return 'testing';
    if (getenv('APP_ENV'))
        return getenv('APP_ENV');
});

$env = ($env == 'production') ?: $app->detectEnvironment([
 'development' => ['homestead'],
 'homestead_host' => ['MYCOMPUTERNAME']
]);
// AcceptanceFeatureContext (Behat)

    /**
     * @static
     * @beforeSuite
     */
    public static function bootstrapLaravel()
    {
        $unitTesting = true;
        $testEnvironment = 'testing';

        $app = require_once __DIR__ . '/../../bootstrap/start.php';
        $app->boot();

        Artisan::call('migrate:install');
        Artisan::call('migrate:refresh');
    }

Turns out this renders my contextual seeding useless, since when the HTTP requests are issued via Guzzle, it is hitting my api.my-great-useless-app.test which is using the same testing environment (w/ in memory database). So that any context specific seeding/etc... is negated since the seeding is initiated by my scenario... (example below)

// some_feature.feature
Given there are 127 existing users // Scenario specific contextual seeding
When I request GET "/users?per_page=50?page=2"
Then the response entry "pagination" entry "page" should be "2"
Then the response entry "pagination" entry "total" should be "127"
And the response entry "items" should have a count of 50

sorry if this is a bit confusing/circular. I think that covers my situation well enough ;)

One alternative I have yet to play around with is to:

  • Forget using the in memory database
  • Simply run artisan migrate:refresh before every scenario.
  • Perform any contextual seeding within each scenario
  • Issue requests to the API (which will be using the testing freshly refreshed/seeded database)
  • Make assertions/etc... on the returned response

My suspicion is that this will turn out much cleaner than trying to wrestle in using database transactions before/after every scenario.

I have yet to try Behat's WebApiExtension. I'm curious if this would ease my pain...

I would love to hear some alternatives and/or improvements on this (I really would prefer to stick with Behat for this). I'm just touching the surface on using Behat so, I'm positive that @JeffreyWay (and others) must have a way that is 437% easier.

MikeHopley's avatar

I wanted to run my acceptance tests against the live site, without breaking it. For that I needed to force Codeception to use a separate database.

This thread was really helpful getting me started. I ended up using a cookie to trigger the testing environment. It was quite a hassle, so I documented the procedure for future me. In case it helps anyone else, I'll share it here. I've gone step-by-step because it helps doofuses like me.

Create a new user profile in Firefox. I called my profile codeception. Open Firefox with this profile and install Firebug, then go to your site and use Firebug to set a cookie.

I had trouble getting Firebug to set a long-lasting cookie, so I opened about:config and changed extensions.firebug.cookies.defaultExpireTime to 1000000000 -- that's one billion seconds, or about 32 years.

Set the cookie from Firebug's cookie panel > Cookies > Create cookie. I called the cookie codeception and gave it a value of super-secret-password. You will need to do this on each of the subdomains where you want to run the tests (e.g. yourdomain, dev.yourdomain, stage.yourdomain...).

At this point you might want to uninstall Firebug from your new profile and restart Firefox, as this will reduce file sizes a lot. Find and open your profile folder. Now close Firefox.

For Codeception to use this profile, it must be zipped and base-64 encoded. Zip the contents of the folder, not the folder itself. Then run a base-64 encoding tool on it (e.g. this one). Afterwards, you should have a file like abc1xy2z.codeception.zip.b64.

Copy this file to your Laravel app; you can also rename the file. Mine was app/tests/_data/browsers/firefox/profile.zip.b64. Now configure your acceptance.suite.yml. You will need Codeception 2.0.7 or later for this to work:

class_name: WebGuy
modules:
  enabled: [WebDriver, Db]
  config:
    WebDriver:
      url: 'http://yourdomain.com'
      browser: firefox
      capabilities:
        firefox_profile: 'app/tests/_data/browsers/firefox/profile.zip.b64'

At this point you have set up Codeception to launch a different Firefox profile, which contains a special cookie. Now in your Laravel environment detection, you can use this to trigger the acceptance testing environment:

// bootstrap/start.php

$env = $app->detectEnvironment(function()
{
    // Use ENV files, ignored in Git, for reliable and safe detection
    if (file_exists(__DIR__ . '/../.env_name.php'))
    {
        $env = include(__DIR__ . '/../.env_name.php');
    }
    else
    {
        $env = 'staging'; // The least dangerous option!
    }

    // Check for a cookie to tell if we're running acceptance tests
    if ( isset($_COOKIE['codeception'])
        && $_COOKIE['codeception'] === 'super-secret-password' )
    {
        $env = 'acceptance';
    }

    return $env;
});

Now Codeception will run in the acceptance environment. You can provide a different database name (e.g. testing) and credentials for this environment. Of course it's important not to use your production database credentials, especially when configuring the Db module in codeception.yml for direct database access.

One thing I haven't shown here is my whole acceptance.suite.yml setup. To make this useful, you need to create a new environment for each site you want to test. You can then run the tests from your local machine, with codecept run acceptance --env=environment_name. This is covered in the Codeception documentation, but if anyone wants an example, just ask.

MikeHopley's avatar

Update & warning

There is a subtle quirk in my method. Running an individual test (codecept run acceptance someCept) will trigger the acceptance enviroment as desired. But running the whole acceptance suite (codecept run acceptance) will not. Instead you will get the normal environment.

If you are in production, then the acceptance suite will run in the production environment and wreck your production database.

I should have known testing on the live site was a Bad Idea -- even if you think you know what you're doing. Thankfully no damage was done in my case.

(I will still want to fix this out for use on the staging site. More to come later.)

MikeHopley's avatar

Having tried and failed to make my fancy Firefox profile trick work properly, I've concluded it's just not worth the hassle.

I have reverted to creating a separate "site" for testing, as Jeffrey described in the first post.

One improvement though: I like to run tests locally first. You can do this by creating a testing.local virtual host, and doing a check on $_SERVER['HTTP_HOST'] in your environment detection.

MikeHopley's avatar

One more thing, which may be obvious to some but wasn't for me:

If you are using Webdriver for your acceptance tests, you can run into problems testing on the remote server (e.g. testing.your-domain.com). I'm not sure how @JeffreyWay set it up, but my guess is that he's using PHPbrowser instead.

PHPbrowser and Webdriver have their pros and cons. PHPbrowser is much faster, but Webdriver is more "realistic" and flexible because it runs a real browser and allows javascript interactions.

Your acceptance tests need direct access to the database, so they can clean up after the test and reset the database to its default state (using a database dump to define that default state). Without this, your acceptance tests are not independent of each other.

This is no problem when running Webdriver locally. But on your real webserver, you wouldn't have a desktop environment installed, so you cannot access a web browser. Therefore you cannot run acceptance tests from the server. This is not an issue for PHPbrowser, which does not require a real browser.

Instead, you need to run Webdriver from your local machine (well, from a machine with a browser installed). But MySQL is configured not to listen for remote connections (or at least, it should be configured this way). While you can potentially change this, it's really not good for security as it's a server-wide setting.

Instead, you can use local port forwarding through an SSH tunnel. This allows you to address the remote MySQL database as if it were running locally on your system. This means Codeception will be able to reset the database after each test. The tests will of course be a bit slower than running locally, but this is not something you would be doing all the time.

This procedure does not weaken your server's security at all. Links:

mass6's avatar

@theshiftexchange @JeffreyWay - Thanks for the great guidance and tips on this. I was able to come up with a solution that doesn't require using a separate testing url.

I took most from your solution, but instead of checking the REMOTE_ADDR value, I check the HTTP_USER_AGENT value. I found that when run from Behat, the HTTP_USER_AGENT will be set to 'Symfony2 BrowserKit'.


if ((gethostname() === 'homestead') && (isset($_SERVER['HTTP_USER_AGENT'])) && ($_SERVER['HTTP_USER_AGENT'] === 'Symfony2 BrowserKit'))
{
    $env = $app->detectEnvironment(['acceptance' => ['homestead']]);
}
else
{
    $env = $app->detectEnvironment(['local' => ['homestead']]);
}

Thanks again!

1 like
parrker's avatar

@mass6 I can confirm that this solution also works for Codeception (PhpBrowser).

In Laravel 5 just add this to the bootstrap/app.php right before the return statement:

if ((gethostname() === 'homestead') && (isset($_SERVER['HTTP_USER_AGENT'])) && ($_SERVER['HTTP_USER_AGENT'] === 'Symfony2 BrowserKit'))
{
    $app->loadEnvironmentFrom('.env.testing');
}

return $app;
2 likes

Please or to participate in this conversation.