-
-
Notifications
You must be signed in to change notification settings - Fork 5.2k
Document how to "integration" test private services #8097
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
Nice idea. Could be also named:
|
Not ideal but I avoided any pain by making all my services public thanks a compiler pass for my libraries... |
I added this to the 4.0 milestone, so we will remember to do it. But a PR for this should go into 3.3 I believe. |
... with a note that the config paths will need to change when merged into 4.0. We're going to be trying to remember to do that a lot :) |
I'd suggest to prefix the aliases by |
@nicolas-grekas I know you're not happy with it but I don't use the container as a service locator anywhere except in my tests so having this limitation makes really no sense to me. I've got the choice between updating hundreds of service definitions and registering a compiler pass turning my library/application services into public services. I don't have much time for it, so I need to be pragmatic, the former solution is too much of a burden. Another idea: maybe having a "non secure" container registered as a different service name than $kernel->getTestContainer()->get('foo');
// or
$kernel->getContainer()->get('test_container')->get('foo'); |
@6ecuk don't do that: if you happen to use a service in eg your controllers, your tests will be green, but your prod will fail. The worst combination of events. |
@6ecuk you do as you want of course - but advocating this is giving a footgun to ppl |
@6ecuk your snippet is incomplete as you are missing the aliases as well. But already mentioned I would strongly suggest against it if there is commands or controllers using the container as a service locator. @nicolas-grekas what do you think of my second proposal? I think that would be a more acceptable compromise than the two extremes we currently see (alias everything or make everything public). |
@theofidry a container is a compiled php class. The removed services are actually removed. So there is "hand" to get them. The only way to make that work is to dump another container just for tests. Which is actually what the compiler pass is doing. I don't think there is anything in between. |
@theofidry The only is issue I see with your method is if I test a controller doing this: $this->get('a.service.that.should.be.private'); It will works on your test, but not in production neither and dev env. |
@soullivaneuh I'm aware of that issue which is why I advised against if you are using that. But in my case, I don't. Tests are the only place where I ever do this. |
I tried to solve the problem with a service locator. I'm currently adding the following compiler pass in the test environment only: namespace App\DependencyInjection;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\ServiceLocator;
class TestContainerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
$services = array_keys($container->findTaggedServiceIds('test.container', true));
foreach ($container->getDefinitions() as $id => $definition) {
if ($definition->isPublic() && !$definition->isAbstract()) {
$services[] = $id;
}
}
foreach ($container->getAliases() as $id => $alias) {
if ($alias->isPublic()) {
$services[] = $id;
}
}
$container->register('test.container', ServiceLocator::class)
->setPublic(true)
->addTag('container.service_locator')
->setArguments([
array_combine($services, array_map(
function (string $id): Reference {
return new Reference($id);
},
$services
))
]);
}
} The result is a public service I then use this service locator instead of the application container in my integration tests: // old
$container = self::$kernel->getContainer();
// new
$container = self::$kernel->getContainer()->get('test.container'); The locator allows me to fetch those private services without tinkering with service visibilities or introducing prefixed fake services. As a convenience feature, the locator also provides all public services from the application container, so I don't have to remember which service needs to be pulled from which container. If you don't want this, simply remove the |
I really do not like the solutions that require you to write extra yaml configuration. I believe Symfony has introduced the service autodiscovery to avoid it and now you are telling me I have to put extra three lines in yaml for every service I want to integration test? I really appreciate the idea with a compiler pass though. However, tagging services still requires you to write yaml, so I ended up with something simpler (not necessarily "cleaner"): <?php
namespace YourBundle\Tests;
use YourBundle\Services\AService;
use YourBundle\Services\BService;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
/**
* Makes enumerated private services that needs to be tested public
* so they can be fetched from the container without a deprecation warning.
*
* @see https://github.com/symfony/symfony-docs/issues/8097
* @see https://github.com/symfony/symfony/issues/24543
*/
class TestContainerPass implements CompilerPassInterface {
private static $PUBLIC_IN_TESTS = [
AService::class,
BService::class,
];
public function process(ContainerBuilder $container) {
foreach ($container->getDefinitions() as $id => $definition) {
if (in_array($id, self::$PUBLIC_IN_TESTS, true)) {
$definition->setPublic(true);
}
}
}
} Need to register it in your kernel: class AppKernel extends Kernel {
// ...
protected function build(ContainerBuilder $container) {
parent::build($container);
if ($this->getEnvironment() === 'test') {
$container->addCompilerPass(new TestContainerPass(), PassConfig::TYPE_OPTIMIZE);
}
}
} |
@fracz I think that solution has been discussed in multiple variants in this thread already. You will still create a situation in which services that are private in prod will be public during test execution. That is potentially dangerous. This solution might work for you, but it's not the one that should be advertised in the documentation, imho. |
You are right and I'm aware of the danger here. I still wonder why it is impossible to have tests instantiated by the container like e.g. Spring does with its custom test runner? I mean that tests should declare their dependencies just like production classes do. class PostControllerTest extends ContainerAutoconfiguredTestCaseOrWhatever
{
/** @var MyPrivateService */
private $serviceToTest;
public function __construct(MyPrivateService $service) {
$this->serviceToTest = $service;
}
public function testSomething() {
//...
}
} |
@fracz The main reason why test cases aren't built by the container is probably that (to my knowledge) nobody has implemented that yet. 😉 If you want to start a discussion about that feature, I'd suggest to open an issue in the symfony/symfony issue tracker. |
@derrabus @fracz Running PHPUnit tests within the existing Symfony container is a terrible idea. What you can do is using a dedicated container to simplify DI within PHPUnit like I did in my test tools. In fact, there are already way too many dependencies in a typical Symfony project. Tell me if I'm wrong, but the original idea of DI containers was to simplify loose coupling. Instead it now encourages some developers to create a dependency hell powered by auto-wiring (interfaces enable changing dependencies, but they don't remove them). Imagine forms created by factories and builders do request parsing, validation and database requests via ORM. In the worst case, half of the logic is hidden in annotations you can not possibly cover with tests. When faced with such a situation, you either have to invest a lot of time to understand what exactly is happening behind the scenes and build mocks (remember that excessive mocking is a TDD anti-pattern?) for all of that complexity or you go the easy way and fetch the service with all its many dependencies and a working configuration from the existing container (yes, it will be slow and using the app container is not a great idea). I've seen good developers getting an impostor syndrome from trying to write unit tests for a Symfony application. You know what? I'm starting to understand DHH when he said TDD is dead. I'm at the point where I think I'm giving up and only do acceptance testing when working with full featured Symfony applications. The direction in which development is going is not doing the agile community a favor, the rest might be happy but will probably never understand what's the point about writing tests. |
Try the alternative without DI, good luck with that. Refactoring is a massive pain. The dependency hell you describe is not an issue of the DI pattern but of having too many dependencies.
Of course it can. That "logic" is plain configuration, it makes zero difference wether it's PHP, XML, YAML or annotations.
I think you are missing DDH point. The thing is e2e/acceptance tests are the most effective tests. They however also tend to:
If you are in a position where the slowness is not bad and they are not very brittle, then go for it. It's the most reliable way to ensure things are working. Unit tests are are here to test isolated bricks from every angle which might be painfully hard to do with an e2e tests (and horrible brittle & slow). They also have the big advantage to be a hell lot faster. And FYI, TDD is not about unit tests, it's about testing: make sure that each line you write is tested, unit test or not it doesn't matter. |
This looks like a nice approach: https://github.com/jakzal/phpunit-injector |
Thank you @symm, what you found looks interesting. I'll give it a try. Dear @theofidry, I'm a huge fan of DI and I love Symfony. My daily business for the last 20 years was to refactor slow and untested PHP applications. So let me explain: This ticket is just a symptom of an ongoing process in the community to add more magic, more complexity and force developers to use workarounds if they want to work in a different way. I advocate for building less bloated PHP applications (and frameworks), which was the original selling point against Java, if somebody remembers. First, it makes testing easier and causes less overhead (also developers can possibly understand what's going on). Code that is written to work in a complex environment with lots of dependencies is also hard to test as "isolated bricks", if that refers to mocking. Mocking makes tests easier to maintain because they don't break when they should, so that's generally not an option for me. The type of tests matters and performance is especially important for refactoring. It also matters if you do bottom up or top down development. Second, PHP applications need to be bootstrapped for every single request, so complex software with lots of classes and dependencies is very slow. I'm currently working with a Symfony application that requires 16 seconds to load in dev mode (sure, docker for mac is slow, but similar PHP apps only need 500ms, even in dev mode). I'm totally clueless why some developers in the PHP community think performance is not important but it's good for my business. It's not an issue with Symfony in general, just to be clear. You can also work in a lean way, it just doesn't happen very often in practice. I'm already way off topic... sorry. |
I think you'll have to expand here, because you lost me as for me the recent updates removes the need for workarounds.
By "it" I assume you mean acceptance testing? Then yes I fully agree with what you've just said that's my point. Unit tests are a compromise: they are cheaper but they provide less guarantee. If you can cover everything with acceptance tests which are cheap enough (i.e. not too hard to write, not too brittle & not too slow) then you should always favour acceptance tests IMO.
Get rid of docker :) That may sound like a troll but really docker for mac is just not cut for it. I've seen projects of various size (and different docker optimisation strategies) taking well over 15s where on linux the slowest was 5s. What I personally do is use docker for the DB and non-PHP related things which requires little FS and leave the PHP config to phpbrew and likes which make it relatively easy. Not ideal but at least that works better than docker at least in my experience. The only thing that could make it better here is maybe switch to a React based web server for dev which will be a hell lot faster. |
Let's not put such claims out there without backing them up. Anything can be a right idea in the right context, or a terrible idea in a wrong context.
The aim of a service container is to simplify creating objects since we're making the process more complex by following dependency injection. Not sure what are you trying to accomplish with this comment? With Flex number of dependencies can be really minimal. The rest is up to you.
Symfony doesn't prevent you from writing good unit tests in any way. If anything, it encouraged many developers to improve their testing strategies by popularizing dependency injecting with the service container in the PHP community. It only up to developers to choose the right testing strategy that will work for them. Again, what are you suggesting here?
Do you have anything constructive to add here please? I've been following TDD for years, consider myself an agile programmer, and one of the reasons I choose to use Symfony for my PHP projects is that it doesn't get into my way and lets me to design my part the way I want. Hexagonal architecture? No problem! RAD? Sure! What direction are you talking about?
My perception is we're trying to avoid magic and complexity for the end users. Would be nice if we heard some examples of the magic you're talking about so we could fix that.
No one is negating the value of unit tests or mocking. Of course the type of tests matters. Furthermore, most projects will need a wide range of different kinds of tests in appropraite proportions. This ticket is only about improving experience while writing integration tests. Unit tests are rather stratight forward, as Symfony will be hardly involved in those. Conventionally I'd write unit tests to my code, and then integration test my boundaries with the framework or libraries. Perhaps we could add an article to docs explaining unit testing.
Not sure if you saw the latest benchmarks, but Symfony is rather fast. Especially if you look at what it offers. Did you know that not all services are actually loaded on each request, but only those that are actually needed? PHP 7 allowed us to load a smaller container on each request. We're going a bit offtopic here, but there are ways to "fix" docker even on mac (I prefer to use sth like docker sync). Finally, if you don't like complex software as you describe it, feel free to create smaller software. Symfony doesn't care how big your app is and behaves equally well in all sizes. @lastzero do you have any concrete suggestion to improve your experience with Symfony? |
As I wrote, I think it's getting a bit off topic. I'll still write one last reply and hope that's ok with everyone (I'm happy to continue the conversation via e-mail or you can flame me on Twitter). Here are some examples of being forced to use workarounds:
For me, the best framework is the one that doesn't get in your way. Diversity is a good thing and it is not my intention to offend anyone. There are different needs like when some people speak about high performance they mean < 10 ms response time while others have 100 to 200 ms in their mind. All I want is to highlight some context and look at the big picture when I stumble upon an issue, like in this case. |
Public aliases are the way to go in 3.4. Anyone willing to send a doc PR? |
See https://symfony.com/blog/new-in-symfony-4-1-simpler-service-testing for some intro. |
Out of the options available.. creating a service locator is probably the easiest to maintain.
create a service_test.yml
add the service_test.yml to the top of config_test.yml
The TestServiceContainer is just a service locator subscriber
I then extended WebTestCase with
|
Use |
https://symfony.com/doc/4.4/testing.html#accessing-the-container |
I posted that 6months ago. Of course, it is handled in Symfony 4.4. In 3.x, services were mostly public by default. |
friendly @weaverryan or @wouterj, this one could be close with #8097 (comment) |
What about replacing built in stuff? I know that I can define my custom version of this provider, make it public and then replacing it. But if I need to do it only for tests - it's not really convenient... |
As said in #8097 (comment), this issue should be closed now that 4.4 (the lower maintained branch in the docs) can use the special test container. |
For the record, see #17314. |
Question:
Answer:
Then, you would actually fetch
test_alias.AppBundle\Service\MyService
out of the container in your unit test. It's a nice idea too: you only need to expose your service as public in the test environment.The text was updated successfully, but these errors were encountered: