如何对 Google API 调用进行单元测试

发布于 2024-10-15 05:48:28 字数 1453 浏览 1 评论 0原文

我有一个以下方法,它从 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;
}

它肯定需要一些重构,但问题是:

  • 您将如何为此方法编写 PHPUnit 测试?

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?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

漫雪独思 2024-10-22 05:48:28

据我了解,通常您希望将依赖项(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。

明白我的意思吗?

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:

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

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 the setClient($client) method described above.

See what I mean?

萌辣 2024-10-22 05:48:28

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

上面的代码将测试 URL 是否正确并返回 Google 期望的 XML 提要。它消除了对 Zend_Gdata 类的所有依赖。如果您不在 setClient() 上使用类型提示,您甚至可以使用 stdClass 作为两个模拟的基础,因为您将只使用模拟方法。

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.

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

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.

硬不硬你别怂 2024-10-22 05:48:28

我的第一个想法是告诉您,这个函数 getData 是最令人讨厌和最丑陋的代码之一。您问的是如何对其进行单元测试。那么猜猜我的建议是什么?重构。

为了重构此代码,您将需要覆盖率测试

重构的原因有很多:

  1. 对第三方框架的依赖。
  2. 对外部服务的依赖。
  3. getData 的职责太多。

    a.使用第三方框架登录外部服务。

    b.创建外部服务的查询。

    c.解析来自外部服务的查询响应。

您如何将代码与第三方框架和外部服务的更改隔离开来?

你真的应该看看迈克尔·费瑟的书。 有效使用旧代码

[编辑]

我对你的观点(即将剧透)是,使用这段代码你永远无法获得真正的单元测试。这是因为对外部服务的依赖。单元测试无法控制服务或其返回的数据。单元测试应该能够执行,使得每次执行的结果都是一致的。对于外部服务,情况可能并非如此。 您无法控制外部服务返回的内容。

如果服务出现故障,您该怎么办?单元测试失败

如果返回的结果发生变化怎么办?单元测试失败

单元测试结果在执行之间必须保持一致。否则它不是单元测试。

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:

  1. Dependency on third-party framework.
  2. Dependency on external service.
  3. 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.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文