Mocking Dependency

Given you have an application service interface like this:

<?php

namespace Vendor\ModuleName\Config;

interface ConfigProviderInterface
{
    public function isFreeDeliverEnabled(): bool;

    public function getFreeDeliveryThreshold(): float;
}

And you have an implementation for this service:

<?php

namespace Vendor\ModuleName\Config;

use Magento\Framework\App\Config\ScopeConfigInterface;

class ConfigProvider implements ConfigProviderInterface
{
    /** @var ScopeConfigInterface */
    private $scopeConfig;

    public function __construct(ScopeConfigInterface $scopeConfig)
    {
        $this->scopeConfig = $scopeConfig;
    }

    public function isFreeDeliverEnabled(): bool
    {
        return $this->scopeConfig->isSetFlag('path/to/config');
    }

    public function getFreeDeliveryThreshold(): float
    {
        return (float) $this->scopeConfig->getValue('path/to/another/config');
    }
}

And you have the following DI config to mark this implementation as the default implementation:

<preference for="Vendor\ModuleName\Config\ConfigProviderInterface" type="Vendor\ModuleName\Config\ConfigProvider" />

In addition to these you have an application service which depends on this config interface, e.g.:

<?php

namespace Vendor\ModuleName\Service;

use Magento\Quote\Model\Quote;

class DeliveryCostCalculator implements DeliveryCostCalculatorInterface
{
    private const DELIVERY_COST = 5.0;

    /** @var ConfigProviderInterface */
    private $deliveryConfig;

    public function __construct(ConfigProviderInterface $deliveryConfig)
    {
        $this->deliveryConfig = $deliveryConfig;
    }

    public function calculate(Quote $quote): float
    {
        if (!$this->deliveryConfig->isFreeDeliverEnabled()) {
            return self::DELIVERY_COST;
        }

        if ($quote->getGrandTotal() < $this->deliveryConfig->getFreeDeliveryThreshold()) {
            return self::DELIVERY_COST;
        }

        return 0.0;
    }
}

When you write your application tests, if you would like to avoid relying on the database, then you either need to mock Magento\Framework\App\Config\ScopeConfigInterface or Vendor\ModuleName\Config\ConfigProviderInterface. Lets assume we would like to mock our own ConfigProviderInterface this time.

First of all we need to configure a test area in Magento. We can do this by adding the following to the module’s global etc/di.xml:

<?xml version="1.0" encoding="utf-8"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <type name="Magento\Framework\App\AreaList">
        <arguments>
            <argument name="areas" xsi:type="array">
                <item name="test" xsi:type="null" />
            </argument>
        </arguments>
    </type>
</config>

Or we can simply install the Test area Magento 2 module which will define an area called test in the same way. :)

Now we can define our DI overrides in the module’s etc/test/di.xml.

It will look like this:

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <preference for="Vendor\ModuleName\Config\ConfigProviderInterface" type="Vendor\ModuleName\Test\FakeConfigProvider" />
</config>

And we are done. After a cache clear everything should be ready to use. If you inject the Vendor\ModuleName\Service\DeliveryCostCalculator into your Behat Context then it will use the Vendor\ModuleName\Test\FakeConfigProvider which we can freely modify in our tests.

E.g.:

FakeConfigProvider:

<?php

namespace Vendor\ModuleName\Test;

use Magento\Framework\App\Config\ScopeConfigInterface;

class FakeConfigProvider implements ConfigProviderInterface
{
    /** @var bool */
    private $isFreeDeliveryEnabled = false;

    /** @var float */
    private $freeDeliveryThreshold = 0.0;

    public function isFreeDeliverEnabled(): bool
    {
        return $this->isFreeDeliveryEnabled;
    }

    public function getFreeDeliveryThreshold(): float
    {
        return (float) $this->freeDeliveryThreshold;
    }

    public function enableFreeDelivery(): void
    {
        $this->isFreeDeliveryEnabled = true;
    }

    public function disableFreeDelivery(): void
    {
        $this->isFreeDeliveryEnabled = false;
    }

    public function setFreeDeliveryThreshold(float $threshold): void
    {
        $this->freeDeliveryThreshold = $threshold;
    }
}

DeliveryContext:

<?php

use Behat\Behat\Context\Context;
use Behat\Gherkin\Node\TableNode;
use Exception;
use Vendor\ModuleName\Service\DeliveryCostCalculator;
use Vendor\ModuleName\Test\FakeConfigProvider;

class DeliveryContext implements Context
{
    /** @var DeliveryCostCalculator */
    private $deliveryCostCalculator;

    /** @type float|null */
    private $deliveryCost = null;

    public function __construct(DeliveryCostCalculator $deliveryCostCalculator)
    {
        $this->deliveryCostCalculator = $deliveryCostCalculator;
    }

    /**
     * @Given The cart contains the following items:
     */
    public function theCartContainsTheFollowingItems(TableNode $table)
    {
        // Create a Cart here
        // $this->currentQuote = ...
    }

    /**
     * @Given The free delivery is enabled
     */
    public function theFreeDeliveryIsEnabled(FakeConfigProvider $deliveryConfig)
    {
        $deliveryConfig->enableFreeDelivery();
    }

    /**
     * @Given The free delivery is disabled
     */
    public function theFreeDeliveryIsDisabled(FakeConfigProvider $deliveryConfig)
    {
        $deliveryConfig->disableFreeDelivery();
    }

    /**
     * @Given The free delivery cost threshold is configured to :threshold
     */
    public function theFreeDeliveryCostThresholdIsConfiguredTo(float $threshold, FakeConfigProvider $deliveryConfig)
    {
        $deliveryConfig->setFreeDeliveryThreshold($threshold);
    }

    /**
     * @When The delivery cost is calculated
     */
    public function theDeliveryCostIsCalculated()
    {
        $this->deliveryCost = $this->deliveryCostCalculator->calculate($this->currentQuote);
    }

    /**
     * @Then The delivery cost is :expectedDeliveryCost
     */
    public function theDeliveryCostIs(float $expectedDeliveryCost)
    {
        if ($expectedDeliveryCost !== $this->deliveryCost) {
            throw new Exception(
                spritf('Delivery cost expected to be %s but got %s', $expectedDeliveryCost, $this->deliveryCost)
            );
        }
    }
}

The above context is not complete, it is just an example to show how easy to mock the dependencies this way.