skip to Main Content

in CommonHelper class:

class CommonHelper {
    public static function getValue(string $category, string $name)
    {
       
    }
}

in MyService class:

 public function getContentOrtherService(): array
    {
...
         $this->lineUrl = CommonHelper::getValue(CommonHelper::LINE, CommonHelper::BASE_ENDPOINT);
   ...         
        } catch (Exception $e) {
            return $contents;
        }
    }

in MyServiceTest class:

        $lineUrl = 'http:line/api';
        $this->commonHelper = Mockery::mock('alias:CommonHelper');
        $this->commonHelper->shouldReceive('getValue')
            ->with(CommonHelper::LINE, CommonHelper::BASE_ENDPOINT)
            ->andReturn($lineUrl);

But when I actually run it, I don’t receive the value I mocked but return null

I want the mock to be correct with the parameters and values ​​I pass to the mock

2

Answers


  1. Your MyService::getContentOrtherService() implementation relies on a hard-encoded global static method name CommonHelper::getValue you would like to mock to change the return value during tests.

    As PHP only allows to have one definition of any class loaded, you can only "mock" static methods by not loading the original definition of the static method but a replacement with the same name unless the original implementation has the behaviour you’re looking for in all of it’s use cases.

    In your question, this includes the behaviour of returning the value you’re looking for in that test case specifically.

    Henceforth you may want to create a test adapter for your CommonHelper class that is in use (loaded) and that has the ability during tests at thosetest-points (reading the return value from the method call) to change the behaviour. This could be done by changing the effective implementation and/or with parameterization.

    As during tests then, the test adapter class would be in use, which then allows to have the required behvaiour/values at your test-points.


    A downside then could be, that you may then test too many things or that the limitation of the design under test is continuously more visible and it may not feel right constantly, despite it may still be the most effective way in the overall implementation. The skill issues using a mocking framework during the test you highlight often suggest such hints. When I encounter them, I try to defer such decisions as much as possible and instead focus on keeping mocks out of the test suite at all cost. Dropping mocking out of tests often helps me to better understand my implementation, where it is hard to test, how to write code that is good to test which often then is an enabler to keep maintaining the code and the application for a superior long time over many, many PHP version changes.

    We can imagine that within a Composer driven Laravel based project this could also be of similar benefits.


    Now on the bright side of light: While it is easy to say it can be solved with dependency injection (swapping the hard encoded classname with a reference to class instance which is easy to mock), the power of utility methods often lies within functional decomposition and a class and object tree based design is on a higher level, henceforth as with with higher complexity.

    Probably you have to make the decision at this point whether or not you’d like to refactor already or it’s just missing testing and/or a missing test-fixture.

    If you can’t decide, you have to do the experimentation until you can.

    From experience I’d give the suggestion to first have your test environment properly setup so that all tests can run fine without any additional test-points required and without re-implementing static methods (the mocks are technically spoken such re-implementations as well, so read: without mocks).

    Such an approach has the benefit that it immediately works, also for such hard to test places and you can see and verify it with every run. If a commit can’t test, revert to last known good, you know it’s broken. Simple and sound, you get the feedback you need immediately.

    If you then still have hard to test places, decide your own with which priority you would like to improve. In general, you should improve over time anyway as otherwise you will see degradation on the test suite over time, just very slowly.

    Apart from constant improvements, you may also want to do your experimentation on certain test points. There are basically no extra rules you have to follow when you do your experiments, and a test-suite is super-nice to already have them under test.


    To overcome PHPs "limitation" of only being able to load one class with the same name, you can use a different tool to have the otherwise not possible branching on class names, branching of the whole work tree, e.g. version control with branches. This makes running experiments very effective, be it to find out that the mocking framework is not a fit in a concrete case or finding the right fixture and environment parameters for the tests.

    The branching allows you to do everything you want, the only precondition is that your original test setup is already working flawlessly – which it reports to you.

    Override Example:

    class CommonHelper {
        public static array $overrides = [];
    
        public static function override(string $method, callable $forward)
        {
            self::$overrides[$method] = $forward;        
        }
    
        public static function getValue(string $category, string $name)
        {
            $forward = self::$overrides[__FUNCTION__] ?? null;
            if ($forward) {
                 return $forward(...func_get_args());
            }       
        }
    }
    

    Now the getValue method has a test for an optional forward call which you can make use of within tests.

    Once I’ve wired this up and see it working, I’d rest it there over night and then throw it away next morning, because I’d see better options then.

    Login or Signup to reply.
  2. Mocking static methods in Laravel can be a bit tricky, but using Mockery’s mock method with the alias keyword is the correct approach.

    Here’s the corrected code for your MyServiceTest class:

    use Mockery;
    use PHPUnitFrameworkTestCase;
    
    class MyServiceTest extends TestCase
    {
        protected function tearDown(): void
        {
            Mockery::close();
            parent::tearDown();
        }
    
        public function testGetContentOrtherService()
        {
            $lineUrl = 'http:line/api';
    
            // Mock the CommonHelper class
            $this->commonHelper = Mockery::mock('alias:CommonHelper');
            $this->commonHelper->shouldReceive('getValue')
                ->with(CommonHelper::LINE, CommonHelper::BASE_ENDPOINT)
                ->andReturn($lineUrl);
    
            // Initialize the service class
            $service = new MyService();
    
            // Call the method
            $result = $service->getContentOrtherService();
    
            // Assert the result, assuming $result contains the expected data
            $this->assertEquals($lineUrl, $service->lineUrl);
        }
    }
    
    • Make sure that CommonHelper::LINE and CommonHelper::BASE_ENDPOINT are defined in your CommonHelper class. If they are not, the test might fail because of undefined constants.
    • Ensure you have the Mockery package installed and correctly configured in your Laravel project.
    • Ensure that your test class properly initializes Mockery and tears it down after the tests.
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search