1

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!

Saeven
  • 2,280
  • 1
  • 20
  • 33

1 Answers1

1

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);
}
Pete Mitchell
  • 2,879
  • 1
  • 16
  • 22
  • Pete, thanks very much for having taken the time to elaborate in such detail. The current mesh is a function of the several packagist components I was using, modifying them as such would have a ripple effect, but I can see the benefit. I guess I'll have to evaluate the business case for rewriting these items for testability. Thanks very much! – Saeven Jan 23 '16 at 02:53