Codeception / module-symfony

Codeception module for testing apps using Symfony framework
MIT License
89 stars 24 forks source link

Symfony: `.env.local` and `.env.test` are ignored #99

Closed ThomasLandauer closed 7 months ago

ThomasLandauer commented 5 years ago

Starting in November 2018, Symfony has changed the way .env files are working: https://symfony.com/doc/current/configuration.html#the-env-file-environment-variables

These changes are not realized in Codeception.

To assert this, I created a controller which just outputs an environment variable:

return new Response(getenv('MAILER_URL'));`

When I access the url with codeception (codecept_debug($I->grabPageSource());), I always get what I have in .env (should be .env.local or .env.test).

However, when I open the page in the browser in DEV environment, I get the content of .env.dev, when in TEST environment, .env.test; this is the expected behavior.

Details

Naktibalda commented 5 years ago

@ThomasLandauer Can you fix it?

leroy0211 commented 5 years ago

Same here

alexkunin commented 5 years ago

I have similar issue, it happens because Codeception neither loads environment directly nor uses Symfony's way to do that (specifically, does not include config/bootstrap.php).

My solution is the following snippet in codeception.yml:

params:
  - .env.test

Maybe it will be better to modify Symfony module to check version and config/env file layout and automatically load config/bootstrap.php. Though, that file could contain custom code which might not be desired in testing environment. So, the fix above is a bit awkward but also least risky for now?

c33s commented 5 years ago

the main problem is, that config/bootstrap.php is not loaded and also that phpunit.xml or phpunit.xml.dist also are not considered.

my phpunit.xml.dist:

...
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/6.5/phpunit.xsd"
         backupGlobals="false"
         colors="true"
         bootstrap="config/bootstrap.php"
>
    <php>
        <ini name="error_reporting" value="-1" />
        <server name="APP_ENV" value="test" force="true" />
...

so phpunit normally loads config/bootstrap.php and also sets APP_ENV=test. so a solution could be to parse the phpunit.xml* and respect the content.

my workaround is to rebuild the behavior of phpunit.xml.dist with codeception.

  1. remove all params from codeception.yml
  2. load symfonys config/bootstrap.php in codeceptions tests/_bootstrap.php

ad 1.

#params:
#    - .env.test # get params from .env.test file
#    - env # to get params from environment vars (only real env)
#    - .env # get params from .env file

ad 2. tests/_bootstrap.php:

// force test environment
$_ENV['APP_ENV'] = 'test';
// load the symfony bootstrap file to ensure correct dotenv handling.
require dirname(__DIR__).'/config/bootstrap.php';

ad 3.

Add this to your codeception.yml to load the new tests/_bootstrap.php you just created - see https://codeception.com/docs/reference/Configuration#Global-Configuration:

    bootstrap: _bootstrap.php

edit: updated comment with comment from @ThomasLandauer https://github.com/Codeception/Codeception/issues/5411#issuecomment-585279655

ThomasLandauer commented 4 years ago

@c33s solution works for me (Symfony 4.4) - there's just one step missing:

  1. Add this to your codeception.yml to load the new tests/_bootstrap.php you just created - see https://codeception.com/docs/reference/Configuration#Global-Configuration:
    bootstrap: bootstrap.php
Naktibalda commented 4 years ago

I'm using Symfony 5 in my current project and I had this issue too.

Adding env.test file to params section worked for me, but it would definitely be better if Symfony module did it automatically.

Including config/bootstrap in _initialize method works too, but it is a breaking change, so it should be released as 2.0.0 of Symfony module.

An alternative approach is to add .env.test to codeception.yaml in recipe: https://github.com/symfony/recipes-contrib/blob/master/codeception/codeception/2.3/codeception.yaml The only reason .env is currently loaded automatically is that it is set by that recipe file.

Currently I prefer modifying recipe file.

Naktibalda commented 4 years ago

So I wanted to commit my code and realized that env.test must be committed to git, but my secrets are there.

Comment at the top of .env says that

# In all environments, the following files are loaded if they exist,
# the latter taking precedence over the former:
#
#  * .env                contains default values for the environment variables needed by the app
#  * .env.local          uncommitted file with local overrides
#  * .env.$APP_ENV       committed environment-specific defaults
#  * .env.$APP_ENV.local uncommitted environment-specific overrides

Comment above Dotenv::loadEnv method says that

  • .env.local is always ignored in test env because tests should produce the same results for everyone.

Secrets must go to .env.test.local file, right? Then .env.test.local must be enabled in params section and if the file doesn't exist codeception run fails with error that Params file .../.env.test.local not found.

Updating recipe looks like bad option now and it would be better to include bootstrap.php

ThomasLandauer commented 4 years ago

Maybe a stupid question, but which secrets do you have in test environment?

dant89 commented 4 years ago

Same issue here, isn't possible to pass in testing credentials without hacking the config.

c33s commented 4 years ago

why hack the config? can you explain the problem? do you have "real" secrets in your tests? i have dummy secrets for my tests and the are in .env.test and commited inside the repo to ensure tests are running everywhere. so what "secrets" are you trying to store? @ThomasLandauer & @dant89

dant89 commented 4 years ago

@c33s Yes we do have "real" secrets because we test production APIs and third party API / bundle integrations periodically, I don't think that's the point though... .env.test.local should be read over .env.test. Turning this into a discussion about why it's okay to put "real" secrets into git repositories isn't helpful or the right solution.

callmebob2016 commented 4 years ago

I tried the approach suggested by @c33s but it doesn't seem to work for me. Controller always uses .env instead of switching to the .env.test. I am using Symfony5.

I removed all params: from codeception.yml and added bootstrap: bootstrap.php. I also added $_ENV['APP_ENV'] = 'test'; to tests/bootstrap.php. var_dump still shows values from .env.

Any idea what I may be missing ?

c33s commented 4 years ago

@dant89 ok, i get it, you have real secrets in your file, so .env.test.local (which is not in the repo) is the right location for it. what is the problem/task you want to archive? if you put the secrets in the .env.test.local file or maybe even in your real environment, they are "safe" there. i use gitlab, which has its own CI, where i have a gitlab-ci.yml where i can define non-secret env variables for the docker container and in the gitlab gui i can define secret environment variables that are additionally set for the docker container where the tests run. if you have server, which is doing the production "tests" (calling external apis to check them periodically has nothing to do with testing for me, this is monitoring). than you can even deploy a .env.test.local directly on the server.

my question would be, what are you trying to archive?

c33s commented 4 years ago

@callmebob2016 do you have a minimal example for me? for me it sounds really stange, if you define $_ENV['APP_ENV']=test that your app is started in dev/prod env. i think it's a dotenv or php config problem,

symfony initiates the dotenv class in bootstrap.php:

---snip---
if (is_array($env = @include dirname(__DIR__).'/.env.local.php') && (!isset($env['APP_ENV']) || ($_SERVER['APP_ENV'] ?? $_ENV['APP_ENV'] ?? $env['APP_ENV']) === $env['APP_ENV'])) {
    foreach ($env as $k => $v) {
        $_ENV[$k] = $_ENV[$k] ?? (isset($_SERVER[$k]) && 0 !== strpos($k, 'HTTP_') ? $_SERVER[$k] : $v);
    }
} elseif (!class_exists(Dotenv::class)) {
    throw new RuntimeException('Please run "composer require symfony/dotenv" to load the ".env" files configuring the application.');
} else {
    // load all the .env files
    (new Dotenv(false))->loadEnv(dirname(__DIR__).'/.env');
}
---snip---

first check if the dotenv is instanced with false or true the default parameter was changed from true to false as far as i can remember. use your debugger or just use dump to see if dotenv ist even loaded. check the content of APP_ENV.

or it can have something to do with the phpconfig variables_order = EGPCS see https://www.php.net/manual/en/ini.core.php can be that your env variables are not set.

general note: stuff defined in .env is always loaded if not overwritten from .env.test or .env.test.local (in this order)

how the dotenv component has changed and how it's working: https://symfony.com/blog/new-in-symfony-4-2-define-env-vars-per-environment https://symfony.com/doc/current/configuration/dot-env-changes.html

c33s commented 4 years ago

btw i am online in slack (symfony & codeception)

callmebob2016 commented 4 years ago

@c33s thanks for quick reply.

Please have a look at this example: https://github.com/callmebob2016/env_swap_test

This is basically symfony5 + codeception + phpbrowser. Everything should be as per your original description. However, controller always returns dev, also when run from Codeception.

c33s commented 4 years ago

@callmebob2016 you are using acceptance tests and not functional tests, also you are using phpbrowser and not the symfony module. so your setup behave differently.

see my PR at your project, i added the symfony module and i am not using phpbrowser. in the config you see, that the symfony module also has the hardcoded value in which environment the kernel should be booted. then the other environment variables should be correctly loaded from your .env.test.local file

the APP_ENV is a special variable, which handles all choices afterwards. you should either set them hardcoded in the symfony module or via environment variables in your webserver. if you use a real browser to access the page, the symfony app will boot into the normal environment setup in your project. you can add a index-test.php where you also force an environment if you don't want to use the symfony module.

edit: for future reference: you should enable the symfony module for use with your tests in tests/acceptance.suite.yml and/or tests/functional.suite.yml and there define the environment:

modules:
    enabled:
        - Symfony:
            url: 'http://localhost/'
            app_path: 'src' #  specify custom path to your app dir, where the kernel interface is located.
            var_path: 'var' # specify custom path to your var dir, where bootstrap cache is located.
            environment: 'test' # environment used for load kernel
c33s commented 4 years ago

@Naktibalda

Adding env.test file to params section worked for me, but it would definitely be better if Symfony module did it automatically.

:-1:

Including config/bootstrap in _initialize method works too, but it is a breaking change, so it should be released as 2.0.0 of Symfony module.

:+1:

An alternative approach is to add .env.test to codeception.yaml in recipe: https://github.com/symfony/recipes-contrib/blob/master/codeception/codeception/2.3/codeception.yaml The only reason .env is currently loaded automatically is that it is set by that recipe file. Currently I prefer modifying recipe file.

:-1:

Secrets must go to .env.test.local file, right?

yes. but secrets in the tests? if you have secrets they should got to real environment variables but as i wrote above, if you use codeception to monitor your production application, it's not testing, its monitoring and then we should write a real prod symfony app (lets call it monitoring), which is doing the calls to the other symfony app (lets call it app-app). not sure if codeception is the right way for it. if codeception is just the tool to easily access the prod pages, i am sure the code of codeception can directly be used in symfony but then it should be called in prod environment.

Then .env.test.local must be enabled in params section and if the file doesn't exist codeception run fails with error that Params file .../.env.test.local not found.

no, the app environment must be correctly set and the bootstrap.php of symfony will do the rest.

Updating recipe looks like bad option now and it would be better to include bootstrap.php

:+1:

callmebob2016 commented 4 years ago

@c33s

Brilliant, thanks a lot.

One more question: why do you see using acceptance test as incorrect here? I thought (perhaps I am wrong here), that under the hood they are the same code. The difference apart from the conceptual is modules are in their config files. Also I can see in your PR, that both work now. What am I missing ?

c33s commented 4 years ago

@callmebob2016 i absolutly don't see it as incorrect, it's just something i noticed. which test type you use for what is your decision or the "decision" of best practices you are following.

i personally just connect acceptance tests with real remote controlled browser or phantom (which is/may be totally wrong).

https://stackoverflow.com/questions/3370334/difference-between-acceptance-test-and-functional-test

you miss nothing, both work just fine now :)

dompie commented 4 years ago

why hack the config? can you explain the problem? do you have "real" secrets in your tests? i have dummy secrets for my tests and the are in .env.test and commited inside the repo to ensure tests are running everywhere. so what "secrets" are you trying to store? @ThomasLandauer & @dant89

@c33s fyi: .env().local for symfony are not only for storing secrets. They are also meant to store settings that are dependent where the tests are run. E.g. paths go obviously also very often into the .env().local config file. Every developer in a team will have e.g. another path to a local folder where certificates/thumbnails/cache-things are stored for dev/test environment when using symfony.

vworldat commented 3 years ago

Symfony dropped the config/bootstrap.php file with either 5.0 or 5.1, since most of the logic was moved to Dotenv. This is the codeception setup that now works for me when using Sf 5.1:

tests/_bootstrap.php:

<?php

$_ENV['APP_ENV'] = 'test';
(new Symfony\Component\Dotenv\Dotenv())->bootEnv(dirname(__DIR__).'/.env');

And I'm with the others here: I prefer to have the tests handle .env the same way Symfony does. This includes using the pre-compiled php environment files as well if necessary. Dotenv now does all that and it just works.

ThomasLandauer commented 3 years ago

@TavoNiievez Shouldn't this be moved to https://github.com/Codeception/module-symfony/issues too?

TavoNiievez commented 3 years ago

@ThomasLandauer yeap. Done. Is #88 the same issue?

c33s commented 3 years ago

@TavoNiievez via slack:

can you please check the current status of issue #99?

to sum things up:

codeception should never automatically load any .env files via params (if a user want to load it, they should load it via manual config). the reason behind this is that symfony is loading the dotenv files in a specific way which also differs between symfony versions. to have a correct behavior it should be always loaded like symfony is loading it. https://symfony.com/blog/improvements-to-the-handling-of-env-files-for-all-symfony-versions https://symfony.com/doc/current/configuration/dot-env-changes.html#updating-my-application

for older symfony versions this means we have to include the bootstrap.php file where the dotenv loading is done like it's done in https://github.com/Codeception/module-symfony/pull/4

to give a little insignt this is one variant of bootstrap.php

if (is_array($env = @include dirname(__DIR__).'/.env.local.php') && (!isset($env['APP_ENV']) || ($_SERVER['APP_ENV'] ?? $_ENV['APP_ENV'] ?? $env['APP_ENV']) === $env['APP_ENV'])) {
    foreach ($env as $k => $v) {
        $_ENV[$k] = $_ENV[$k] ?? (isset($_SERVER[$k]) && 0 !== strpos($k, 'HTTP_') ? $_SERVER[$k] : $v);
    }
} elseif (!class_exists(Dotenv::class)) {
    throw new RuntimeException('Please run "composer require symfony/dotenv" to load the ".env" files configuring the application.');
} else {
    // load all the .env files
    (new Dotenv(false))->loadEnv(dirname(__DIR__).'/.env');
}

but this code also differs between different symfony versions. in older versions the dotenv component was initialized without a parameter, then the parameters was added. in the past the dotenv code was inside of the console and index.php then it moved into the dotenv component.

so the code in https://github.com/Codeception/module-symfony/pull/4 should be adapted to handle this.

gravitiq-cm commented 1 year ago

Is there any update on this? Just started trying to use codeception and got stung by this unexpected dotenv behaviour - wasted a few hours trying to figure it out, which almost put me off using codeception altogether...

Naktibalda commented 1 year ago

No updates. I haven't used this module since I posted my comment 3 years ago.

TavoNiievez commented 1 year ago

No updates from my side either.

TavoNiievez commented 7 months ago

For anyone interested, especially @ThomasLandauer and @c33s I opened a new PR for this: #190