4

This four year old question uses third party libraries which I am a little dubious about.

For testing purposes only, I want to redefine a static method of one of my classes. Take the following example:

class Driver {
   public static function getVersion() : string
   {
        // Retrieves a version from a system executable
        return some_system_call();
   }
}

class Module {
   public function methodToTest()
   {
       if (Driver::getVersion() === '4.0.0') {
          // allow for additional options/methods
       } else {
          // use a subset
       }
   }
}

I need for the Driver::getVersion to return different version strings. I would usually mock the class, but since this is neither injected nor an instance, it's not going to work.

I could change the source, adding in methods and property testing, so that the classes being tested would never need to call Driver, but, in my opinion, refactoring the code just to make tests "work" is not a solution.

I'm thinking along the lines of creating another Driver class and somehow loading it in place of the original.

How can I do this?

Community
  • 1
  • 1
Twifty
  • 3,267
  • 1
  • 29
  • 54
  • Inherit Driver class, make Driver2 class with getVersion() method you need and use Driver2 instead of Driver – SergeyLebedev Nov 14 '16 at 21:57
  • On one side you can use extentions like `runkit` to manipulate/redefine the behavior while testing (like mocking in phpunit), on the other you should prevent static method if possible. In your example is the real interaction of the classes not shown. Are they loaded at once, have they namespaces and so on. – JOUM Nov 14 '16 at 21:59
  • Offtop but there is [version_compare](http://php.net/version_compare) function – Nikita U. Nov 15 '16 at 10:34
  • for people coming here for the monkey patching part, it's possible to achieve similar results with anonymous classes and reflection – Jonathan DS May 06 '20 at 20:34

3 Answers3

2

You might wanna use smth like:

class Module
{
   private $version;

   public function __construct($version){
      $this->version = $version;
   }

   public function methodToTest()
   {
       if ($this->version === '4.0.0') {
          // allow for additional options/methods
       } else {
          // use a subset
       }
   }
}

or another option would be injecting not version but a provider for that (if you know you will have some bit of complicated logic for versioning control -- so you can split the logic between Module and Provider as appropriate):

class Module
{
   private $versionProvider;

   public function __construct($provider){
      $this->versionProvdier = $provider;
   }

   public function methodToTest()
   {
       if ($this->versionProvider->getVersion() === '4.0.0') {
          // it could be even $this->versionProvider->newFeaturesAreSupported()
       } else {
          // some other stuff
       }
   }
}

and still another could be implementing some proxy class like

class Module
{
   public function methodToTest()
   {
       $myMonostateProxy = new MyMonostateProxy();
       $version = $myMonostateProxy->getVersion();
       if ($version === '4.0.0') {
          // allow for additional options/methods
       } else {
          // use a subset
       }
   }
}

so you can mock your monostate separately (probably via reflectioning on privtates or via its public interface, anyway don't forget to tearDown it). Real implementation of it would just call that uncontrollable Driver::getVersion().

I think first two options are cleaner but require some efforts for creation (as you need some injection to perform). Third has that hidden dependecy and is somewhat tricky in testing and thus not quite clean and needs more efforts to maintaine but hides all that choice stuff inside itself making regular usage easier.

xmike
  • 1,029
  • 9
  • 14
  • For now I am using reflection to change the version property within the `Driver` class. But, and the reason I'm accepting this answer, Injection would be a better choice. Making use of a `DriverAdapter` class would make testing much easier and encapsulate all the underlying system specifics to a single class. – Twifty Nov 15 '16 at 10:39
0
class Driver {
    private static $testVersion;

    public static function setTestVersion(string $testVersion = null)
    {
        static::$testVersion = $testVersion;
    }

    public static function getVersion() : string
    {
        if (static::$testVersion !== null) {
            return static::$testVersion;
        }
        // Retrieves a version from a system executable
        return some_system_call();
    }
}
Mateusz Drost
  • 1,171
  • 10
  • 23
  • That requires a public, and open to abuse, method adding to the driver. The `Driver` class should not require modifying at all. – Twifty Nov 14 '16 at 22:04
  • http://carbon.nesbot.com/docs/#api-testing uses this pattern. You can remove `setTestVersion` function and set `$testVersion` using some reflection magic. – Mateusz Drost Nov 14 '16 at 22:06
0

You could register a class loader that is somehow made aware of the testing and loads a modified Driver class from a different location.

uli
  • 1
  • 1