Search code examples
phpphpspec

Testing Use of Class Optionally Included in Project


I have a class that goes something like the following:

class Foo {

  /**
   * @var array|Bar
   */
  protected $source;

  public function __construct($source = []) {
    if (class_exists(Bar::class)) {
      $source = new Bar($source);
    }

    $this->source = $source;
  }

  public function getSource() {
    return $this->source;
  }

  // ...

}

Bar comes from a separate PHP package, an optional dependency listed in suggest section of composer.json.

I want to write two separate expectations in phpspec for getSource()

function it_provides_array_source_by_default() {
  $this->getSource()->shouldBeArray();
}

function it_provides_bar_instance_when_available() {
  $this->getSource()->shouldReturnAnInstanceOf(Bar::class);
}

I can't test the shouldBeArray() if my require-dev for the package includes the dependency containing Bar.

I can't mock Bar like:

function it_provides_bar_instance_when_available(Bar $bar) {
  $this->getSource()->shouldReturnAnInstanceOf(Bar::class);
}

because I get a phpspec error:

[PhpSpec\Exception\Locator\ResourceCreationException] Can not find appropriate suite scope for class Bar.

What is the best-practice method for testing the return value being an optional class like this?


Solution

  • Move the logic that selects the type of $source into an external factory function.

    class Foo {
    
      protected $source;
    
      public function __construct($source = []) {
        $this->source = $source;
      }
    
    }
    
    class FooFactory {
    
      public get() {
        $source = [];
        if (class_exists(Bar::class)) {
          $source = new Bar($source);
        }
    
        return new Foo($source);
      }
    
    }
    

    This allows you to explicitly test Foo both ways and decouples the implementation by moving everything to a dependency injected model.