我有以下方法,可以从Google Analytics中检索热门访问过的网页:
public function getData($limit = 10)
{
$ids = '12345';
$dateFrom = '2011-01-01';
$dateTo = date('Y-m-d');
// Google Analytics credentials
$mail = 'my_mail';
$pass = 'my_pass';
$clientLogin = Zend_Gdata_ClientLogin::getHttpClient($mail, $pass, "analytics");
$client = new Zend_Gdata($clientLogin);
$reportURL = 'https://www.google.com/analytics/feeds/data?';
$params = array(
'ids' => 'ga:' . $ids,
'dimensions' => 'ga:pagePath,ga:pageTitle',
'metrics' => 'ga:visitors',
'sort' => '-ga:visitors',
'start-date' => $dateFrom,
'end-date' => $dateTo,
'max-results' => $limit
);
$query = http_build_query($params, '');
$reportURL .= $query;
$results = $client->getFeed($reportURL);
$xml = $results->getXML();
Zend_Feed::lookupNamespace('default');
$feed = new Zend_Feed_Atom(null, $xml);
$top = array();
foreach ($feed as $entry) {
$page['visitors'] = (int) $entry->metric->getDOM()->getAttribute('value');
$page['url'] = $entry->dimension[0]->getDOM()->getAttribute('value');
$page['title'] = $entry->dimension[1]->getDOM()->getAttribute('value');
$top[] = $page;
}
return $top;
}
肯定需要一些重构,但问题是:
答案 0 :(得分:6)
据我了解,通常您会希望将依赖项(Google客户端对象)注入受测系统(SUT,包含getData()
方法的类)。
我总是看到专家使用构造函数注入 - 我确信这是一种更好的方法,因为它可以在前面清楚地识别依赖关系。但是,说实话,我似乎永远不能设计好我的物品,以便始终做到这一点。所以我最终做了setter注入。
这样的事情:
public function getClient()
{
if (null === $this->_client){
// $mail and $pass are stored somewhere, right?
$clientLogin = Zend_Gdata_ClientLogin::getHttpClient($mail, $pass, "analytics");
$this->_client = new Zend_Gdata($clientLogin);
}
return $this->_client;
}
public function setClient($client)
{
$this->_client = $client;
return $this;
}
然后在单元测试中,您创建一个$client
对象作为您的实时$client
的模拟,设置期望值,然后使用setClient($client)
方法将其注入您的SUT如上所述。
明白我的意思?
答案 1 :(得分:2)
David Weinraub给你上半部分(如何设置你的类可以模拟),所以我将解决下半部分(如何构建模拟)。
PHPUnit提供了一个很棒的模拟工具和一个简单的API。传递用户和密码太简单了,无法在我的书中测试,所以我只是模拟查询和结果的处理。这需要Zend_Gdata和Zend_Gdata_App_Feed的模拟。
public function testGetData() {
// expected input to and output from mocks
$url = 'https://www.google.com/analytics/feeds/data?ids=ga:12345...';
$xml = <<<XML
<feed>
...
</feed>
XML;
// setup the mocks and method expectations
$client = $this->getMock('Zend_Gdata', array('getFeed'));
$feed = $this->getMock('Zend_Gdata_App_Feed', array('getXML'));
$client->expects($this->once())
->method('getFeed')
->with($url)
->will($this->returnValue($feed));
$feed->expects($this->once())
->method('getXML')
->will($this->returnValue($xml));
// create the report (SUT) and call the method being tested
$report = new MyReport();
$report->setClient($client);
$top = $report->getData();
// check the final output; mocks are verified automatically
$this->assertEquals(10, count($top));
$this->assertEquals(array(
'visitors' => 123,
'url' => 'http://...',
'title' => 'My Home Page'
), $top[0]);
}
以上内容将测试网址是否正确并返回Google预期的XML Feed。它消除了对Zend_Gdata类的所有依赖。如果你不在setClient()上使用类型提示,你甚至可以使用stdClass作为两个模拟的基础,因为你只使用模拟方法。
答案 2 :(得分:-1)
我的第一个倾向是告诉你,这一个函数getData
是最讨厌和最丑陋的代码之一。你问的是如何对它进行单元测试。那么猜猜我的推荐是什么?重构。
为了重构此代码,您需要覆盖率测试。
重构的原因很多:
getData
责任太多了。
一个。使用第三方框架登录外部服务。
湾创建外部服务查询。
℃。解析来自外部服务的查询响应。
您是如何将代码与第三方框架和外部服务的更改隔离开来的?
你真的应该看看Michael Feather的书。 Working Effectively with Legacy Code
<强> [编辑] 强>
我对你的观点(扰乱者来了),是这个代码,你永远不会得到真正的单元测试。这是因为依赖外部服务。单元测试无法控制服务或返回的数据。单元测试应该能够执行,以便每次执行时它的结果都是一致的。对于外部服务,情况可能并非如此。 您无法控制外部服务返回的内容。
如果服务中断,你会怎么做?单元测试 FAIL 。
如果结果返回怎么办?单元测试 FAIL 。
单元测试结果必须在执行之间保持一致。否则它不是单元测试。