Search code examples
phpphpspec

How can I test the type of something generated in my phpspec test?


For example:

Test code

function it_records_last_checked()
{
    $this->getWrappedObject()->setServiceLocator( $this->getServiceLocator() );
    $this->isAvailable( 'google.com' )->shouldReturn( false );

    /** @var Url $last */
    $last = $this->getLastChecked();
    $last->shoudHaveType( Url::class );
    $last->host->registrableDomain->shouldBeLike('google.com');
}

The spec wraps an object whose code is this:

namespace Application\Service;

use Application\Exception\DomainInvalidException;
use Application\Model\Whois;
use Pdp\Uri\Url;
use Zend\ServiceManager\ServiceLocatorAwareInterface;
use Zend\ServiceManager\ServiceLocatorAwareTrait;
use Application\Exception\DomainRequiredException;

class DomainService implements ServiceLocatorAwareInterface{
    use ServiceLocatorAwareTrait;

    /** @var  Url */
    protected $last_checked;


    /**
     * @return Url
     */
    public function getLastChecked()
    {
        return $this->last_checked;
    }

    /**
     * @param Url $last_checked
     */
    public function setLastChecked( $last_checked )
    {
        $this->last_checked = $last_checked;
    }


    /**
     * Use available configuration to determine if a domain is available
     * @param $domain
     * @return bool
     * @throws DomainRequiredException
     * @throws \Exception
     */
    public function isAvailable($domain)
    {
        if( !$domain )
            throw new DomainRequiredException();

        $pslManager = new \Pdp\PublicSuffixListManager();
        $parser     = new \Pdp\Parser($pslManager->getList());
        $host       = 'http://' . $domain;

        if( !$parser->isSuffixValid( $host ) )
            throw new DomainInvalidException();

        $this->last_checked = $parser->parseUrl($host);
        $whois = new Whois($this->last_checked->host->registerableDomain);

        return $whois->isAvailable();
    }
}

The service sets its last_checked member whose type I want to test for example. It seems that it doesn't return a wrapped object, it returns the actual Pdp\Uri\Url instance.

What's the rule in writing tests, to ensure that we get wrapped objects back (Subject)?

Thanks!


Solution

  • The difficulty you are finding in testing this logic is PhpSpec trying to push you to a different design. Your test is validating and reliant on the behaviour/structure of 6/7 other objects making it more of an integration test rather than a unit test (doing this is intentionally difficult in PhpSpec)

    I have highlighted some of these dependencies:

    <?php
    public function isAvailable($domain)
    {
        // Pdp\Parser instantiation and configuration
        $pslManager = new \Pdp\PublicSuffixListManager();
        $parser     = new \Pdp\Parser($pslManager->getList());
    
        // Validation and parsing of $domain into an Url object
        if( !$domain ) {
            throw new DomainRequiredException();
        }
    
        $host = 'http://' . $domain;
    
        if( !$parser->isSuffixValid( $host ) ) {
            throw new DomainInvalidException();
        }
    
        $this->last_checked = $parser->parseUrl($host);
    
        // The "isAvailable" check
        // This depends on `Pdp\Uri\Url\Host` (in addition to Whois and `Pdp\Uri\Url`
        $whois = new Whois($this->last_checked->host->registerableDomain);
    
        return $whois->isAvailable();
    }
    

    By moving the configuration/instantiation of the Pdp classes, and splitting the validation/parsing logic from the Whois check you quickly arrive at something that is a bit more testable (but with a less convenient API)

    public function __construct(\Pdp\Parser $parser)
    {
        $this->parser = $parser;
    }
    
    public function parseDomain($domain)
    {
        if( !$domain ) {
            throw new DomainRequiredException();
        }
    
        $host = 'http://' . $domain;
    
        if( !$parser->isSuffixValid( $host ) )
            throw new DomainInvalidException();
    
        return $parser->parseUrl($host);
    }
    
    public function isAvailable(Url $domain)
    {
        $whois = new Whois($domain->host->registerableDomain);
    
        return $whois->isAvailable();
    }
    

    But by making Whois capable of checking if your Url object is available, and injecting it testing gets even easier

    class DomainParser
    {
        // Pdp\Parser should be registered as a service
        public function __construct(\Pdp\Parser $parser)
        {
            $this->parser = $parser;
        }
    
        public function parseDomain($domain)
        {
            if( !$domain ) {
                throw new DomainRequiredException();
            }
    
            $host = 'http://' . $domain;
    
            if( !$parser->isSuffixValid( $host ) )
                throw new DomainInvalidException();
    
            return $parser->parseUrl($host);
        }
    }
    
    class Whois
    {
        public function isUrlAvailable(Url $url)
        {
            // Whois logic
        }
    }
    
    class DomainService
    {
        public function __construct(DomainParser $parser, Whois $whois)
        {
            $this->parser = $parser;
            $this->whois = $whois;
        }
    
        public function isAvailable($domain)
        {
            $url = $this->parser->parseDomain($domain);
    
            $this->last_checked = $url;
    
            return $this->whois->isUrlAvailable($url);
        }
    }
    

    With these three classes, it is easy to unit test DomainService and DomainParser, Whois can be mocked and tested using another strategy (assuming it communicates with a third party system)

    e.g.

    function let(DomainParser $parser, Whois $whois)
    {
        $this->beConstructedWith($parser, $whois);
    }
    
    function it_shows_a_domain_is_available(
        DomainParser $parser,
        Whois $whois,
        Url $url
    ) {
        $parser->parseDomain('http://test.com')->willReturn($url);
        $whois->isUrlAvailable($url)->willReturn(true);
    
        $this->isAvailable('http://test.com')->shouldReturn(true);
    }
    
    function it_records_last_checked(
        DomainParser $parser,
        Whois $whois,
        Url $url
    ) {
        $parser->parseDomain('http://test.com')->willReturn($url);
        $whois->isUrlAvailable($url)->willReturn(true);
    
        $this->isAvailable('http://test.com');
    
        // Note that we don't validate any properties on Url, that is the
        // responsibility of the tests for DomainParser and the Url object itself
        $this->getLastChecked()->shouldReturn($url);
    }