What are the Slack Archives?

It’s a history of our time together in the Slack Community! There’s a ton of knowledge in here, so feel free to search through the archives for a possible answer to your question.

Because this space is not active, you won’t be able to create a new post or comment here. If you have a question or want to start a discussion about something, head over to our categories and pick one to post in! You can always refer back to a post from Slack Archives if needed; just copy the link to use it as a reference..

Hello everybody, does anybody has experience with faking/mocking external dependencies (like externa

UPWG9AYH2
UPWG9AYH2 Posts: 509 🧑🏻‍🚀 - Cadet

Hello everybody,
does anybody has experience with faking/mocking external dependencies (like external api’s) in spryker? So what could be a clean approach for this?
Best

Comments

  • i think @UKGT7RC7P mentioned php vcr in one of his past talks

  • UPWG9AYH2
    UPWG9AYH2 Posts: 509 🧑🏻‍🚀 - Cadet

    The question is more, how they get integrated in spryker, so where is the switch to decide between mock call/real call and so on … does this also cover the topic?

  • Alberto Reyer
    Alberto Reyer Lead Spryker Solution Architect / Technical Director Posts: 690 🪐 - Explorer

    We have build a module for that which determines which implementation should be used for a given interface.

    interface MerchantServicePluginInterface
    {
        /**
         * @param \Generated\Shared\Transfer\MerchantServiceQueryTransfer $query
         *
         * @return bool
         */
        public function isMatching(MerchantServiceQueryTransfer $query): bool;
    
        /**
         * @return int
         */
        public function getPriority(): int;
    }
    
    class ServiceDefinitionMatcher
    {
        /**
         * @var \Pyz\Zed\MerchantServices\Dependency\MerchantServicePluginInterface[]
         */
        protected $merchantServices;
    
        /**
         * @param \Pyz\Zed\MerchantServices\Dependency\MerchantServicePluginInterface[] $merchantServicePlugins
         */
        public function __construct(array $merchantServicePlugins)
        {
            foreach ($merchantServicePlugins as $merchantServicePlugin) {
                $this->addMerchantServicePlugin($merchantServicePlugin);
            }
        }
    
        /**
         * @param \Pyz\Zed\MerchantServices\Dependency\MerchantServicePluginInterface $merchantServicePlugin
         *
         * @return void
         */
        public function addMerchantServicePlugin(MerchantServicePluginInterface $merchantServicePlugin)
        {
            $this->merchantServices[get_class($merchantServicePlugin)] = $merchantServicePlugin;
        }
    
        /**
         * @param \Generated\Shared\Transfer\MerchantServiceQueryTransfer $query
         *
         * @throws \Pyz\Zed\MerchantServices\Business\Exception\ServiceNotFoundException
         *
         * @return string
         */
        public function getServiceClass(MerchantServiceQueryTransfer $query): string
        {
            $matchingQueue = new SplPriorityQueue();
    
            foreach ($this->merchantServices as $merchantService) {
                if ($merchantService->isMatching($query)) {
                    $priority = $merchantService->getPriority();
                    $matchingQueue->insert($merchantService, $priority);
                }
            }
    
            if ($matchingQueue->isEmpty() === true) {
                throw new ServiceNotFoundException(sprintf(
                    'No service definition found which matches the given query, did you added the plugin to the MerchantServicesDependencyProvider? (Query: %s)',
                    $query->serialize()
                ));
            }
    
            $priorityMatch = $matchingQueue->top();
    
            return get_class($priorityMatch);
        }
    
  • Alberto Reyer
    Alberto Reyer Lead Spryker Solution Architect / Technical Director Posts: 690 🪐 - Explorer

    Example Mock Implementation:

    class MockLiveAvailabilityPlugin extends AbstractPlugin implements LiveAvailabilityPluginInterface, MerchantServicePluginInterface
    {
        /**
         * @param string $sku
         * @param array $warehouseIds
         *
         * @return \Generated\Shared\Transfer\LiveAvailabilitiesCollectionTransfer
         */
        public function getLiveAvailabilityBySku(string $sku, array $warehouseIds = []): LiveAvailabilitiesCollectionTransfer
        {
            ...
        }
    
        /**
         * @param array $skus
         * @param array $warehouseIds
         *
         * @return \Generated\Shared\Transfer\LiveAvailabilitiesCollectionTransfer
         */
        public function getLiveAvailabilitiesBySkus(array $skus, array $warehouseIds = []): LiveAvailabilitiesCollectionTransfer
        {
           ...
        }
    
        /**
         * @param \Generated\Shared\Transfer\MerchantServiceQueryTransfer $query
         *
         * @return bool
         */
        public function isMatching(MerchantServiceQueryTransfer $query): bool
        {
            return (
                $query->getInterface() === LiveAvailabilityPluginInterface::class
                && $this->getConfig()->isMockActive() === true
            );
        }
    
        /**
         * @return int
         */
        public function getPriority(): int
        {
            //high priority, if the mock live availability is active it should be used
            return 1000;
        }
    }
    
  • Alberto Reyer
    Alberto Reyer Lead Spryker Solution Architect / Technical Director Posts: 690 🪐 - Explorer

    And where ever the "live service" should be used we use the following code for a facade:

        /**
         * @return \Pyz\Zed\LiveAvailability\Dependency\LiveAvailabilityPluginInterface
         */
        protected function createLiveAvailabilityServiceAdapterPlugin(): LiveAvailabilityPluginInterface
        {
            $pluginClass = $this->getMerchantServicesFacade()
                ->getServiceClassByInterface(LiveAvailabilityPluginInterface::class);
            $plugin = new $pluginClass();
    
            return $plugin;
        }
    
  • Alberto Reyer
    Alberto Reyer Lead Spryker Solution Architect / Technical Director Posts: 690 🪐 - Explorer

    So it's sort of a prioritized service registry we use were mock services become a very high priority and a config to determine if they are active.
    We use the Interface of the Service as registry key, but you could also use a different key.

  • UPWG9AYH2
    UPWG9AYH2 Posts: 509 🧑🏻‍🚀 - Cadet

    @UL6DGRULR and determining if a mock is active is actually configured on different project config levels? So there is a config in config_default-docker.php for example which just say, that the mock client is active?

  • Alberto Reyer
    Alberto Reyer Lead Spryker Solution Architect / Technical Director Posts: 690 🪐 - Explorer

    @UPWG9AYH2 Exactly, so a developer could use his config_local.php to test with different implementations, a test system config could always use the mock and it can be abused to provide different real implementations (we currently use this to toggle if the new ERP or the old ERP API should be used)

  • UPWG9AYH2
    UPWG9AYH2 Posts: 509 🧑🏻‍🚀 - Cadet

    And the prioritization is because you have multiple service that are active … so you would have an order when some of the higher proritized endpoint isnt availabel

  • UPWG9AYH2
    UPWG9AYH2 Posts: 509 🧑🏻‍🚀 - Cadet
    edited November 2020

    That looks like a clean solution to me … my highest priorities focus on don’t messing up anything in spryker aka introducing ugly code switches etc …

  • Alberto Reyer
    Alberto Reyer Lead Spryker Solution Architect / Technical Director Posts: 690 🪐 - Explorer

    Yes, the prioritization is the main part here which makes it work that either the mock or the real implementation is returned.

    This approach has one obvious issue, if you want to decide very late in the process which implementation should be used, this approach might not be usable.
    For example we use a mock implementation if an order will be created and the customer name in the billing address is Tester McTesty, this can't be done with this approach or at least you would need to move the part were the implementation is created (new ${plugin}()) outside of the factory.

  • UPWG9AYH2
    UPWG9AYH2 Posts: 509 🧑🏻‍🚀 - Cadet
    edited November 2020

    okay, that make sense … so your approach is as far as i see designed for a very general purpose, maybe we dont need it that general in the first run …
    However, how does this look like in the dependencyprovider? Is this registered as a new service which gets included like any other dependency in the dependencyprovider of the certain module?

  • UPWG9AYH2
    UPWG9AYH2 Posts: 509 🧑🏻‍🚀 - Cadet

    Something like

    $container->getLocator()->serviceDefinitionMatcher()
    

    ?

  • Alberto Reyer
    Alberto Reyer Lead Spryker Solution Architect / Technical Director Posts: 690 🪐 - Explorer

    Yes, it generalized as we have this requirement for many services.
    If you want to have this only in one place you could adapt the priorization part.

    And yes, the service definition matcher is a standalone bundle, so it can be found via locator exactly as you have shown.

  • UPWG9AYH2
    UPWG9AYH2 Posts: 509 🧑🏻‍🚀 - Cadet

    Cool, very nice in my opinion. Thanks for your insights 😉

  • Andriy Netseplyayev
    Andriy Netseplyayev Domain Lead Solution Architecture Sprykee Posts: 519 🧑🏻‍🚀 - Cadet
    edited November 2020

    also 5 cents from my side, you can mock external services/APIs with donatj/mock-webserver library.
    It starts locally a mock, that listens to the particular port. Then, you can mock any endpoint so that your test would look like that:

    protected function _before()
    {
        $this->server = new MockWebServer(8083, '127.0.0.1');
            $this->server->start();
    }
    
    public function testMyFeature() 
    {
        $this->server->setResponseOfPath(
                '/api/some/endpoint,
                new Response('JSON GERE')
            );
    
        $facade = new SomeFacade();
        $facade->callMethodThatUsesExternalApi()
    
        // assertions here 
    }
    
    public function _after(): void
    {
        $this->server->stop();
    }
    
  • Andriy Netseplyayev
    Andriy Netseplyayev Domain Lead Solution Architecture Sprykee Posts: 519 🧑🏻‍🚀 - Cadet

    _before and _after can be done as a codeception Module/Helper

  • Andriy Netseplyayev
    Andriy Netseplyayev Domain Lead Solution Architecture Sprykee Posts: 519 🧑🏻‍🚀 - Cadet

    the only thing you would need to do - is to set the config for accessing that API, f.e., if you go with a codecept Module approach:

    use SprykerTest\Shared\Testify\Helper\ConfigHelper;
    ...
    $config = $this->getModule('\\' . ConfigHelper::class);
    
            $config->setConfig(ExternalApiConstants::HOST, '127.0.0.1:8083');
    
  • Andriy Netseplyayev
    Andriy Netseplyayev Domain Lead Solution Architecture Sprykee Posts: 519 🧑🏻‍🚀 - Cadet
    edited November 2020

    like this you mock “as later as possible” in your process (answering the where is the switch to decide between mock call/real call and so on), you don’t need any dependency faking, client mocks and so on

  • Andriy Netseplyayev
    Andriy Netseplyayev Domain Lead Solution Architecture Sprykee Posts: 519 🧑🏻‍🚀 - Cadet

    plus, the usage is very simple - and it’s easier to re-use it for your further tests/endpoints, and it’s more TDD friendly if you need

  • Andriy Netseplyayev
    Andriy Netseplyayev Domain Lead Solution Architecture Sprykee Posts: 519 🧑🏻‍🚀 - Cadet
    edited November 2020

    from all the approaches we’ve tried on one of the projects that was the most elegant so far

  • Alberto Reyer
    Alberto Reyer Lead Spryker Solution Architect / Technical Director Posts: 690 🪐 - Explorer

    @UKJSE6T47 Thanks a lot, this is a bundle I wasn't aware of and it will help in our project as well 😉

  • Andriy Netseplyayev
    Andriy Netseplyayev Domain Lead Solution Architecture Sprykee Posts: 519 🧑🏻‍🚀 - Cadet

    you’re welcome! Before that we were using another one, but it was node-js based. This one is PHP which was preferable for us