Idearia's avatar

How to test method for object not accessible in the test

Hello everyone. Let's say I have a class like this:

class User
{
    public string $name;

    public static function create(string $name): self
    {
        $user = new self();

        return $user->setName($name);
    }

    public function setName(string $name): self
    {
        $this->name = $name;

        return $this;
    }
}

I want to test that if I call User::create('John') then method setName has been called once. How can I achieve this? The fact is, that I should mock an object which will be instantiated during the create method.

I use PHPUnit as testing framework, but I accept answers that solve this via external libraries (e.g. Mockery)

EDIT

This is a toy problem and not the real application, which has been deliberately simplified to illustrate the problem.

Of course I know I can easily test the final result of the operation like in $this->assertEquals('John', User::create('John')), but this is not what I am asking.

I need to test if the setName method is being called on an instance of the User class.

P.S. This question is about Vanilla PHP + PHPUnit, it's not about Laravel.

0 likes
15 replies
cwhite's avatar

Something like below?

        $mockUser = Mockery::mock(User::class)
            ->shouldReceive('setName')->once()->andReturnSelf();
            ->getMock();

        $mockUser::create('John');
Idearia's avatar

@cwhite It does not work.

Mockery\Exception\BadMethodCallException: Static method Mockery_0_User::create() does not exist on this mock object
1 like
martinbean's avatar

@idearia The property you’re trying to set is public. So just assert its value?

You’re trying to test how your code is put together and not actually what your code is doing, which is just going to lead to brittle tests.

So instead, test the actual result and side effects of your code. Not how your classes and methods are put together. If calling that static method should return an object with a particular nane set, then test for that instead.

1 like
Idearia's avatar

@martinbean yes I could in this case, but not in the scenario I really need in my project. That is why I asked for how to test that the method is called, not what results it produced. In the application I am testing, I have already tested the final results, but there are a few more things I need to test and I need to know at least if what I am asking for is actually possible to test or not (it is actually possible in other testing frameworks like Jest for JavaScript, so in principle why not in PHPUnit?)

1 like
cwhite's avatar

@idearia,

Something like this should work:

    public function testCreateSetsNameAndReturnsUser(): void
    {
        $name = 'John';
        $user = User::create($name);
         
        $this->assertTrue($user instanceof User);
        $this->assertEquals($user->name, $name);
    }

Idearia's avatar

@cwhite this does not test if the method is being called, it only checks the final result. As I've already explained, this is a toy problem and not a real piece of code in which I need how to test that the method is being called.

1 like
cwhite's avatar

@Idearia,

I've tested this and it works, basically you need to resolve the new class from your app's service container:

<?php

namespace Tests;

use Illuminate\Support\Facades\App;
use Mockery;

class UserTest extends TestCase
{
    public function testSetNameCalled(): void
    {
        $name = 'John';

        $mockUser = Mockery::mock(User::class)
            ->shouldReceive('setName')->with($name)->once()->andReturnSelf()
            ->getMock();

        $this->app->instance(User::class, $mockUser);

        User::create($name);
    }
}

class User
{
    public string $name;

    public static function create(string $name): self
    {
        $user = App::make(static::class);

        return $user->setName($name);
    }

    public function setName(string $name): self
    {
        $this->name = $name;

        return $this;
    }
}

If this is satisfactory please mark as best answer to close thread.

Idearia's avatar

@cwhite thanks but there's still one point to clarify: my question is not about Laravel, but plain old PHP + PHPUnit (and in fact, I need it for a WordPress plugin).

Is this going to work outside Laravel, since you are talking about app service container?

cwhite's avatar

@Idearia, that's pertinent information that should have been included in the original post---particularly since this is primarily a Laravel forum.

Unfortunately I'm not that familiar with WordPress plugins, but I don't think what you're asking for is possible (or if it is, then it's beyond me)....

Maybe convert the WP site to Laravel/Statamic? :upside_down:

Idearia's avatar

@cwhite Actually the question is about PHP & PHPUnit, nothing related to WordPress.

Though Laracasts is not only a Laravel forum but it's a lot related to PHP and frontend frameworks like Vue.js, though I've posted this in the testing section which is more about PHPUnit, I agree that it's better to point that out, so I'll add a disclaimer to the original post.

Nevertheless @cwhite, the question is not about WordPress but only PHP. Can you find a way to make your solution work on a vanilla PHP project? Like composer require --dev phpunit/phpunit mockery/mockery and those 2 classes + some way of reproducing Laravel's service container?

cwhite's avatar
cwhite
Best Answer
Level 19

@idearia

For example, below is a very rudimentary implementation, but the test does pass without Laravel.

<?php

namespace Tests;

use Mockery;

class UserTest extends TestCase
{
    public function testSetNameCalled(): void
    {
        $name = 'John';

        $mockUser = Mockery::mock(User::class)
            ->shouldReceive('setName')->with($name)->once()->andReturnSelf()
            ->getMock();

        App::bind(User::class, $mockUser);

        User::create($name);
    }
}

class User
{
    public string $name;

    public static function create(string $name): self
    {
        $user = App::make(static::class);

        return $user->setName($name);
    }

    public function setName(string $name): self
    {
        $this->name = $name;

        return $this;
    }
}

class App
{
    protected static array $registry = [];

    public static function bind($key, $value): void
    {
        static::$registry[$key] = $value;
    }

    public static function make(string $class)
    {
        return static::$registry[$class]
            ?? new $class();
    }
}
Idearia's avatar

@cwhite very helpful, thanks. However, the tests are not passing. Here is my composer.json:

{
    "require-dev": {
        "phpunit/phpunit": "^9.5",
        "mockery/mockery": "^1.5"
    },
    "scripts": {
        "test": "phpunit --testdox"
    },
    "autoload": {
        "psr-4": {
            "Tests\\": "tests"
        }
    }
}

tested on both PHP 7.4 and 8.1, here is the output I get:

composer test
> phpunit --testdox
PHPUnit 9.5.26 by Sebastian Bergmann and contributors.

User (Tests\User)
 ☢ Set name called

Time: 00:00.005, Memory: 6.00 MB


OK, but incomplete, skipped, or risky tests!
Tests: 1, Assertions: 0, Risky: 1.

it looks like no assertions are performed.

What do you think about that?

cwhite's avatar

@Idearia,

If you're not using Laravel, then you need to integrate Mockery with PHPUnit. Easiest way is to just import the following trait:

<?php

namespace Tests;

use Mockery;
use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration;
use PHPUnit\Framework\TestCase;

class UserTest extends TestCase
{
    use MockeryPHPUnitIntegration;
//...

Also, if you want PHPUnit to automatically run the tests, then you'll need to configure your phpunit.xml file. However, the following command works for me:

 ./vendor/bin/phpunit tests/UserTest.php
PHPUnit 9.5.26 by Sebastian Bergmann and contributors.

.                                                                   1 / 1 (100%)

Time: 00:00.006, Memory: 6.00 MB

OK (1 test, 1 assertion)

(can also use ./vendor/bin/phpunit ./tests)

1 like
Idearia's avatar

@cwhite wonderful. I assumed Mockery was integrated with testing by default. Adding that trait solved that issue. Though I've set the former as best answer since it has most of the relevant code.

Anyway, I wonder if there is still a way to test it without refactoring the entire app.

Your solution in fact requires a change in the implementation of the User::create method in order to take into account the App container class.

On a real application, the game is likely not worth the candle if you only need that for testing.

But nevermind, thank you anyway

Please or to participate in this conversation.