web-dev-qa-db-ja.com

Pythonの単体テストシナリオでHTTPリクエストをモックする方法

HTTPに関連するすべてのテストにWebサーバーを含めたいと思います。それほど洗練されている必要はありません。私はオンラインに依存することを望まないでしょう。したがって、プログラムのいくつかのオプションをテストできました。

  1. サーバーを起動します
  2. 適切なMIMEタイプ、応答コードなどを使用して、いくつかのリソース(URI)を作成します。
  3. テストを実行します(各テストでもサーバーを起動する必要がないのは良いことです)
  4. サーバーをシャットダウンします。

このコードに関するヒントは役立ちます。 BaseHTTPServerでいくつか試してみましたが、まだ成功していません。 nosetestsコマンドは無期限に待機するようです。

import unittest
from foo import core

class HttpRequests(unittest.TestCase):
    """Tests for HTTP"""

    def setUp(self):
        "Starting a Web server"
        self.port = 8080
        # Here we need to start the server
        #
        # Then define a couple of URIs and their HTTP headers
        # so we can test the code.
        pass

    def testRequestStyle(self):
        "Check if we receive a text/css content-type"
        myreq = core.httpCheck()
        myuri = 'http://127.0.0.1/style/foo'
        myua = "Foobar/1.1"
        self.asserEqual(myreq.mimetype(myuri, myua), "text/css")

    def testRequestLocation(self):
        "another test" 
        pass

    def tearDown(self):
        "Shutting down the Web server"
        # here we need to shut down the server
        pass

助けてくれてありがとう。


更新-2012:07:10T02:34:00Z

これは、特定のWebサイトに対してCSSのリストを返すコードです。 CSSの正しいリストが返されるかどうかをテストします。

import unittest
from foo import core

class CssTests(unittest.TestCase):
    """Tests for CSS requests"""

    def setUp(self):
        self.css = core.Css()
        self.req = core.HttpRequests()

    def testCssList(self):
        "For a given Web site, check if we get the right list of linked stylesheets"
        WebSiteUri = 'http://www.opera.com/'
        cssUriList = [
        'http://www.opera.com/css/handheld.css',
        'http://www.opera.com/css/screen.css',
        'http://www.opera.com/css/print.css',
        'http://www.opera.com/css/pages/home.css']
        content = self.req.getContent(WebSiteUri)
        cssUriListReq = self.css.getCssUriList(content, WebSiteUri)
        # we need to compare ordered list.
        cssUriListReq.sort()
        cssUriList.sort()
        self.assertListEqual(cssUriListReq, cssUriList)

その後、foo/core.py

import urlparse
import requests
from lxml import etree
import cssutils

class Css:
    """Grabing All CSS for one given URI"""


    def getCssUriList(self, htmltext, uri):
        """Given an htmltext, get the list of linked CSS"""
        tree = etree.HTML(htmltext)
        sheets = tree.xpath('//link[@rel="stylesheet"]/@href')
        for i, sheet in enumerate(sheets):
            cssurl = urlparse.urljoin(uri, sheet)
            sheets[i] = cssurl
        return sheets

現在、コードはオンラインサーバーに依存しています。すべきではありません。スタイルシートのさまざまな種類の組み合わせをたくさん追加し、プロトコルをテストしてから、解析、組み合わせなどのオプションを後で実行できるようにしたい.

33
karlcow

単体テストのためにWebサーバーを起動することは、間違いなく良い習慣ではありません。単体テストは単純で独立している必要があります。つまり、IO操作を実行しないようにする必要があります。

書きたいものが本当に単体テストである場合は、独自のテスト入力を作成し、 モックオブジェクト も調べる必要があります。 Pythonは動的言語であるため、モックとモンキーパスはユニットテストを書くための簡単で強力なツールです。特に、優れた モックモジュール をご覧ください。

簡単な単体テスト

したがって、CssTestsの例を見ると、_css.getCssUriList_が、与えたHTMLで参照されているすべてのCSSスタイルシートを抽出できることをテストしようとしています。この特定の単体テストで行うことは、リクエストを送信し、Webサイトから応答を取得できることをテストすることではありませんか? HTMLが与えられたときに、関数がCSS URLの正しいリストを返すことを確認したいだけです。したがって、このテストでは、明らかに実際のHTTPサーバーと通信する必要はありません。

私は次のようなことをします:

_import unittest

class CssListTestCase(unittest.TestCase):

    def setUp(self):
        self.css = core.Css()

    def test_css_list_should_return_css_url_list_from_html(self):
        # Setup your test
        sample_html = """
        <html>
            <head>
                <title>Some web page</title>
                <link rel='stylesheet' type='text/css' media='screen'
                      href='http://example.com/styles/full_url_style.css' />
                <link rel='stylesheet' type='text/css' media='screen'
                      href='/styles/relative_url_style.css' />
            </head>
            <body><div>This is a div</div></body>
        </html>
        """
        base_url = "http://example.com/"

        # Exercise your System Under Test (SUT)
        css_urls = self.css.get_css_uri_list(sample_html, base_url)

        # Verify the output
        expected_urls = [
            "http://example.com/styles/full_url_style.css",
            "http://example.com/styles/relative_url_style.css"
        ]
        self.assertListEqual(expected_urls, css_urls)    
_

依存性注入によるモック

さて、それほど明白ではないものは、_core.HttpRequests_クラスのgetContent()メソッドを単体テストすることです。 HTTPライブラリを使用しており、TCPソケットの上で独自の要求を行っていないと思います。

テストをunitレベルで維持するために、ネットワーク経由で何も送信したくありません。それを避けるためにできることは、HTTPライブラリを正しく使用することを確認するテストを行うことです。これは、コードの動作ではなく、コードが他のオブジェクトと相互作用する方法をテストすることに関するものです。

そのための1つの方法は、そのライブラリへの依存関係を明示的にすることです。パラメータを_HttpRequests.__init___に追加して、ライブラリのHTTPクライアントのインスタンスを渡すことができます。 get()を呼び出すことができるHttpClientオブジェクトを提供するHTTPライブラリを使用するとします。次のようなことができます:

_class HttpRequests(object):

    def __init__(self, http_client):
        self.http_client = http_client

   def get_content(self, url):
        # You could imagine doing more complicated stuff here, like checking the
        # response code, or wrapping your library exceptions or whatever
        return self.http_client.get(url)
_

依存関係を明示的にしたので、要求はHttpRequestsの呼び出し元が満たす必要があります。これは依存性注入(DI)と呼ばれます。

DIは次の2つのことに非常に役立ちます。

  1. コードがどこかに存在するオブジェクトに密かに依存しているという驚きを避ける
  2. テストの目的に応じて異なる種類のオブジェクトを注入するテストを作成できます

ここでは、_core.HttpRequests_に与えるモックオブジェクトを使用できます。モックオブジェクトは、知らないうちに実際のライブラリであるかのように使用します。その後、相互作用が期待どおりに行われたことをテストできます。

_import core

class HttpRequestsTestCase(unittest.TestCase):

    def test_get_content_should_use_get_properly(self):
        # Setup

        url = "http://example.com"

        # We create an object that is not a real HttpClient but that will have
        # the same interface (see the `spec` argument). This mock object will
        # also have some Nice methods and attributes to help us test how it was used.
        mock_http_client = Mock(spec=somehttplib.HttpClient) 

        # Exercise

        http_requests = core.HttpRequests(mock_http_client)
        content = http_requests.get_content(url)

        # Here, the `http_client` attribute of `http_requests` is the mock object we
        # have passed it, so the method that is called is `mock.get()`, and the call
        # stops in the mock framework, without a real HTTP request being sent.

        # Verify

        # We expect our get_content method to have called our http library.
        # Let's check!
        mock_http_client.get.assert_called_with(url)

        # We can find out what our mock object has returned when get() was
        # called on it
        expected_content = mock_http_client.get.return_value
        # Since our get_content returns the same result without modification,
        # we should have received it
        self.assertEqual(content, expected_content)
_

_get_content_メソッドがHTTPライブラリと正しく相互作用することをテストしました。 HttpRequestsオブジェクトの境界を定義してテストしましたが、これは単体テストレベルで行うべき範囲です。現在、リクエストはそのライブラリの手にあり、ライブラリが期待どおりに動作することをテストすることは、ユニットテストスイートの役割ではありません。

モンキーパッチ

ここで、すばらしい requests library を使用することにしたと想像してください。そのAPIはより手続き的であり、HTTP要求を作成するために取得できるオブジェクトを提示しません。代わりに、モジュールをインポートし、そのgetメソッドを呼び出します。

_core.py_のHttpRequestsクラスは、次のようになります。

_import requests

class HttpRequests(object):

    # No more DI in __init__

    def get_content(self, url):
        # We simply delegate the HTTP work to the `requests` module
        return requests.get(url)
_

もうDIはないので、今、私たちは不思議に思っています:

  • ネットワークの相互作用が起こらないようにするにはどうすればよいですか?
  • requestsモジュールを適切に使用していることをテストするにはどうすればよいですか?

ここで、動的言語が提供する別の素晴らしい、しかし議論の余地のあるメカニズムを使用できます: monkey patching 。実行時に、requestsモジュールを、作成してテストで使用できるオブジェクトに置き換えます。

ユニットテストは次のようになります。

_import core

class HttpRequestsTestCase(unittest.TestCase):

    def setUp(self):
        # We create a mock to replace the `requests` module
        self.mock_requests = Mock()

        # We keep a reference to the current, real, module
        self.old_requests = core.requests

        # We replace the module with our mock
        core.requests = self.mock_requests

    def tearDown(self):
        # It is very important that each unit test be isolated, so we need
        # to be good citizen and clean up after ourselves. This means that
        # we need to put back the correct `requests` module where it was
        core.requests = self.old_requests

    def test_get_content_should_use_get_properly(self):
        # Setup

        url = "http://example.com"

        # Exercise
        http_client = core.HttpRequests()
        content = http_client.get_content(url)

        # Verify

        # We expect our get_content method to have called our http library.
        # Let's check!
        self.mock_requests.get.assert_called_with(url)

        # We can find out what our mock object has returned when get() was
        # called on it
        expected_content = self.mock_requests.get.return_value
        # Since our get_content returns the same result without modification,
        # we should have received
        self.assertEqual(content, expected_content)
_

このプロセスの冗長性を軽減するために、mockモジュールには、足場を管理するpatchデコレーターがあります。次に書く必要があるのは:

_import core

class HttpRequestsTestCase(unittest.TestCase):

    @patch("core.requests")
    def test_get_content_should_use_get_properly(self, mock_requests):
        # Notice the extra param in the test. This is the instance of `Mock` that the
        # decorator has substituted for us and it is populated automatically.

        ...

        # The param is now the object we need to make our assertions against
        expected_content = mock_requests.get.return_value
_

結論

単体テストを小さく、シンプルで、高速で、自己完結型に保つことは非常に重要です。実行中の別のサーバーに依存する単体テストは、単体テストではありません。これを支援するために、DIは優れたプラクティスであり、モックオブジェクトは優れたツールです。

最初は、モックの概念とその使用方法を理解するのは簡単ではありません。すべての電動工具と同様に、それらはあなたの手で爆発する可能性があり、たとえば、実際にはテストしていないときに何かをテストしたと信じさせることができます。モックオブジェクトの動作と入力/出力が現実を反映していることを確認することが重要です。

追伸.

単体テストレベルで実際のHTTPサーバーとやり取りしたことがないことを考えると、アプリケーションが実際に処理する種類のサーバーと通信できることを確認する統合テストを作成することが重要です。統合テスト用に特別に設定された本格的なサーバーでこれを行うか、不自然なサーバーを作成します。

71
Rodrigue