34

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

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

4

1 に答える 1

75

単体テストのために Web サーバーを起動することは、間違いなく良い方法ではありません。単体テストは単純で分離されている必要があります。つまり、たとえば IO 操作の実行を避ける必要があります。

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

簡単な単体テスト

したがって、あなたの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)    

依存性注入によるモック

ここで、クラスのgetContent()メソッドの単体テストについては、それほど明白ではありません。core.HttpRequestsHTTP ライブラリを使用していて、TCP ソケット上で独自のリクエストを行っていないと思います。

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

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

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 ライブラリを使用することにしたとします。その API はより手続き型であり、HTTP 要求を作成するために取得できるオブジェクトを提示しません。代わりに、モジュールをインポートしてそのgetメソッドを呼び出します。

HttpRequestsのクラスcore.pyは次のようになります。

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モジュールが適切に使用されていることをテストするにはどうすればよいですか?

ここで、動的言語が提供するもう 1 つの素晴らしい、しかし物議をかもしているメカニズム、モンキー パッチを使用できます。実行時に、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 は優れたプラクティスであり、オブジェクトのモックは優れたツールです。

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

PS

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

于 2012-07-09T16:25:18.550 に答える