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

Can I mock singletons? #471

Open
Luc45 opened this issue May 25, 2020 · 6 comments
Open

Can I mock singletons? #471

Luc45 opened this issue May 25, 2020 · 6 comments

Comments

@Luc45
Copy link

Luc45 commented May 25, 2020

Hello,

Can I mock a class with a protected/private constructor and a public static method ::instance()? (The classic Singleton setup)

Eg:

class Foo {
  protected static $instance;

  protected function __construct(){}

  public static function instance() {
    if ( ! self::$instance ) {
	self::$instance = new self;
    }

    return self::$instance;
  }
}

I know I shouldn't be using singletons anyway, but this is what the code I'm trying to test looks like.

Thanks.

@stof
Copy link
Member

stof commented May 26, 2020

Well, you won't be able to mock the singleton directly, as you would need a way to instantiate the double object and to inject it in the Foo::$instance property.
And that's not something we want to support in Prophecy itself.

@ciaranmcnulty
Copy link
Member

No, there's no sensible way to do that

The important thing about a singleton is that it is enforcing the constraint that there's a single instance - the global availability is an unfortunate abused side-effect of the pattern. You can refactor your objects to take the singleton in their constructor as part of testing

@Luc45
Copy link
Author

Luc45 commented May 26, 2020

Well, you won't be able to mock the singleton directly, as you would need a way to instantiate the double object and to inject it in the Foo::$instance property.
And that's not something we want to support in Prophecy itself.

Like Reflection? Indeed, I'm not sure how to approach that if Prophecy don't plan to support it... 🤔

You can refactor your objects to take the singleton in their constructor as part of testing

Yes, that's exactly what I'm doing. The codebase is WordPress, for an enterprise plugin that's been around for 10 years now. Getting rid of singletons is just not an option. We have now a DI container in place, and I'm injecting the dependencies in the constructor, Singletons included.

That's where the "Can I mock a singleton" question comes in. Since I'm feeding it in the constructor, I would like to mock it to test the class that takes it as a dependency.

Thanks for the help so far.

@ciaranmcnulty
Copy link
Member

You can certainly mock it then; you won't need to call getInstance() on the mock because you already have the instance

@Luc45
Copy link
Author

Luc45 commented May 26, 2020

Hmm, I don't have the instance because I can't call new (protected/private constructor).

For instance:

class Foo {
    private $singleton;
 
    public function __construct( Singleton $singleton ) {
        $this->singleton = $singleton;
    }
 
    public function do_something() {
        if ( ! $this->singleton->hasCap() ) {
            // Don't do something
            return false;
        }
 
        // Do something
        return true;
    }
}
class FooTest {
    public function should_do_something_if_has_caps() {
        $singleton = $this->prophesize(Singleton::class);
        $singleton->hasCap()->willReturn( true );
        $foo = new Foo( $singleton->reveal() );
 
        $this->assertTrue( $foo->do_something() );
    }
 
    public function should_do_something_if_does_not_has_caps() {
        $singleton = $this->prophesize(Singleton::class);
        $singleton->hasCap()->willReturn( false );
        $foo = new Foo( $singleton->reveal() );
 
        $this->assertFalse( $foo->do_something() );
    }
}

I'm pretty sure (I've tried it yesterday but can't quite remember), that I can't test Foo as in the examples, because trying to mock Singleton fails because of the private/protected constructor:

@ciaranmcnulty
Copy link
Member

Ah "can't mock private constructor" sounds like a more specific issue

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants