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!
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);
}