Skip to content

Commit f2c43ab

Browse files
Merge pull request #646 from maximium/463-mock-services
Add helper method to set service mocks in the container.
2 parents 07018b7 + 10d8b49 commit f2c43ab

File tree

9 files changed

+262
-0
lines changed

9 files changed

+262
-0
lines changed

doc/basic.md

+53
Original file line numberDiff line numberDiff line change
@@ -234,4 +234,57 @@ $this->isSuccessful($client->getResponse());
234234
> Consider hard-coding the URLs in the test: it will ensure that if a route is changed,
235235
> the test will fail, so you'll know that there is a Breaking Change.
236236
237+
#### Mock services
238+
239+
##### setServiceMock()
240+
241+
Mock a service:
242+
243+
```php
244+
// mock a service
245+
$client = static::createClient();
246+
$mock = $this->getServiceMockBuilder(SomeService::class)->getMock();
247+
$mock->expects($this->once())->method('get')->willReturn('mocked service');
248+
$this->setServiceMock(static::$kernel->getContainer(), SomeService::class, $mock);
249+
250+
// the service is mocked
251+
$client->request('GET', '/service');
252+
$this->assertSame('mocked service', $client->getResponse()->getContent());
253+
```
254+
255+
There are some additional conditions to be aware of when mocking services:
256+
257+
- The service you want to mock must be public.
258+
You can set all services to be public in the configuration of the test environment in the `services.yaml` file:
259+
```yaml
260+
when@test:
261+
services:
262+
_defaults:
263+
public: true
264+
autowire: true
265+
autoconfigure: true
266+
```
267+
268+
- The service must be mocked before any dependent services are used.
269+
Otherwise, you must restart the kernel to mock the service.
270+
```php
271+
...
272+
// run some tests with the original service
273+
$client->request('GET', '/service');
274+
$this->assertSame('non mocked output', $client->getResponse()->getContent());
275+
276+
// kernel reboot is required to mock the service if the service is already loaded
277+
// because the service we want to mock is already injected into the dependent services
278+
static::ensureKernelShutdown();
279+
// boot the kernel again
280+
$client = static::createClient();
281+
282+
// mock the service as shown above
283+
...
284+
```
285+
286+
- When mocking a service for command testing,
287+
you must set the `$reuseKernel` argument to `true` in the `runCommand` method call.
288+
See example code [here](./command.md#service-mock).
289+
237290
[Installation](./installation.md)[Command test](./command.md)

doc/command.md

+31
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,35 @@ class MyTestCase extends WebTestCase {
4646
}
4747
```
4848

49+
### Service mock
50+
51+
It is important to set the `$reuseKernel` argument to `true` in the `runCommand` method call
52+
if you want to keep your services mocked in the command.
53+
54+
```php
55+
use Liip\FunctionalTestBundle\Test\WebTestCase;
56+
use App\Service\YourService;
57+
58+
class MyTestCase extends WebTestCase
59+
{
60+
public function myTest()
61+
{
62+
// boot kernel
63+
$kernel = static::bootKernel(['environment' => static::$env]);
64+
$container = $kernel->getContainer();
65+
// create a service mock
66+
$mock = $this->getServiceMockBuilder(YourService::class)
67+
->onlyMethods(['getId'])
68+
->getMock();
69+
$mock->expects($this->once())
70+
->method('getId')
71+
->willReturn(42);
72+
// set the service mock in the container
73+
$this->setServiceMock($container, YourService::class, $mock);
74+
// now you have mocked service App\Service\YourService in your command
75+
$this->runCommand('myCommand', [], true);
76+
}
77+
}
78+
```
79+
4980
← [Basic usage](./basic.md) • [Logged client](./logged.md) →

src/Test/WebTestCase.php

+19
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,25 @@ protected function getServiceMockBuilder(string $id): MockBuilder
8484
return $this->getMockBuilder($class)->disableOriginalConstructor();
8585
}
8686

87+
/**
88+
* Mock service in the container.
89+
*/
90+
protected function setServiceMock(
91+
ContainerInterface $container,
92+
string $serviceId,
93+
object $mock
94+
): void {
95+
$containerRef = new \ReflectionObject($container);
96+
97+
if ($containerRef->hasProperty('services')) {
98+
$servicesProperty = $containerRef->getProperty('services');
99+
$servicesProperty->setAccessible(true);
100+
$services = $servicesProperty->getValue($container);
101+
$services[$serviceId] = $mock;
102+
$servicesProperty->setValue($container, $services);
103+
}
104+
}
105+
87106
/**
88107
* Builds up the environment to run the given command.
89108
*/

tests/App/Controller/DefaultController.php

+6
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
use Doctrine\ORM\EntityManagerInterface;
1717
use Liip\Acme\Tests\App\Entity\User;
18+
use Liip\Acme\Tests\App\Service\Service;
1819
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
1920
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
2021
use Symfony\Component\Form\Extension\Core\Type\TextType;
@@ -112,4 +113,9 @@ public function exceptionAction(): Response
112113
{
113114
throw new \Exception('foo');
114115
}
116+
117+
public function serviceAction(Service $service): Response
118+
{
119+
return new Response($service->get());
120+
}
115121
}
+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of the Liip/FunctionalTestBundle
7+
*
8+
* (c) Lukas Kahwe Smith <[email protected]>
9+
*
10+
* This source file is subject to the MIT license that is bundled
11+
* with this source code in the file LICENSE.
12+
*/
13+
14+
namespace Liip\Acme\Tests\App\Service;
15+
16+
class DependencyService
17+
{
18+
public function get(): string
19+
{
20+
return 'dependency service result';
21+
}
22+
}

tests/App/Service/Service.php

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of the Liip/FunctionalTestBundle
7+
*
8+
* (c) Lukas Kahwe Smith <[email protected]>
9+
*
10+
* This source file is subject to the MIT license that is bundled
11+
* with this source code in the file LICENSE.
12+
*/
13+
14+
namespace Liip\Acme\Tests\App\Service;
15+
16+
class Service
17+
{
18+
private $dependencyService;
19+
20+
public function __construct(DependencyService $dependencyService)
21+
{
22+
$this->dependencyService = $dependencyService;
23+
}
24+
25+
public function get(): string
26+
{
27+
return $this->dependencyService->get();
28+
}
29+
}

tests/App/config.yml

+5
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@ services:
6868
_defaults:
6969
autowire: true
7070
autoconfigure: true
71+
72+
Liip\Acme\Tests\App\Service\:
73+
resource: './Service/'
74+
public: true
75+
7176
Liip\Acme\Tests\App\Command\TestCommand:
7277
tags: ['console.command']
7378
Liip\Acme\Tests\App\Command\TestInteractiveCommand:

tests/App/routing.yml

+5
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,8 @@ liipfunctionaltestbundle_json:
2929
liipfunctionaltestbundle_exception:
3030
path: /exception
3131
defaults: { _controller: 'Liip\Acme\Tests\App\Controller\DefaultController::exceptionAction' }
32+
33+
liipfunctionaltestbundle_service:
34+
path: /service
35+
defaults: { _controller: 'Liip\Acme\Tests\App\Controller\DefaultController::serviceAction' }
36+

tests/Test/WebTestCaseTest.php

+92
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,12 @@
1515

1616
use Doctrine\Common\Annotations\Annotation\IgnoreAnnotation;
1717
use Liip\Acme\Tests\App\AppKernel;
18+
use Liip\Acme\Tests\App\Service\DependencyService;
19+
use Liip\Acme\Tests\App\Service\Service;
1820
use Liip\FunctionalTestBundle\Test\WebTestCase;
1921
use PHPUnit\Framework\AssertionFailedError;
22+
use PHPUnit\Framework\MockObject\MockObject;
23+
use Symfony\Component\HttpFoundation\RequestStack;
2024

2125
/**
2226
* @IgnoreAnnotation("depends")
@@ -398,4 +402,92 @@ public function testJsonIsSuccessful(): void
398402
'application/json'
399403
);
400404
}
405+
406+
public function testSetServiceMockCommand(): void
407+
{
408+
$mockedServiceClass = RequestStack::class;
409+
$mockedServiceName = 'request_stack';
410+
411+
$kernel = static::bootKernel();
412+
$container = $kernel->getContainer();
413+
$mock = $this->getMockBuilder('\stdClass')->getMock();
414+
415+
$this->assertInstanceOf($mockedServiceClass, $container->get($mockedServiceName));
416+
$this->setServiceMock($container, $mockedServiceName, $mock);
417+
$this->assertInstanceOf(MockObject::class, $kernel->getContainer()->get($mockedServiceName));
418+
}
419+
420+
public static function provideSetServiceMockClientData(): array
421+
{
422+
return [
423+
'no mock' => [
424+
'dependency service result',
425+
null,
426+
],
427+
'mock service dependency' => [
428+
'mocked dependency service result',
429+
DependencyService::class,
430+
],
431+
'mock controller dependency' => [
432+
'mocked service result',
433+
Service::class,
434+
],
435+
];
436+
}
437+
438+
/**
439+
* @dataProvider provideSetServiceMockClientData
440+
*/
441+
public function testSetServiceMockClient(string $expectedOutput, ?string $mockedServiceName): void
442+
{
443+
$client = static::createClient();
444+
445+
// mock the service
446+
if ($mockedServiceName) {
447+
$mock = $this->getServiceMockBuilder($mockedServiceName)->getMock();
448+
$mock->expects($this->once())->method('get')->willReturn($expectedOutput);
449+
$this->setServiceMock(static::$kernel->getContainer(), $mockedServiceName, $mock);
450+
}
451+
452+
$client->request('GET', '/service');
453+
$this->assertSame($expectedOutput, $client->getResponse()->getContent());
454+
}
455+
456+
public static function provideSetServiceMockKernelRebootData(): array
457+
{
458+
return [
459+
// do a kernel reboot, expects 'get' method call on mock, expected output
460+
'no kernel reboot' => [false, false, 'dependency service result'],
461+
'reboot kernel' => [true, true, 'mocked result'],
462+
];
463+
}
464+
465+
/**
466+
* @dataProvider provideSetServiceMockKernelRebootData
467+
*/
468+
public function testSetServiceMockKernelReboot(
469+
bool $rebootKernel,
470+
bool $expectedMethodCall,
471+
string $expectedOutput
472+
): void {
473+
// use the real service
474+
$client = static::createClient();
475+
$client->request('GET', '/service');
476+
$this->assertSame('dependency service result', $client->getResponse()->getContent());
477+
478+
if ($rebootKernel) {
479+
static::ensureKernelShutdown();
480+
$client = static::createClient();
481+
}
482+
483+
// mock the service
484+
$mock = $this->getServiceMockBuilder(Service::class)->getMock();
485+
$mock->expects($expectedMethodCall ? $this->once() : $this->never())
486+
->method('get')
487+
->willReturn('mocked result');
488+
$this->setServiceMock(static::$kernel->getContainer(), Service::class, $mock);
489+
490+
$client->request('GET', '/service');
491+
$this->assertSame($expectedOutput, $client->getResponse()->getContent());
492+
}
401493
}

0 commit comments

Comments
 (0)