Giter VIP home page Giter VIP logo

testdoublebundle's Introduction

TestDoubleBundle

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 the github_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 and http.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();
    }
}

testdoublebundle's People

Contributors

docteurklein avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

testdoublebundle's Issues

Disable using mocked/stubbed services in dev (app_dev.php) environment

Hi,

The dev environment picks up the mock service as well so in my opinion this is a wrong behaviour. Mocked services should apply only to test environment.

e.g. If you access http://myapp.dev/app_dev.php/....., the application will never use real APIs although it is perfectly legal to consume live APIs in app_dev.php.

Regards

Different Double Classes

Hey there,

first of all, thanks for this package! I'm trying to use it in an api-platform project but unfortunately it doesn't work as expected.
I'm trying to use a test double of the Guzzle HTTP client to mock external API calls. For that I annotated "GuzzleHttp\ClientInterface" with "test_double". Then I defined a "given" where the mocking happens. Something like that:

 public function aPostRequestReturns201(): void
{
    // Creates a ObjectProphecy of Psr\Http\Message\ResponseInterface
    $responseProphecy = $this->getResponseProphecy(
        201,
        \file_get_contents(__DIR__ . '/../fixtures/environments/POSTResponse.json')
    );
    /** @var MethodProphecy $send */
    $send = $this->getClientProfecy()->send(
        Argument::that(
            function (RequestInterface $request): bool {
                // Checking, if the client gets the proper request...
            }
        ),
        []
    );
    $send->willReturn($responseProphecy);
}

After using that "given", I defined the "when" and "then" steps:

 When I add "Content-Type" header equal to "application/json"
And I add "Accept" header equal to "application/json"
And I send a "POST" request to "/myresource" with body:
...
Then the response status code should be 201

For unknown reasons the behat test is only getting green, when I put these lines to the top of the feature. The test fails, whenever I define two scenarios like that in the same feature. Moreover, the test fails, whenever I use "I send a :method request to :url" before.
When doing so, the mocked "send()" method returns null instead of the ResponseInterface mock. And then the API client, that uses Guzzle, throws an exception, because it tries to call a method on the response object.

I have no idea what happens here exactely. What I found so far is, that the container returns different objects in the context class and in the API client. In the context class I get an instance of "Double\GuzzleHttp\Client\P15", where in the API client I get an instance of "Double\GuzzleHttp\Client\P18". For that reason mocking seems not to work for me.
When I put the scenario to the top of the feature file I get the same object, an instance of "Double\GuzzleHttp\Client\P1" on both places.

Do you have any idea, what I'm doing wrong?

Thanks in advance!

Best,
Christoph

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.