sebastianbergmann / phpunit

The PHP Unit Testing framework.
https://phpunit.de/
BSD 3-Clause "New" or "Revised" License
19.71k stars 2.2k forks source link

withConsecutive fails with objects #3266

Closed chubidu closed 6 years ago

chubidu commented 6 years ago
Q A
PHPUnit version 7.3.2
PHP version 7.2.8
Installation Method Composer

Verifying the arguments passed to a method call with withConsecutive fails. The mocked method is being called within a loop. The first iteration passes and the error occurs on the second iteration of the loop. The error I'm getting is telling me that it failed on the first iteration since it's saying Parameter 0 for invocation #0 ...:

There was 1 failure:

1) App\Tests\Unit\Example\MyMessageHandlerTest::testInvoke
Expectation failed for method name is equal to "all" when invoked 4 time(s)
Parameter 0 for invocation #0 App\Example\Repository::all(Symfony\Component\HttpFoundation\ParameterBag Object (...)): array does not match expected value.
Failed asserting that two objects are equal.
--- Expected
+++ Actual
@@ @@
 Symfony\Component\HttpFoundation\ParameterBag Object (
     'parameters' => Array (
         'limit' => 10
-        'offset' => 0
+        'offset' => 10
     )
 )

Now, if I'm going to change the expected values for the second iteration (in withConsecutive) to something invalid, the Testrunner is telling me the truth and giving me the expected output, that Parameter 0 for invocation #1 ... failed. As soon as the expected values are exactly like the actual ones (which you can verify dumping the params), the test fails with the error message mentioned above (Parameter 0 for invocation #0 ...).

Maybe I'm doing something wrong? Thanks for your time/help in advance.

Example to reproduce

This class is getting mocked in the test. It's accepting an instance of ParameterBag. This object will be verified in the test which fails.

<?php
namespace App\Example;

use Symfony\Component\HttpFoundation\ParameterBag;

class Repository
{
    public function all(ParameterBag $parameterBag): array
    {
        return [];
    }
}
<?php
namespace App\Example;

class MyMessage
{
    public function getPayload(): array
    {
        return ['asdf' => 'lol'];
    }
}

This is the class which is getting tested.

<?php
namespace App\Example;

use Symfony\Component\HttpFoundation\ParameterBag;

class MyMessageHandler
{
    private $repository;
    private $parameterBag;
    private $chunkSize = 10;

    public function __construct(Repository $repository)
    {
        $this->repository = $repository;
        $this->parameterBag = new ParameterBag();
    }

    public function __invoke(MyMessage $message)
    {
        $payload = $message->getPayload();

        $result = [];
        foreach ($payload as $key => $value) {
            $iteration = 0;

            do {
                // dumping the content of $this-getParameterBag([...]) at this point returns the expected values
                $tmp = $this->repository->all(
                    $this->getParameterBag([
                        'limit'  => $this->chunkSize,
                        'offset' => $this->chunkSize * $iteration,
                    ])
                );

                if (\count($tmp) > 0) {
                    $result[$key][] = $tmp;
                }

                $iteration++;
            } while (\count($tmp) > 0);
        }

        return $result;
    }

    protected function getParameterBag(array $params): ParameterBag
    {
        $this->parameterBag->replace($params);
        return $this->parameterBag;
    }
}

This is the test

<?php
namespace App\Tests\Unit\Example;

use App\Example\MyMessage;
use App\Example\MyMessageHandler;
use App\Example\Repository;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\ParameterBag;

class MyMessageHandlerTest extends TestCase
{
    private $messageHandler;
    private $message;

    public function testInvoke()
    {
        $invoke = $this->messageHandler;
        $invoke($this->message);
    }

    protected function setUp(): void
    {
        $repository = $this->createMock(Repository::class);
        $repository->expects(self::exactly(4))->method('all')
            ->withConsecutive(
                [
                    self::equalTo(
                        new ParameterBag([
                            'limit'  => 10,
                            'offset' => 0,
                        ])
                    )
                ],
                [
                    self::equalTo(
                        new ParameterBag([
                            'limit'  => 10,
                            'offset' => 10,
                        ])
                    )
                ],
                [
                    self::equalTo(
                        new ParameterBag([
                            'limit'  => 10,
                            'offset' => 0,
                        ])
                    )
                ],
                [
                    self::equalTo(
                        new ParameterBag([
                            'limit'  => 10,
                            'offset' => 10,
                        ])
                    )
                ]
            )
            ->willReturnOnConsecutiveCalls(
                [
                    'result' => 'of-first-outer-first-inner-iteration',
                ],
                [], // nothing here to break the inner do-while
                [
                    'result' => 'of-second-outer-first-inner-iteration',
                ],
                [] // nothing here to break the inner do-while
            );

        $this->message = $this->createMock(MyMessage::class);
        $this->message->expects(self::once())->method('getPayload')
            ->willReturn(
                [
                    11 => 'first-outer-iteration',
                    22 => 'second-outer-iteration',
                ]
            );

        $this->messageHandler = new MyMessageHandler($repository);
    }
}

Last but not least the output of composer info | sort

$ composer info | sort
allure-framework/allure-php-api     1.1.4                 PHP API for Allure adapter
allure-framework/allure-phpunit     1.2.3                 A PHPUnit adapter for Allure report.
blackfire/php-sdk                   v1.17.1               Blackfire.io PHP SDK
composer/ca-bundle                  1.1.2                 Lets you find a path to the system CA bundle, and includes a fallback to the Mozilla CA bundle.
composer/xdebug-handler             1.2.1                 Restarts a process without xdebug.
doctrine/annotations                v1.6.0                Docblock Annotations Parser
doctrine/cache                      v1.8.0                Caching library offering an object-oriented API for many cache backends
doctrine/collections                v1.5.0                Collections Abstraction library
doctrine/common                     v2.9.0                Common Library for Doctrine projects
doctrine/dbal                       v2.8.0                Database Abstraction Layer
doctrine/doctrine-bundle            1.9.1                 Symfony DoctrineBundle
doctrine/doctrine-cache-bundle      1.3.3                 Symfony Bundle for Doctrine Cache
doctrine/doctrine-migrations-bundle v1.3.1                Symfony DoctrineMigrationsBundle
doctrine/event-manager              v1.0.0                Doctrine Event Manager component
doctrine/inflector                  v1.3.0                Common String Manipulations with regard to casing and singular/plural rules.
doctrine/instantiator               1.1.0                 A small, lightweight utility to instantiate objects in PHP without invoking their constructors
doctrine/lexer                      v1.0.1                Base library for a lexer that can be used in Top-Down, Recursive Descent Parsers.
doctrine/migrations                 v1.8.1                Database Schema migrations using Doctrine DBAL
doctrine/orm                        v2.6.2                Object-Relational-Mapper for PHP
doctrine/persistence                v1.0.1                Doctrine Persistence abstractions.
doctrine/reflection                 v1.0.0                Doctrine Reflection component
egulias/email-validator             2.1.5                 A library for validating emails against several RFCs
exsyst/swagger                      v0.4.1                A php library to manipulate Swagger specifications
fig/http-message-util               1.1.2                 Utility classes and constants for use with PSR-7 (psr/http-message)
firebase/php-jwt                    v5.0.0                A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.
google/auth                         v1.3.3                Google Auth Library for PHP
google/cloud-core                   v1.23.1               Google Cloud PHP shared dependency, providing functionality useful to all components.
google/cloud-firestore              v0.13.0               Cloud Firestore client for PHP
google/gax                          0.36.0                Google API Core for PHP
google/protobuf                     v3.6.1                proto library for PHP
gregwar/captcha-bundle              v2.0.5                Captcha bundle
gregwar/captcha                     v1.1.7                Captcha generator
grpc/grpc                           1.13.0                gRPC library for PHP
guzzlehttp/guzzle                   6.3.3                 Guzzle is a PHP HTTP client library
guzzlehttp/promises                 v1.3.1                Guzzle promises library
guzzlehttp/psr7                     1.4.2                 PSR-7 message implementation that also provides common utility methods
jdorn/sql-formatter                 v1.2.17               a PHP SQL highlighting library
jean85/pretty-package-versions      1.2                   A wrapper for ocramius/package-versions to get pretty versions strings
jms/metadata                        1.6.0                 Class/method/property metadata management in PHP
jms/parser-lib                      1.0.0                 A library for easily creating recursive-descent parsers.
jms/serializer                      1.13.0                Library for (de-)serializing data of any complexity; supports XML, JSON, and YAML.
jms/serializer-bundle               2.4.2                 Allows you to easily serialize, and deserialize data of any complexity
johnkary/phpunit-speedtrap          v3.0.0                Find slow tests in your PHPUnit test suite
kreait/firebase-php                 3.9.3                 Firebase Admin SDK
kreait/firebase-tokens              1.7.1                 A library to work with Firebase tokens
lcobucci/jwt                        3.2.4                 A simple library to work with JSON Web Token and JSON Web Signature
lexik/jwt-authentication-bundle     v2.5.4                This bundle provides JWT authentication for your Symfony REST API
mikey179/vfsStream                  v1.6.5                Virtual file system to mock the real file system in unit tests.
monolog/monolog                     1.23.0                Sends your logs to files, sockets, inboxes, databases and various web services
moontoast/math                      1.1.2                 A mathematics library, providing functionality for large numbers
mtdowling/jmespath.php              2.4.0                 Declaratively specify how to extract elements from a JSON document
myclabs/deep-copy                   1.8.1                 Create deep copies (clones) of your objects
namshi/jose                         7.2.3                 JSON Object Signing and Encryption library for PHP.
nelmio/api-doc-bundle               v3.2.1                Generates documentation for your REST API from annotations
nette/bootstrap                     v2.4.6                🅱 Nette Bootstrap: the simple way to configure and bootstrap your Nette application.
nette/di                            v2.4.13               💎 Nette Dependency Injection Container: Flexible, compiled and full-featured DIC with perfectly usable auto...
nette/finder                        v2.4.2                🔍 Nette Finder: find files and directories with an intuitive API.
nette/neon                          v2.4.3                🍸 Nette NEON: encodes and decodes NEON file format.
nette/php-generator                 v3.0.5                🐘 Nette PHP Generator: generates neat PHP code for you. Supports new PHP 7.2 features.
nette/robot-loader                  v3.1.0                🍀 Nette RobotLoader: high performance and comfortable autoloader that will search and autoload classes with...
nette/utils                         v2.5.2                🛠 Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/de...
nikic/php-parser                    v4.0.3                A PHP parser written in PHP
ocramius/package-versions           1.3.0                 Composer plugin that provides efficient querying for installed package versions (no runtime IO)
ocramius/proxy-manager              2.2.1                 A library providing utilities to generate, instantiate and generally operate with Object Proxies
paragonie/random_compat             v9.99.99              PHP 5.x polyfill for random_bytes() and random_int() from PHP 7
phar-io/manifest                    1.0.3                 Component for reading phar.io manifest information from a PHP Archive (PHAR)
phar-io/version                     2.0.1                 Library for handling version information and constraints
php-amqplib/php-amqplib             v2.7.2                Formerly videlalvaro/php-amqplib.  This library is a pure PHP implementation of the AMQP protocol. It's been t...
php-amqplib/rabbitmq-bundle         v1.14.4               Integrates php-amqplib with Symfony & RabbitMq. Formerly oldsound/rabbitmq-bundle.
phpcollection/phpcollection         0.5.0                 General-Purpose Collection Library for PHP
phpdocumentor/reflection-common     1.0.1                 Common reflection classes used by phpdocumentor to reflect the code structure
phpdocumentor/reflection-docblock   4.3.0                 With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve informa...
phpdocumentor/type-resolver         0.4.0
phpoption/phpoption                 1.5.0                 Option Type for PHP
phpspec/prophecy                    1.8.0                 Highly opinionated mocking framework for PHP 5.3+
phpstan/phpdoc-parser               0.3                   PHPDoc parser with support for nullable, intersection and generic types
phpstan/phpstan                     0.10.3                PHPStan - PHP Static Analysis Tool
phpstan/phpstan-strict-rules        0.10.1                Extra strict and opinionated rules for PHPStan
phpunit/php-code-coverage           6.0.7                 Library that provides collection, processing, and rendering functionality for PHP code coverage information.
phpunit/php-file-iterator           2.0.1                 FilterIterator implementation that filters files based on a list of suffixes.
phpunit/php-text-template           1.2.1                 Simple template engine.
phpunit/php-timer                   2.0.0                 Utility class for timing
phpunit/php-token-stream            3.0.0                 Wrapper around PHP's tokenizer extension.
phpunit/phpunit                     7.3.2                 The PHP Unit Testing framework.
predis/predis                       v1.1.1                Flexible and feature-complete Redis client for PHP and HHVM
psr/cache                           1.0.1                 Common interface for caching libraries
psr/container                       1.0.0                 Common Container Interface (PHP FIG PSR-11)
psr/http-message                    1.0.1                 Common interface for HTTP messages
psr/log                             1.0.2                 Common interface for logging libraries
psr/simple-cache                    1.0.1                 Common interfaces for simple caching
ramsey/uuid                         3.8.0                 Formerly rhumsaa/uuid. A PHP 5.4+ library for generating RFC 4122 version 1, 3, 4, and 5 universally unique id...
rize/uri-template                   0.3.2                 PHP URI Template (RFC 6570) supports both expansion & extraction
sebastian/code-unit-reverse-lookup  1.0.1                 Looks up which function or method a line of code belongs to
sebastian/comparator                3.0.2                 Provides the functionality to compare PHP values for equality
sebastian/diff                      3.0.1                 Diff implementation
sebastian/environment               3.1.0                 Provides functionality to handle HHVM/PHP environments
sebastian/exporter                  3.1.0                 Provides the functionality to export PHP variables for visualization
sebastian/global-state              2.0.0                 Snapshotting of global state
sebastian/object-enumerator         3.0.3                 Traverses array structures and object graphs to enumerate all referenced objects
sebastian/object-reflector          1.1.1                 Allows reflection of object attributes, including inherited and non-public ones
sebastian/recursion-context         3.0.0                 Provides functionality to recursively process PHP variables
sebastian/resource-operations       1.0.0                 Provides a list of PHP built-in functions that operate on resources
sebastian/version                   2.0.1                 Library that helps with managing the version number of Git-hosted PHP projects
sensio/framework-extra-bundle       v5.2.0                This bundle provides a way to configure your controllers with annotations
snc/redis-bundle                    2.1.6                 A Redis bundle for Symfony
swiftmailer/swiftmailer             v6.1.2                Swiftmailer, free feature-rich PHP mailer
symfony/asset                       v4.1.4                Symfony Asset Component
symfony/browser-kit                 v4.1.4                Symfony BrowserKit Component
symfony/cache                       v4.1.4                Symfony Cache component with PSR-6, PSR-16, and tags
symfony/config                      v4.1.4                Symfony Config Component
symfony/console                     v4.1.4                Symfony Console Component
symfony/debug                       v4.1.4                Symfony Debug Component
symfony/dependency-injection        v4.1.4                Symfony DependencyInjection Component
symfony/doctrine-bridge             v4.1.4                Symfony Doctrine Bridge
symfony/dom-crawler                 v4.1.4                Symfony DomCrawler Component
symfony/dotenv                      v4.1.4                Registers environment variables from a .env file
symfony/event-dispatcher            v4.1.4                Symfony EventDispatcher Component
symfony/expression-language         v4.1.4                Symfony ExpressionLanguage Component
symfony/filesystem                  v4.1.4                Symfony Filesystem Component
symfony/finder                      v4.1.4                Symfony Finder Component
symfony/flex                        v1.1.0                Composer plugin for Symfony
symfony/form                        v4.1.4                Symfony Form Component
symfony/framework-bundle            v4.1.4                Symfony FrameworkBundle
symfony/http-foundation             v4.1.4                Symfony HttpFoundation Component
symfony/http-kernel                 v4.1.4                Symfony HttpKernel Component
symfony/inflector                   v4.1.4                Symfony Inflector Component
symfony/intl                        v4.1.4                A PHP replacement layer for the C intl extension that includes additional data from the ICU library.
symfony/lts                         dev-master            Enforces Long Term Supported versions of Symfony components
symfony/maker-bundle                v1.7.0                Symfony Maker helps you create empty commands, controllers, form classes, tests and more so you can forget abo...
symfony/messenger                   v4.1.4                Symfony Messenger Component
symfony/monolog-bridge              v4.1.4                Symfony Monolog Bridge
symfony/monolog-bundle              v3.3.0                Symfony MonologBundle
symfony/options-resolver            v4.1.4                Symfony OptionsResolver Component
symfony/orm-pack                    v1.0.5                A pack for the Doctrine ORM
symfony/phpunit-bridge              v4.1.4                Symfony PHPUnit Bridge
symfony/polyfill-ctype              v1.9.0                Symfony polyfill for ctype functions
symfony/polyfill-intl-icu           v1.9.0                Symfony polyfill for intl's ICU-related data and classes
symfony/polyfill-mbstring           v1.9.0                Symfony polyfill for the Mbstring extension
symfony/polyfill-php56              v1.9.0                Symfony polyfill backporting some PHP 5.6+ features to lower PHP versions
symfony/polyfill-php72              v1.9.0                Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions
symfony/polyfill-util               v1.9.0                Symfony utilities for portability of PHP codes
symfony/profiler-pack               v1.0.3                A pack for the Symfony web profiler
symfony/property-access             v4.1.4                Symfony PropertyAccess Component
symfony/property-info               v4.1.4                Symfony Property Info Component
symfony/routing                     v4.1.4                Symfony Routing Component
symfony/security-bundle             v4.1.4                Symfony SecurityBundle
symfony/security                    v4.1.4                Symfony Security Component
symfony/serializer                  v4.1.4                Symfony Serializer Component
symfony/stopwatch                   v4.1.4                Symfony Stopwatch Component
symfony/swiftmailer-bundle          v3.2.3                Symfony SwiftmailerBundle
symfony/translation                 v4.1.4                Symfony Translation Component
symfony/twig-bridge                 v4.1.4                Symfony Twig Bridge
symfony/twig-bundle                 v4.1.4                Symfony TwigBundle
symfony/validator                   v4.1.4                Symfony Validator Component
symfony/var-dumper                  v4.1.4                Symfony mechanism for exploring and dumping PHP variables
symfony/web-profiler-bundle         v4.1.4                Symfony WebProfilerBundle
symfony/yaml                        v4.1.4                Symfony Yaml Component
theseer/tokenizer                   1.1.0                 A small library for converting tokenized PHP source code into XML and potentially other formats
twig/twig                           v2.5.0                Twig, the flexible, fast, and secure template language for PHP
webmozart/assert                    1.3.0                 Assertions to validate method input/output with nice error messages.
zendframework/zend-code             3.3.1                 provides facilities to generate arbitrary code using an object oriented interface
zendframework/zend-eventmanager     3.2.1                 Trigger and listen to events within a PHP application
zircote/swagger-php                 2.0.13                Swagger-PHP - Generate interactive documentation for your RESTful API using phpdoc annotations
sebastianbergmann commented 6 years ago

Thank you for your report.

Please provide a minimal, self-contained, reproducing test case that shows the problem you are reporting.

Without such a minimal, self-contained, reproducing test case I will not be able to investigate this issue.

chubidu commented 6 years ago

Hi Sebastian,

so I've created a repository containing the example to reproduce the error I'm getting.

https://github.com/chubidu/phpunit-withConsecutive-failure

You just need to clone it, run composer install and run the tests. I've also added a README.md file which contains the error output and the steps to reproduce. Please let me know if anything is missing.

Thanks in advance.

sebastianbergmann commented 6 years ago

Sorry, but that is not minimal.

chubidu commented 6 years ago

Then please tell me your definition of minimal.

This would be just 3 commands on cli. All relevant code is just located in ./src/Examples/ and ./tests/.../Example/.

chubidu commented 6 years ago

So, I removed everything from the repository which might be unnecessary.

vsouz4 commented 6 years ago

@chubidu I'm not sure this is a problem from phpunit. Check this out:

$tmp = $this->repository->all(
    new ParameterBag([
        'limit'  => $this->chunkSize,
        'offset' => $this->chunkSize * $iteration,
    ])
);

this would work (every time you're creating a new instance/reference), but when you're doing this:

$tmp = $this->repository->all(
    $this->getParameterBag([
        'limit'  => $this->chunkSize,
        'offset' => $this->chunkSize * $iteration,
    ])
);

you're using the same instance/reference of ParameterBag inside the do/while loop, meaning that if inside this all method you would store it for some reason (and not use it immediately), in the second, third etc. iteration of your do/while loop you'll be changing the parameter bag values (and thus the same reference as well).

That's what's happening with the phpunit mock code, it stores the object you're sending to all method, but that reference is being modified inside the do/while loop (the last iteration set parameter values to 10/10, that's why the expectation shows "Actual: 10".

You could clone your parameter bag (to have a different references sent to your repository), unless this is really something phpunit should do (clone the references when running the mock objects).

What do you think @sebastianbergmann ?

chubidu commented 6 years ago

@vsouz4 Thanks for your answer. I will check that out and let you know.

chubidu commented 6 years ago

@vsouz4 I adjusted the method getParameterBag to simply return a new instance of ParameterBag every time it gets called. This fixed my issue with withConsecutive failing. Thank you.