Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Creating a partial mock with all the methods mocked but one #4652

Closed
charlesnannan opened this issue Apr 18, 2021 · 6 comments
Closed

Creating a partial mock with all the methods mocked but one #4652

charlesnannan opened this issue Apr 18, 2021 · 6 comments
Labels
feature/test-doubles Stubs and Mock Objects type/enhancement A new idea that should be implemented

Comments

@charlesnannan
Copy link

charlesnannan commented Apr 18, 2021

In my company, we are asked by our manager and tech leaders to write a unit test as follows (simplified example):

$classUnderTestMock = $this
    ->getMockBuilder(ClassUnderTest::class)
    ->disableOriginalConstructor()
    ->setMethodsExcept(['methodUnderTest'])
    ->getMock();

$this->assertEquals(
    'foo',
    $classUnderTestMock->methodUnderTest()
);

The idea is to be as restrictive as possible by mocking everything that can be mocked, including the class under test to begin with, and all its methods except the one being called. I guess this can be seen as some sort of pure white box testing.

I was wondering if this is common practice or not.

If it is the case, we could expect a method in the TestCase class to create a partial mock with all the methods mocked but one; something like createExclusivePartialMock or whatever that would call setMethodsExcept instead of setMethods (or achieve the same result with the new onlyMethods). Yet, S. Bergmann wrote somewhere that the MockBuilder API was more of an internal API that developers shouldn't rely on (not annotated as such, though), as opposed to its wrappers from TestCase — namely createMock and createPartialMock.

What do you think? Am I missing something? Are we doing things wrong? Would such a dedicated method be actually a good solution?
(My deepest apologies if the subject has already been discussed. If that is the case, I did not find it in the issues.)

@charlesnannan charlesnannan added the type/enhancement A new idea that should be implemented label Apr 18, 2021
@sebastianbergmann
Copy link
Owner

The idea is be as restrictive as possible by mocking everything that can be mocked, including the class under test to begin with, and all its methods except the one being called.

This is the first time I hear about this approach and my initial reaction is: it does not make sense to me. To me, the "unit under test" is not the method, but the object on which it is called.

Also note that setMethodsExcept() is deprecated in PHPUnit 9 and will be removed in PHPUnit 10.

@sebastianbergmann sebastianbergmann added the feature/test-doubles Stubs and Mock Objects label Apr 18, 2021
@charlesnannan
Copy link
Author

To me, the "unit under test" is not the method, but the object on which it is called.

Thank you for your input. It would be interesting to know if this point of view is largely accepted by object-oriented developers, and especially by mockists.

Just to be clear, I was thinking about something like this:

/**
 * Returns a partial mock object for the specified class
 * with all of its methods mocked but one.
 */
protected function createExclusivePartialMock(string $originalClassName, string $method): MockObject
{
    $classMethods = get_class_methods($originalClassName);

    if (!in_array($method, $classMethods)) {
        throw new Exception(
            sprintf(
                'The method "%s" does not exist in the class "%s"',
                $method,
                $originalClassName
            )
        );
    }

    unset($classMethods[array_search($method, $classMethods)]);

    return $this->createPartialMock($originalClassName, $classMethods);
}

@sebastianbergmann
Copy link
Owner

I would suggest you put into that createExclusivePartialMock() method into your code, for instance into a trait. I would rather not add that to PHPUnit.

@leighman
Copy link

This is a common pattern if you are working with bad/legacy code to get a harness around a particular unit which may not be "the object". It seems a shame to remove setMethodsExcept without an alternative.

@joemaller
Copy link

Looking for some guidance on best practices going forward.

We want to test the response of a single method in a class. The class constructor is a mess, so we don't want to instantiate it. Refactoring that code is not an option right now.

Previously, we used the soon-to-be-removed setMethodsExcept:

$ExampleClass= $this->getMockBuilder('\ExampleClass')
    ->disableOriginalConstructor()
    ->setMethodsExcept(['keepThisMethod'])
    ->getMock();

Besides being sloppy, are there reasons not to call onlyMethods with an empty array?

$ExampleClass= $this->getMockBuilder('\ExampleClass')
    ->disableOriginalConstructor()
    ->onlyMethods([])
    ->getMock();

Another solution might be to retrieve all class methods, then remove the methods to keep:

$methods = get_class_methods(ExampleClass::class);
$methods =  array_diff( $methods, ['keepThisMethod']);

$ExampleClass= $this->getMockBuilder('\ExampleClass')
    ->disableOriginalConstructor()
    ->onlyMethods($methods)
    ->getMock();

The last example isn't too verbose, and seems like the best choice.

@joemaller
Copy link

Coming back to my own question 18 months later while refactoring tests for PHPUnit 10, our solution was not to create mock at all, but rather to use a PHP's ReflectionClass ::newInstanceWithoutConstructor to provide safe access to methods we wanted to test.

Based on the above example code, we ended up with this:

        $ref = new \ReflectionClass('\ExampleClass');
        $ExampleClass = $ref->newInstanceWithoutConstructor();

This avoids the instantiation side-effects present in ExampleClass::__construct, but still lets us provide test coverage for individual methods.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature/test-doubles Stubs and Mock Objects type/enhancement A new idea that should be implemented
Projects
None yet
Development

No branches or pull requests

4 participants