Phpunit Mock Objects and Static Methods

PHPUnit Mock Objects and Static Methods

Sebastian Bergmann, the author of PHPUnit, recently had a blog post about Stubbing and Mocking Static Methods. With PHPUnit 3.5 and PHP 5.3 as well as consistent use of late static binding, you can do

$class::staticExpects($this->any())
->method('helper')
->will($this->returnValue('bar'));

Update: staticExpects is deprecated as of PHPUnit 3.8 and will be removed completely with later versions.

phpunit - Mock external static methods

I could solve my issue with the Mockery library. I tried out a few but nothing worked. With Mockery everything seems possible now. This link really helped: https://robertbasic.com/blog/mocking-hard-dependencies-with-mockery/

You can easily mock static calls to classes that don't belong to you:

public function methodToTest() {
return \Context::getData();
}

public function testMethodToTest() {
m::mock('alias:\Context')
->shouldReceive('getData')
->andReturn('foo');
}

And even instantiations for classes you don't have access to:

public function methodToTest() {
$obj = new \Category(5);
return $obj->id;
}

public function testMethodToTest() {
m::mock('overload:\Category')
->shouldReceive('__construct')
->with(5)
->andSet('id', 5);
}

But you have to keep in mind that you need the two phpunit annotations at the beginning of the class:

* @runTestsInSeparateProcesses
* @preserveGlobalState disabled

PHP Unit - Mock Class

PHPUnit will ignore mocking static methods:

Please note that final, private, and static methods cannot be stubbed or mocked. They are ignored by PHPUnit’s test double functionality and retain their original behavior except for static methods that will be replaced by a method throwing a \PHPUnit\Framework\MockObject\BadMethodCallException exception.

https://phpunit.readthedocs.io/en/9.2/test-doubles.html?highlight=static#test-doubles

You can't do it with PHPUnit’s phpunit-mock-objects library. Seek to the alternative - Mockery:

$this->exampleClassMock = \Mockery::mock('overload:App\ExampleClass');

$this->exampleClassMock
->shouldReceive('getResult')
->once()
->andReturn(true);

Mock Objects in PHPUnit to emulate Static Method Calls?

I agree with both of you that it would be better not to use a static call. However, I guess I forgot to mention that DB_DataObject is a third party library, and the static call is their best practice for their code usage, not ours. There are other ways to use their objects that involve constructing the returned object directly. It just leaves those darned include/require statements in whatever class file is using that DB_DO class. That sucks because the tests will break (or just not be isolated) if you're meanwhile trying to mock a class of the same name in your test--at least I think.

PHP Unit / Mockery - Mock static function not working

Thanks for your question and welcome to Stackoverflow.

Any thoughts on why I can't use this named mock to replace the Manager call? New to unit testing and phpunit/mockery so any other pointers welcome. Thank you! :)

This is perhaps less the question about the testing framework and the mocking library but better answered by the language in question, here PHP, but it may also apply to other languages similarly.

Given a call to a static method like:

$baseTypeManagerConfig = Manager::getConfiguration($baseType);

there is nothing that can be dynamically replaced because Manager::getConfiguration is static.

  • Static Methods - Static Keyword - Classes and Objects (PHP Manual)

As this is so common, why does it appear as a problem for mocking specifically while writing a test for such code?

As a class can only exist once (by its name not a constraint for aliasing, e.g. the same class with one or more different names) that specific method can as well only be defined once.

With a generic mocking framework (mock builder, stub builder etc.), a problem can arise. While it's possible to create a mock of the manager class:

Manager -> ManagerMock

The code under test does not magically change to it, e.g. like this:

$baseTypeManagerConfig = ManagerMock::getConfiguration($baseType);

But stays the same:

$baseTypeManagerConfig = Manager::getConfiguration($baseType);

And therefore knows nothing about the mock.

This might appear that you can't mock Manager, however it is just that Manager is in the global static state and it remained unchanged. The mock of Manager was added to it, however it is of no use to execute the code under test for your testing purposes.

So far, so good. Embrace this for a moment.


One common answer to this kind of problem is to make Manager inject able.

This could be by a test-point, e.g. having a specific Manager class only in testing that allows you to inspect deep for assertions.

This could also be with dependency injection, that is injecting the Manager as a dependency, for example when creating the object as a parameter of its constructor.

public function getBaseTypeManagerConfig() {
$baseTypeManagerConfig = array();

if (!empty($this->dataConfig)
&& !empty($baseType = $this->dataConfig->getBaseType())) {
$configurationGettingFunction = $this->configurationGettingFunction;
$baseTypeManagerConfig = $configurationGettingFunction($baseType);
}

return $baseTypeManagerConfig;
}

It turns the hidden dependency of Manager::getBaseTypeManagerConfig into a visible and inject able one.

In tests then, when creating the Subject Under Test (SUT), you inject the "mocked" function instead of the static one.

This also makes visible which collaborators the SUT needs to do its work. Your tests cover that refactoring.

This should be easy to do.

Mind thought that this adds a layer of indirection. While it perhaps might be worth to consider that it is beneficial to your design, it may also not be.

Therefore one might decide that the indirection is unwanted and Manager::getConfiguration should remain static, and the static method call use is intended (by design).

Then it should be able to represent test-able configuration, so while it remains static, it is capable to address the configuration needs in at least both environments: developing and testing.

The developing environment is just the default one, your code exists and executes.

The testing environment is the one when you test the code, e.g. within unit-tests.

Regardless how you solve it in the end, these two environments you'll always find when writing tests.

And only code that is compatible with the testing environment can be tested.

So what you aim for is that your code in general is test-able (and with as little mocks and stubs, that is code you write only for testing and then you most of all test your mocks and stubs and less the code you want to test).

This is often also how when starting to write tests for the code, this reveals design issues showing places where the code might not be portable for different environments. This can:

  • Reveal missing configuration capabilities (e.g. make the code work as intended by injecting the data (parameters) as needed)
  • Reveal higher level design issues (e.g. functions and classes can not be used flexible enough to work as intended)

The important part here is that you, as you write the tests and the code understand where the issue stems from that makes it hard for you to write the test.

So first of all realizing that there is not a technical issue foremost (why does the mocking library not support feature xyz?) but to understand the code under test (what is it actually doing here? is that my intention?).

Only the later part will allow you to change the code and develop further and to obtain the benefit from writing the test.

Otherwise you would write a test just to write one and get it pass. Not much use of that. And totally easy with an extension that is able to actually overwrite Manager::getConfiguration - Uopz or Componere.

However, treat it as non technical issue here, and instead as a design problem with your code. Why? Because you gain more benefit from testing, and your code is to be changed. Now and in future. Testing allows you to see these change requirements in a reproducible manner now and, not only for now, but also ahead of time when you start to write tests.

Use the tests to define how the code should work. Not to create mocks.

And if you allow me a personal comment: I hate to write mocks in testing. It starts to become cumbersome pretty early even for code for which mocks technically can work. Therefore I often take a look how the code can be simplified so that it can be used more easily. As then it can be also tested more easily.

Mock and inject static method in PHPUnit

You cannot inject something static because you are not using an instance to access the method.

A workaround for your case would be to build a wrapper class for that static call:

class RequestWrapper
{
public function request_multiple($yourParam) {
return Request::request_multiple($yourParam);
}
}

You can then inject an instance of this class in the method/class that you want to test and mock it when appropriate.

Test static method that call another from same class

No no no. You MUST mock dependencies, not testing code.
Static function is bad design for testing.

If the functions weren't static, you could use Filesystem and mock them.

In this case, the best way is to mock the directory by using temp.

public function testDirectoryExists(): void
{
//prepare
$temp = sys_get_temp_dir();
$logsPath = 'logs';
$logsDir = $temp.DIRECTORY_SEPARATOR.$logsPath;
$type = 'someType';
mkdir($logsDir);

//mock
$this->app->useStoragePath($temp);
config()->set('logs.storage_path', $logsPath);

//assert not created
$this->assertFalse(Path::directoryExists($type));

//assert created
mkdir($logsDir.DIRECTORY_SEPARATOR.$type);
$this->assertTrue(Path::directoryExists($type));
}

Updated

Or, if you use Laravel, you can use File facade:

public static function directoryExists(string $type): bool
{
return File::isDirectory(self::getStorage($type));
}
public function testDirectoryExists(): void
{
$type = 'myType';
$expectedDir = '/logs/vendor/orchestra/testbench-core/laravel/storage/logs/'.$type;

$mock = Mockery::mock(\Illuminate\Filesystem\Filesystem::class);
$mock->shouldReceive('isDirectory')
->with($expectedDir)
->andReturn(true);

$this->app->instance(\Illuminate\Filesystem\Filesystem::class, $mock);

$this->assertTrue(Path::directoryExists($type));
}


Related Topics



Leave a reply



Submit