10000 Cannot get service in tests · Issue #36147 · symfony/symfony · GitHub
[go: up one dir, main page]

Skip to content

Cannot get service in tests #36147

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

Closed
enumag opened this issue Mar 20, 2020 · 20 comments
Closed

Cannot get service in tests #36147

enumag opened this issue Mar 20, 2020 · 20 comments

Comments

@enumag
Copy link
Contributor
enumag commented Mar 20, 2020

Symfony version: 4.4.5

I'm starting a new project (using SF 4.4 because it's LTS) and in my tests I'd like to use the new feature that enables getting services easily in tests.

However even when doing everything as described on a completely fresh project without any hacks I'm still getting this error:

Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException: The "MyServiceClass" service or alias has been removed or inlined when the container was compiled. You should either make it public, or stop using the container directly and use dependency injection instead.

The exception stack trace contains vendor/symfony/framework-bundle/Test/TestContainer.php:106 which proves that the "special container" as described in the post is indeed used. But I still can't get the service.

What else do I need to enable to use this feature?

@dmaicher
Copy link
Contributor

See #28528

I'm guessing the service MyServiceClass is not used anywhere in your app (yet)? So its removed when the container is compiled.

@enumag
Copy link
Contributor Author
enumag commented Mar 20, 2020

Yeah, you're right... While I understand the argument that there is no point in testing a service which isn't used anywhere it is quite confusing. The thing is that I added a new service and wanted to cover it by tests before actually using it in the app.

@blowski
Copy link
blowski commented Mar 28, 2020

In which case, why do you need to load the service from the container?

@enumag
Copy link
Contributor Author
enumag commented Mar 28, 2020

@blowski ? I just explained that in my last comment. Wanna have the service tested before I start using it.

@blowski
Copy link
blowski commented Mar 28, 2020

Why do you need to load the service from the container, instead of instantiating it with new MyService?

@enumag
Copy link
Contributor Author
enumag commented Mar 28, 2020

lol because it has non-trivial dependencies obviously

@blowski
Copy link
blowski commented Mar 28, 2020

Without seeing your exact use case, it’s hard to comment specifically.

Let me confirm I’ve understood what you’re saying. It sounds like you’re building a class and want to test it before integrating it with the rest of the project (standard TDD approach). Because you haven’t yet configured anything else to depend on it, it’s not available from the container, even when all services are set to be public. So you’d like it to be available from the container anyway. Have I got that right?

You also mentioned that it’s hard to instantiate the service in the test using standard PHP new MyService because of non-trivial constructor arguments.

Personally, I would instantiate the service using simple PHP`, because then you’re not depending on and having to load the framework.

$service = new Service(new Dependency);

If it’s hard to do that, I would first try refactoring to make it easier.

If the dependencies themselves are hard to instantiate I guess you could load those from the container, and pass them to the service. For example:

$service = new Service($container->get(‘a_dependency’);

I definitely don’t see this as a bug in the service container. If you really want to do this, you can define it yourself in the service yaml file and get rid of the definition when you no longer need it.

@enumag
Copy link
Contributor Author
enumag commented Mar 29, 2020

@blowski

Let me confirm I’ve understood what you’re saying. It sounds like you’re building a class and want to test it before integrating it with the rest of the project (standard TDD approach). Because you haven’t yet configured anything else to depend on it, it’s not available from the container, even when all services are set to be public. So you’d like it to be available from the container anyway. Have I got that right?

Yeah.

You also mentioned that it’s hard to instantiate the service in the test using standard PHP new MyService because of non-trivial constructor arguments.

Personally, I would instantiate the service using simple PHP`, because then you’re not depending on and having to load the framework.

$service = new Service(new Dependency);

If it’s hard to do that, I would first try refactoring to make it easier.

It's not hard to do, but literally impossible. We're getting quite off-topic here since you're questioning my needs but alright, let me explain.

The service in question has a dependency on EntityManager and it is a service responsible for fetching the correct data from the database. I need to test that the SQL queries are returning the intended results.

I think you'll agree that EntityManager is not something I can just create using new EntityManager, right?

If the dependencies themselves are hard to instantiate I guess you could load those from the container, and pass them to the service. For example:

$service = new Service($container- 8000 >get(‘a_dependency’);

Yes, this is something I can do and actually did in my case. I just don't exactly like it because I don't wanna care about the service dependencies in the test. I'd much rather just fetch it from the DIC. Also there are cases where the arguments are not actual services but the services are wrapped by let's say symfony/cache adapters. So while not my case this time, even this construction may sometimes get very non-trivial compared to fetching the service I wanna test.

I definitely don’t see this as a bug in the service container. If you really want to do this, you can define it yourself in the service yaml file and get rid of the definition when you no longer need it.

I don't see it as a bug per-se either but the exception is extremely confusing and it's difficult to find out the "actual" reason why the service was removed. If the exception message at least said that the service was removed because it was never used then it would be somewhat acceptable. The current state is definitely not.

@blowski
Copy link
blowski commented Mar 29, 2020

So when you’re trying to load the service in an integration test, the container is returning a confusing message about the service being missing. A message like “this service was removed from the container because it’s not used anywhere” would make it easier for you to debug why you’re not able to load it in the test. Did I understand correctly?

@enumag
Copy link
Contributor Author
enumag commented Mar 29, 2020

Yes. I'd still prefer for the service to be available but at least knowing why it isn't would be better than the current WTF state.

@Taluu
Copy link
Contributor
Taluu commented Mar 29, 2020

From what I can see, you are trying to do here a unit test... So mock the dependencies and be done with it.

If you have too many arguments, then it means you are breaking the single responsability principle which usually means your service does too much.

@enumag
Copy link
Contributor Author
enumag commented Mar 29, 2020

@Taluu 🤦‍♂

From what I can see, you are trying to do here a unit test... So mock the dependencies and be done with it.

From what I see you didn't bother to read:

The service in question has a dependency on EntityManager and it is a service responsible for fetching the correct data from the database. I need to test that the SQL queries are returning the intended results.

How would you unit test a service responsible for fetching database data? Of course I can't mock anything. That would make the test completely pointless. I need to verify that the SQL queries work as intended and of course I can only do that when testing against a real database.

@Taluu
Copy link
Contributor
Taluu commented Mar 29, 2020

Thanks for your awesome reaction, as it really helps me helping your. Anyways.

What you are looking for is a functional / integration test... You must instantiate your service with a new ... and use the container each services (note : you should probably look on how to inject repositories / query functions instead, and then test these ones).

Otherwise it's "pointless" as you said, as you have no idea how your service is instantiated. 🤷‍♂

@dmaicher
Copy link
Contributor
dmaicher commented Mar 29, 2020

@enumag for now as stated here the only solution is to actually make sure your service is used or mark it public in the meantime.

Keep in mind that, because of how Symfony's service container work, unused services are removed from the container. This means that if you have a private service not used by any other service, Symfony will remove it and you won't be able to get it as explained in this article. The solution is to define the service as public explicitly so Symfony doesn't remove it.

Edit: or define a public alias as explained here: #28528 (comment)

@nicolas-grekas
Copy link
Member

The public alias is the recommended way here.

@blowski
Copy link
blowski commented Mar 30, 2020

Personally speaking, I think integration tests should only be testing public services anyway.

@enumag I'm going totally off-topic from your original bug report, but perhaps it might help solve it in a different way. I think most of us have run into this problem in our own projects, and in my own work, I approach this by using interfaces on my repositories.

// UserRepository.php
interface UserRepository
{
    public function findOneByID($id): ?User;
}

// DoctrineUserRepository.php
class DoctrineUserRepository implements UserRepository
{
    public function findOneById($id): ?User
    {
        // normal Doctrine code
    }
}

class InMemoryUserRepository implements UserRepository
{
    private array $users = [];

    public function findOneById($id): ?User
    {
        return $this->users[$id];
    }
}

class UserService
{
    // signature is on the interface, not the implementation
    public function(UserRepository $userRepository)
    {
        $this->userRepository = $userRepository;
    }

    public function doSomething()
    {
        $user = $this->userRepository->findOneById(7);
    }
}

class UserServiceTest extends TestCase
{
    public function test_it_does_something()
    {
        $service = new UserService(new InMemoryUserRepository);
    }
}

These tests are quicker to run and less flaky. The only place I actually test the queries are running correctly is in full functional tests (i.e. WebTestCase tests) and Doctrine repository tests. Unit tests always use the in memory implementations.

Where I have other complex services as dependencies, I use a decorator so I can inject stub implementations in unit tests, and the real thing everywhere else.

There's a good chance that, for some reason, you've already come across and rejected this pattern but I thought I'd leave it here for anyone else struggling with the problem you mentioned.

@enumag
Copy link
Contributor Author
enumag commented Mar 30, 2020

@blowski You don't understand. The service I'm testing effectively IS a Repository. (It's just named differently and doesn't extend EntityRepository because of personal choice (no reason to use inheritance for repositories imo.)

As for services that are calling this class and actually using the data, I'm testing them differently with in memory data of course.

What I'm trying to test here are the actual SQL (or DQL) queries since they're too complex to leave them without tests.

@blowski
Copy link
blowski commented Mar 30, 2020

You're right, sorry, I didn't realise these were repository services you were trying to test.

I agree with you about not extending from EntityRepository, so my repository tests typically look like this:

$doctrineUserRepository = new DoctrineUserRepository($container->get(EntityManager::class));

@Taluu
Copy link
Contributor
Taluu commented Mar 30, 2020

Mine too. :}

Ok, so I really think you should either use a public alias or even better, instantiate them yourself. Do not test the container instantiation, but your object (you can stlll fetch the entity manager from the container...).

@nicolas-grekas
Copy link
Member

Closing as this has been answered now. Public test aliases to the rescue - or any other options.

javiereguiluz added a commit to symfony/symfony-docs that referenced this issue Apr 6, 2020
…terj)

This PR was merged into the master branch.

Discussion
----------

No longer recommend injecting the service container

Fixes #13469

I think it makes sense to no longer document injecting the service container. Service subscriber is the better alternative in any case.

I actually also thought about removing setting a service public at all. But let's keep it for now (it is sometimes usefull in tests see e.g. symfony/symfony#36147 and #12647 . It's also a bit weird to have a "Public Versus Private Services" section that only discusses "private" services

Commits
-------

156ac13 No longer recommend injecting the service container
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants
0