単体テストのために 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.HttpRequests
HTTP ライブラリを使用していて、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 つの点で非常に役立ちます。
- コードがどこかに存在するオブジェクトに密かに依存しているという驚きを回避します
- そのテストの目的に応じて、さまざまな種類のオブジェクトを挿入するテストを作成できます
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 サーバーと対話したことがないことを考えると、アプリケーションが実際に処理する種類のサーバーと通信できることを確認する統合テストを作成することが重要です。統合テスト用に特別にセットアップされた本格的なサーバーでこれを行うか、不自然なサーバーを作成することができます。