27

単体テストの方法がわからないコードがあります。このモジュールは、urllib2を使用して外部XMLフィード(twitter、flickr、youtubeなど)からコンテンツをプルします。これがそのためのいくつかの擬似コードです:

params = (url, urlencode(data),) if data else (url,)
req = Request(*params)
response = urlopen(req)
#check headers, content-length, etc...
#parse the response XML with lxml...

私の最初の考えは、応答をピクルスにしてテスト用にロードすることでしたが、どうやらurllibの応答オブジェクトはシリアル化できません(例外が発生します)。

私のコードはヘッダー情報も使用しているため、応答本文からXMLを保存するだけでは理想的ではありません。応答オブジェクトに作用するように設計されています。

そしてもちろん、単体テストでデータを外部ソースに依存することは恐ろしい考えです。

では、このための単体テストを作成するにはどうすればよいですか?

4

7 に答える 7

25

urllib2 には と呼ばれる関数がbuild_opener()あり、install_opener()これを使用して の動作をモックする必要があります。urlopen()

import urllib2
from StringIO import StringIO

def mock_response(req):
    if req.get_full_url() == "http://example.com":
        resp = urllib2.addinfourl(StringIO("mock file"), "mock message", req.get_full_url())
        resp.code = 200
        resp.msg = "OK"
        return resp

class MyHTTPHandler(urllib2.HTTPHandler):
    def http_open(self, req):
        print "mock opener"
        return mock_response(req)

my_opener = urllib2.build_opener(MyHTTPHandler)
urllib2.install_opener(my_opener)

response=urllib2.urlopen("http://example.com")
print response.read()
print response.code
print response.msg
于 2010-02-16T22:30:06.127 に答える
9

urllib2 のバージョンのように動作するために最低限必要なインターフェイスを提供するモック urlopen (および場合によっては Request) を記述できれば最高です。次に、それを使用する関数/メソッドがこのモック urlopen を何らかの形で受け入れ、それ以外の場合に使用できるようにする必要がありますurllib2.urlopen

これはかなりの量の作業ですが、やりがいがあります。Python はダックタイピングに非常に適しているため、応答オブジェクトのプロパティの類似性を提供してモックを作成するだけでよいことに注意してください。

例えば:

class MockResponse(object):
    def __init__(self, resp_data, code=200, msg='OK'):
        self.resp_data = resp_data
        self.code = code
        self.msg = msg
        self.headers = {'content-type': 'text/xml; charset=utf-8'}

    def read(self):
        return self.resp_data

    def getcode(self):
        return self.code

    # Define other members and properties you want

def mock_urlopen(request):
    return MockResponse(r'<xml document>')

たとえば、通常の「ヘッダー」は、大文字と小文字を区別しないヘッダー名などの楽しいものを実装する HTTPMessage であると私は信じているためです。ただし、応答データを使用して HTTPMessage を単純に構築できる場合があります。

于 2010-02-16T22:08:52.643 に答える
6

外部フィードとの通信を担当する別のクラスまたはモジュールを構築します。

このクラスをテスト doubleにできるようにします。あなたはPythonを使用しているので、そこはかなり金色です。C# を使用している場合は、インターフェイスまたは仮想メソッドのいずれかをお勧めします。

単体テストで、外部フィード クラスのテスト ダブルを挿入します。クラスが外部リソースとの通信を正しく行うと仮定して、コードがクラスを正しく使用していることをテストします。テストで実際のデータではなく偽のデータを返すようにします。データのさまざまな組み合わせと、もちろん urllib2 がスローする可能性のある例外をテストします。

そして...それだけです。

外部ソースに依存する単体テストを効果的に自動化することはできないため、自動化しないことをお勧めします。通信モジュールでときどき統合テストを実行しますが、それらのテストを自動テストの一部として含めないでください。

編集:

私の答えと@Crastの答えの違いについてだけメモしてください。どちらも基本的には正しいですが、アプローチが異なります。Crast のアプローチでは、ライブラリ自体でテスト ダブルを使用します。私のアプローチでは、ライブラリの使用を別のモジュールに抽象化し、そのモジュールを 2 倍にしてテストします。

どのアプローチを使用するかは、完全に主観的なものです。そこに「正解」はありません。よりモジュラーで柔軟なコードを構築できるので、私は自分のアプローチを好みます。しかし、多くのアジャイルな状況では評価されないかもしれない追加のコードを書くという点でコストがかかります。

于 2010-02-16T22:09:20.100 に答える
5

pymoxを使用して、urllib2 (またはその他の) パッケージ内のすべての動作をモックできます。今は 2010 年です。独自のモック クラスを作成するべきではありません。

于 2010-02-16T22:38:02.707 に答える
1

最も簡単な方法は、単体テストで実際に単純な Web サーバーを作成することだと思います。テストを開始するときは、任意のポートでリッスンする新しいスレッドを作成します。クライアントが接続すると、ヘッダーと XML の既知のセットが返され、終了します。

さらに情報が必要な場合は、詳しく説明できます。

ここにいくつかのコードがあります:

import threading, SocketServer, time

# a request handler
class SimpleRequestHandler(SocketServer.BaseRequestHandler):
    def handle(self):
        data = self.request.recv(102400) # token receive
        senddata = file(self.server.datafile).read() # read data from unit test file
        self.request.send(senddata)
        time.sleep(0.1) # make sure it finishes receiving request before closing
        self.request.close()

def serve_data(datafile):
    server = SocketServer.TCPServer(('127.0.0.1', 12345), SimpleRequestHandler)
    server.datafile = datafile
    http_server_thread = threading.Thread(target=server.handle_request())

単体テストを実行するにはserve_data()、次のような URL を要求するコードを呼び出しますhttp://localhost:12345/anythingyouwant

于 2010-02-16T22:24:32.700 に答える
0

@john-la-rooyの回答を少し改善しようとして、単体テストの簡単なモックを可能にする小さなクラスを作成しました

Python 2 および 3 で動作するはずです

try:
    import urllib.request as urllib
except ImportError:
    import urllib2 as urllib

from io import BytesIO


class MockHTTPHandler(urllib.HTTPHandler):

    def mock_response(self, req):
        url = req.get_full_url()

        print("incomming request:", url)

        if url.endswith('.json'):
            resdata = b'[{"hello": "world"}]'
            headers = {'Content-Type': 'application/json'}

            resp = urllib.addinfourl(BytesIO(resdata), header, url, 200)
            resp.msg = "OK"

            return resp
        raise RuntimeError('Unhandled URL', url)
    http_open = mock_response


    @classmethod
    def install(cls):
        previous = urllib._opener
        urllib.install_opener(urllib.build_opener(cls))
        return previous

    @classmethod
    def remove(cls, previous=None):
        urllib.install_opener(previous)

次のように使用します。

class TestOther(unittest.TestCase):

    def setUp(self):
        previous = MockHTTPHandler.install()
        self.addCleanup(MockHTTPHandler.remove, previous)
于 2016-12-31T15:33:30.600 に答える
0

期待どおりの応答を返すWeb サイトをモックしてみませんか? 次に、セットアップのスレッドでサーバーを起動し、ティアダウンでサーバーを強制終了します。私は、smtpサーバーをモックして電子メールを送信するコードをテストするためにこれを行うことになり、うまく機能しました。確かに、httpに対してもっと些細なことを行うことができます...

from smtpd import SMTPServer
from time import sleep
import asyncore
SMTP_PORT = 6544

class MockSMTPServer(SMTPServer):
    def __init__(self, localaddr, remoteaddr, cb = None):
        self.cb = cb
        SMTPServer.__init__(self, localaddr, remoteaddr)

    def process_message(self, peer, mailfrom, rcpttos, data):
        print (peer, mailfrom, rcpttos, data)
        if self.cb:
            self.cb(peer, mailfrom, rcpttos, data)
        self.close()

def start_smtp(cb, port=SMTP_PORT):

    def smtp_thread():
        _smtp = MockSMTPServer(("127.0.0.1", port), (None, 0), cb)
        asyncore.loop()
        return Thread(None, smtp_thread)


def test_stuff():
        #.......snip noise
        email_result = None

        def email_back(*args):
            email_result = args

        t = start_smtp(email_back)
        t.start()
        sleep(1)

        res.form["email"]= self.admin_email
        res = res.form.submit()
        assert res.status_int == 302,"should've redirected"


        sleep(1)
        assert email_result is not None, "didn't get an email"
于 2010-02-16T22:33:46.377 に答える