I have a following method, which retrieves top visited pages from 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;
}
It needs some refactoring for sure, but the question is:
- How would you write PHPUnit tests for this method?
David Weinraub gave you the first half (how to set up your class to be mockable), so I'll address the second half (how to build the mock).
PHPUnit provides a great mocking facility with a simple API. Passing the user and password is too simple to test in my book, so I'd mock just the handling of the query and results. This requires mocks for Zend_Gdata and Zend_Gdata_App_Feed.
The above will test that the URL was correct and return the XML feed expected from Google. It removes all dependence on the Zend_Gdata classes. If you don't use type hinting on setClient(), you can even use stdClass as the base for the two mocks since you will only be using mocked methods.
As I understand it, typically you would want to inject the dependency (the Google client object) into the System Under Test (SUT, the class containing the
getData()
method).I always see the experts use constructor injection - and I'm sure it's a better approach as it clearly identifies the dependencies right up front. But, to tell the truth, I can never seem to design my objects well enough to always make that work. So I end up doing with setter injection.
Something like this:
Then in the unit test, you create a
$client
object as a mock of your live$client
, setting up the expectations, and then inject it into your SUT using thesetClient($client)
method described above.See what I mean?
My first inclination is to tell you that this one function
getData
is one of the most nasty and ugliest piece of code. You are asking how to unit test this. Well guess what my recommendation is going to be? Refactor.In order to refactor this code, you will need a coverage test.
The reasons for refactoring are many:
getData
has too many responsibilites.a. Login in to external service using third-party framework.
b. Create query for external service.
c. Parse query response from external service.
How have you isolated your code from changes to either third-party framework and from external service?
You really should take a look at Michael Feather's Book. Working Effectively with Legacy Code
[EDIT]
My point to you (spoiler coming), is that with this code you can never get a true unit test. It is because of the dependency on external service. The unit test has no control over the service or the data it returns. A unit test should be able to execute such that every time it executes it's outcome is consistent. With an external service this may not be the case. YOU HAVE NO CONTROL OVER WHAT THE EXTERNAL SERVICE RETURNS.
What do you do if the service is down? Unit test FAIL.
What if the results are returned changes? Unit test FAIL.
Unit tests results must remain consistent from execution to execution. Otherwise it is not a unit test.