docteurklein / test-double-bundle
Easily create test doubles using symfony DIC
Installs: 96 035
Dependents: 0
Suggesters: 0
Security: 0
Stars: 41
Watchers: 3
Forks: 13
Open Issues: 4
Type:symfony-bundle
Requires
- php: >=5.4
- phpspec/prophecy: ^1.6
- symfony/dependency-injection: ^2.0|^3.0
- symfony/http-kernel: ^2.0|^3.0
This package is auto-updated.
Last update: 2024-10-29 04:31:07 UTC
README
What ?
A symfony bundle that eases creation of test doubles.
Using DIC tags, you can automatically replace a service with either a stub or a fake.
Why ?
To improve isolation of tests and increase the precision and variation of test fixtures.
Usually, our behat suite is using real data, coming from database fixtures.
This forces us to create gobal, universal, works-for-all fixtures.
A real database also implies to reset the state before each scenario.
This process is slow, and is just a workaround for having broken isolation.
An ideal test suite would run each scenario using only in-memory repositories.
Each scenario should define how the SUS behaves given a specific context.
Having a global implicit context (the database fixtures) makes it really hard to test different cases.
One solution is to replace your repositories with stubs.
Each scenario configures only the stubs required for it to work.
Note: Stubbed data is not resilient across processes, and thus doesn't fit for end-to-end testing like a typical mink+behat suite.
But now that repositories are doubled, how do you know if your real repositories still work?
Well, that's the role of infrastructure tests. Only those run against a real backend,
be it a database for repositories, or a server for an http client.
To access the real services, just use <original-id>.real
.
By doing that, you theoretically have a good coverage, isolation, speed
and you can better catch the origin of a regression.
All this while applying modelling by example.
How ?
install
composer require docteurklein/test-double-bundle --dev
register the bundle
public function registerBundles() { $bundles = [ new \DocteurKlein\TestDoubleBundle, // … ]; return $bundles; }
Note: You might want to add this bundle only in test env.
integrate with behat
This approach integrates very well with the Symfony2Extension.
You can then inject the service and/or the prophecy in your context class.
You can also inject the container and access all the services at once.
Examples
Note: The following examples use JmsDiExtraBundle to simplify code.
Stubs
Stubs are created using prophecy.
Note: if you don't provide any tag attribute, then a stub is created. if no class or interface is given to the
stub
attribute, then a stub for the service class will be created. A stubbed class cannot be final.
- First, define a stub DIC tag for the service
/** * @Service("github_client") * @Tag("test_double", attributes={"stub"="GithubClient"}) */ final class GuzzleClient implements GithubClient { public function addIssue(Issue $issue) { // … } }
- Automatically, the original
github_client
service is replaced with thegithub_client.stub
service.
In order to control this stub, you have to use the github_client.prophecy
service:
$issue = new Issue('test'); $container->get('github_client.prophecy')->addIssue($issue)->willReturn(true);
Fake
Note: fakes are really just DIC aliases.
Imagine you have a service you want to double.
- First, create this service and add a tag with the corresponding fake service:
/** * @Service("github_client") * @Tag("test_double", attributes={"fake"="github_client.fake"}) */ final class GuzzleClient implements GithubClient { public function addIssue(Issue $issue) { // … } }
- Then, create a fake implementation and register it with the fake id:
/** * @Service("github_client.fake") */ final class FakeClient implements GithubClient { public function addIssue(Issue $issue) { // … } }
Behat
Note: We tagged
repo.invoices
andhttp.client
as stub.
class Domain implements Context { public function __construct($container) { $this->container = $container; } /** * @Given a building in "maintenance mode" */ public function aBuildingInMaintenanceMode() { $this->building = new Building('BUILDING1337'); $this->building->putInMaintenanceMode(); } /** * @When its last unpaid invoice is being paid */ public function itsLastUnpaidInvoiceIsBeingPaid() { $this->container ->get('repo.invoices.prophecy') ->findOneByReference('UNPAID04') ->willReturn(Invoice::ownedBy($this->building)) ; $pay = $this->container->get('app.task.invoice.pay'); $pay('UNPAID04'); } /** * @Then it should be removed from maintenance mode */ public function itShouldBeRemovedFromMaintenanceMode() { $this->container ->get('http.client.prophecy') ->removeFromMaintenanceMode('BUILDING1337') ->shouldHaveBeenCalled() ; $this->container->get('stub.prophet')->checkPredictions(); } }