OpenStack 的單元測試

目錄

前言

在 OpenStack 官方指導手冊(https://wiki.openstack.org/wiki/TestGuide)中明確的將 OpenStack 測試分爲小型測試(單元測試)、中型測試(功能測試)以及大型測試(集成測試),在本文中咱們關注的是單元測試(https://wiki.openstack.org/wiki/SmallTestingGuide)。html

Small Tests are the tests that most developer read, write, and run with the greatest frequency. Small Tests are bundled with the source code, can be executed in any environment, and run extremely fast. Small tests cover the codebase in as fine a granularity as possible in order to make it very easy to locate problems when tests fail.python

單元測試能提升生產率

在這裏插入圖片描述
單元測試是與源代碼(測試單元)捆綁最爲緊密的測試方法,若是測試用例不經過,能夠迅速定位出問題所在。上圖清晰明瞭的說明了單元測試(UNIT TESTS)之於生產效率(PRODUCTIVITY)的關係。請記住,單元測試雖然要編寫更多的代碼,但卻能爲團隊節省更多的生產資源。由於你不清楚何時的小提交會致使全局性的邏輯錯亂,而處理這樣的問題每每須要花費昂貴的溝通成本,在開源社區的協做場景中尤甚。ios

Python 單元測試工具清單

  • unittest:是 Python 的標準單元測試庫,提供了最基本的單元測試框架和單元測試運行器。
  • mock:在 Python 3.x 中做爲一個模塊被內嵌到 unittest 標準庫。簡單的說,mock 就是製造假數據(對象)的模塊,以此來模擬多種代碼運行的情景,而無需真的發生了這種情景。
  • fixtures:第三方模塊,對 unittest 的 Test Fixture 機制進行了加強,有效提升了測試代碼的複用率。
  • testtools:第三方模塊,是 unittest 的擴展,對 unittest 進行了斷言之類的功能加強,讓測試代碼的編寫更加方便。
  • testscenarios:第三方模塊,用於知足了 「場景測試」 的需求,是節省重複代碼的有效手段。
  • subunit:第三方模塊,是一種傳輸測試結果的數據流協議,有助於對接多種類型的數據分析工具。
  • testrepository:第三方模塊,提供了一個測試結果存儲庫,同時也提供了一些單元測試用例的管理方法。
  • stestr:第三方模塊,是 testrepository 的分支,實現了一個並行的測試運行器,旨在使用多進程來運行 unittest 的 Test Suites。是推薦的 testrepository 替代方案。
  • coverage:第三方模塊,用於統計單元測試用例的覆蓋率。
  • tox:第三方模塊,用於管理和構建單元測試虛擬環境(virtualenv)。

unittest

unittest 是 Python 的標準單元測試庫,提供了最基本的單元測試框架和單元測試運行器。git

官方文檔https://docs.python.org/3.7/library/unittest.htmlgithub

單元測試框架 TestCast 使用示例web

# filename: test_module.py

import unittest

class TestStringMethods(unittest.TestCase):

    def test_upper(self):
        self.assertEqual('foo'.upper(), 'FOO')

    def test_isupper(self):
        self.assertTrue('FOO'.isupper())
        self.assertFalse('Foo'.isupper())

    def test_split(self):
        s = 'hello world'
        self.assertEqual(s.split(), ['hello', 'world'])
        # check that s.split fails when the separator is not a string
        with self.assertRaises(TypeError):
            s.split(2)

if __name__ == '__main__':
    unittest.main()

單元測試運行器 TestRunner 使用示例數據庫

  • 執行單元測試模塊
[root@localhost test]# python -m unittest -v test_module
test_isupper (test_module.TestStringMethods) ... ok
test_split (test_module.TestStringMethods) ... ok
test_upper (test_module.TestStringMethods) ... ok

----------------------------------------------------------------------
Ran 3 tests in 0.001s

OK
  • 執行單元測試類
[root@localhost test]# python -m unittest -v test_module.TestStringMethods
test_isupper (test_module.TestStringMethods) ... ok
test_split (test_module.TestStringMethods) ... ok
test_upper (test_module.TestStringMethods) ... ok

----------------------------------------------------------------------
Ran 3 tests in 0.000s

OK
  • 執行單元測試用例
[root@localhost test]# python -m unittest -v test_module.TestStringMethods.test_upper
test_upper (test_module.TestStringMethods) ... ok

----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

Test Discover

unittest 庫提供了 Test Discover(測試發現)功能,開發者只須要遵照 「約定俗成」 的命名規則,單元測試用例就能夠被自動的發現並運行。express

經過匹配條件自動發現單元測試模塊示例:json

python -m unittest discover -s project_directory -p "*_test.py"

discover 子命令選項後端

-v, --verbose 詳細輸出
-s, --start-directory {directory} 啓用發現的目錄(默認爲當前目錄)
-p, --pattern {pattern} 匹配單元測試模塊的模式(默認爲 test*.py)
-t, --top-level-directory {directory}

Test Fixture

unittest 庫實現了 Test Fixture(測試夾具)機制,用於抽象測試環境設置、清理邏輯。Such a working environment for the testing code is called a fixture.

單元測試框架 TestCase 經過定義 setUp()tearDown()setUpClass()tearDownClass()setUpModuletearDownModule 等方法來進行 單元測試前的設置工做單元測試後的清理工做

  • setUp:在運行 單元測試用例 以前被自動調用。setUp 異常則不運行單元測試用例。
  • tearDown:在運行完 單元測試用例 以後被自動調用。tearDown 異常,單元測試用例照常運行。
  • setUpClass:在運行 單元測試類 以前被自動調用。
  • tearDownClass:在運行完 單元測試類 以後被自動執行。
  • setUpModule:在運行 單元測試模塊 以前被自動調用。
  • tearDownModule:在運行完 單元測試模塊 以後被自動執行。

Test Fixture 應用示例

import unittest

class SimpleWidgetTestCase(unittest.TestCase):
    def setUp(self):
        # 運行單元測試用例以前設置 self.widget 實例屬性
        self.widget = Widget('The widget')

    def tearDown(self):
        # 運行完單元測試用例以後清理 self.widget 實例屬性
        self.widget.dispose()
        self.widget = None

class DefaultWidgetSizeTestCase(SimpleWidgetTestCase):
    def runTest(self):
        """真·測試用例"""
        # 相等斷言,檢測傳入實參是否相等
        self.assertEqual(self.widget.size(), (50,50),
                         'incorrect default size')

class WidgetResizeTestCase(SimpleWidgetTestCase):
    def runTest(self):
        self.widget.resize(100,150)
        self.assertEqual(self.widget.size(), (100,150),
                         'wrong size after resize')

上述實現方式有一個缺陷,若是咱們但願 Test Fixture 的執行粒度是單元測試用例,而不是單元測試類,咱們就須要爲每一個單元測試用例都實現一個繼承 Fixture 的測試類,如上述的 DefaultWidgetSizeTestCase 和 WidgetResizeTestCase 都繼承了 SimpleWidgetTestCase 以此分別讓各自的單元測試用例 runTest 獲得 Fixture。顯然,爲了單元測試用例獲得 Fixture 而實現單元測試類是不科學的,Test Suite 機制解決了這個問題。

Test Suite

TestCase 的 Test Suite(測試套件)機制,可以讓多個單元測試用例共享同屬一個測試類中實現的 Fixture。

應用 Test Suite 簡化上述實現的示例

import unittest

class WidgetTestCase(unittest.TestCase):

    def setUp(self):
        self.widget = Widget('The widget')

    def tearDown(self):
        self.widget.dispose()
        self.widget = None

    def test_default_size(self):
        self.assertEqual(self.widget.size(), (50,50),
                         'incorrect default size')

    def test_resize(self):
        self.widget.resize(100,150)
        self.assertEqual(self.widget.size(), (100,150),
                         'wrong size after resize')


# 生成測試用例 defaultSizeTestCase
defaultSizeTestCase = WidgetTestCase('test_default_size')
# 生成測試用例 resizeTestCase
resizeTestCase = WidgetTestCase('test_resize')
    
widgetTestSuite = unittest.TestSuite()
# 將測試用例加入 Suite,同一個 Suite 中的每一個測試用例都會執行一次 Fixture
widgetTestSuite.addTest(defaultSizeTestCase)
widgetTestSuite.addTest(resizeTestCase)

Pythonic 的實現

def suite():
    """Return a test suite. """
    tests = ['test_default_size', 'test_resize']
    return unittest.TestSuite(map(WidgetTestCase, tests))

Assert(斷言)

Assert(斷言)是單元測試關鍵,用於檢測一個條件是否符合預期。若是是真,不作任何事。若是爲假,就拋出 AssertionError 和錯誤信息。

NTOE:更詳細的信息建議查看官方文檔。
在這裏插入圖片描述

mock

mock:在 Python 3.x 中做爲一個模塊被內嵌到 unittest 標準庫。簡單的說,mock 就是製造假數據(對象)的模塊,以此來模擬多種代碼運行的情景,而無需真的發生了這種情景。

使用 Mock 對象來模擬測試情景的示例

  • 測試單元的代碼實現
# filename: client.py

import requests

# 該函數不屬於測試範疇
# 是須要被模擬的 Python 對象
def send_request(url):
    r = requests.get(url)
    return r.status_code

# 待測試的單元
# 功能是訪問 URL
# 存在兩種結果:
# 訪問成功:200
# 訪問失敗:404
def visit_baidu():
    return send_request('http://www.baidu.com')
  • 單元測試用例
# filename: test_client.py

import unittest
import mock

import client


class TestClient(unittest.TestCase):

    def test_success_request(self):
        # 測試訪問生成的狀況:
        # 實例化一個 Mock 對象,用於替換 client.send_request 函數
        # 這個 Mock 對象會返回 HTTP Code 200
        success_send = mock.Mock(return_value='200')
        client.send_request = success_send
        self.assertEqual(client.visit_baidu(), '200')

    def test_fail_request(self):
        # 測試訪問失敗的狀況:
        # 實例化一個 Mock 對象,用於替換 client.send_request 函數
        # 這個 Mock 對象會返回 HTTP Code 404
        fail_send = mock.Mock(return_value='404')
        client.send_request = fail_send
        self.assertEqual(client.visit_baidu(), '404')

在單元測試用例中經過構建模擬對象(Class Mock 的實例化)來模擬待測試代碼中指定的 Python 對象的屬性和行爲,經過這種方式在單元測試用例中模擬出代碼運行可能會發生的各類狀況。

上述示例中將 client.visit_baidu() 做爲測試單元,使用 Mock 對象 success_send/fail_send 來模擬了 client.send_request() 的 成功/失敗 返回。在測試單元中被模擬的 Python 對象,每每是這種 「會發生變化的對象」 或 「經過外部接口獲取的對象」。使用 mock 模塊大體上能夠總結出這樣的流程:

  1. 肯定測試單元中要模擬的 Python 對象,能夠是一個類、一個函數、一個實例對象或者是一個輸入。
  2. 在編寫單元測試用例的過程當中,實例化 Mock 對象並設置其屬性、行爲與被替代的 Python 對象的屬性、行爲可以符合預期。好比:被調用時會返回預期的值,被訪問實例屬性時會返回預期的值等。

Mock 類的原型

class Mock(spec=None, side_effect=None, return_value=DEFAULT, wraps=None, name=None, spec_set=None, **kwargs)
  • name:命名一個 Mock 實例化對象,起到標識做用。
  • side_effect:指定一個可調用對象,通常爲函數。當 Mock 實例化對象被調用,若是該可調用對象返回的不是 DEFAULT,則以該可調用對象的返回值做爲 Mock 實例化對象調用的返回值。
  • return_value:顯示指定返回一個值(或對象),當 Mock 實例化對象被調用,若是 side_effect 指定的可調用對象返回的是 DEFAULT,則以 return_value 做爲 Mock 實例化對象調用的返回值。

NTOE:更詳細的信息建議查看官方文檔。

Mock 對象的實例屬性自動建立機制

當訪問一個 Mock 實例化對象不存在的實例屬性時,它首先會自動建立一個子對象,而後對正在訪問的實例屬性進行賦值,這個機制對實現多級屬性的 Mock 很方便。e.g.

>>> import mock
>>> client = mock.Mock()
# 自動建立子對象
>>> client.v2_client.get.return_value = '200'
>>> client.v2_client.get()
'200'

Mock 的做用域

有時候咱們會但願 Mock 對象只在特定的地方模擬,而非全局,這就是 Mock Patch(Mock 對象的做用域)。mock.patch()mock.patch.object() 函數會返回一個 Class _patch 的實例對象,這個實例對象能夠做爲 函數/類 裝飾器(Decorator)或上下文管理器(Context Manager),經過這種 Pythonic 的方式來控制 Mock 對象的做用域。

Mock Patch 實現示例

>>> from unittest.mock import patch

# 在 test function 內: 
# module.ClassName1 被 MockClass1 替代
# module.ClassName2 被 MockClass2 替代
>>> @patch('module.ClassName2')
... @patch('module.ClassName1')
... def test(MockClass1, MockClass2):
...     module.ClassName1()
...     module.ClassName2()
...     assert MockClass1 is module.ClassName1
...     assert MockClass2 is module.ClassName2
...     assert MockClass1.called
...     assert MockClass2.called
...
>>> test()
  • Mock 掉一個 Python 對象的實例方法
class TestClient(unittest.TestCase):

    def test_success_request(self):
        status_code = '200'
        success_send = mock.Mock(return_value=status_code)
        # 在 with 語句範圍內 client.send_request 方法被 mock 掉
        with mock.patch('client.send_request', success_send):
            from client import visit_baidu
            self.assertEqual(visit_baidu(), status_code)

    def test_fail_request(self):
        status_code = '404'
        fail_send = mock.Mock(return_value=status_code)
        # 在 with 語句範圍內 client.send_request 方法被 mock 掉
        with mock.patch('client.send_request', fail_send):
            from client import visit_baidu
            self.assertEqual(visit_baidu(), status_code)
  • Mock 掉一個 Python 對象的實例屬性
def test_fail_request(self):
        status_code = '404'
        fail_send = mock.Mock(return_value=status_code)
        with mock.patch.object(client, 'send_request', fail_send):
            from client import visit_baidu
            self.assertEqual(visit_baidu(), status_code)

fixtures

fixtures:第三方模塊,對 unittest 的 Test Fixture 機制進行了加強,有效提升了測試代碼的複用率。

官方文檔https://pypi.org/project/fixtures/

fixtures 模塊依賴於 testtools 模塊,它提供了一種簡易建立 Fixture 對象的方式,也提供了一些內置的 Fixture。

自定義 Fixture 的示例

# Define _setUp to initialize your state and schedule a cleanup for when cleanUp is called and you’re done
>>> import unittest
>>> import fixtures
>>> class NoddyFixture(fixtures.Fixture):
...     def _setUp(self):
            # 運行單元測試用例以前初始化 frobnozzle 實例屬性
...         self.frobnozzle = 42
            # 運行完單元測試用例以後刪除 frobnozzle 實例屬性
...         self.addCleanup(delattr, self, 'frobnozzle')

每一個 Fixture 對象都應該實現 setUp()cleanUp() 方法,它們對應 unittest 的 setUp() + tearDown()

經過 FunctionFixture 組裝一個 Fixture 對象並使用的示例

>>> import os.path
>>> import shutil
>>> import tempfile

    # setUp()
>>> def setup_function():
...     return tempfile.mkdtemp()

    # tearDown()
>>> def teardown_function(fixture):
...     shutil.rmtree(fixture)

>>> fixture = fixtures.FunctionFixture(setup_function, teardown_function)
>>> fixture.setUp()
>>> print (os.path.isdir(fixture.fn_result))
True
>>> fixture.cleanUp()

Pythonic 的寫法

>>> with fixtures.FunctionFixture(setup_function, teardown_function) as fixture:
       # 單元測試用例
...    print (os.path.isdir(fixture.fn_result))
True

fixtures 模塊提供的 Fixture 對象在使用上更加靈活,並不是必定要在單元測試類中實現 setUp()tearDown()

MockPatchObject 和 MockPatch

fixtures 提供了 Class MockPatchObject 和 Class MockPatch,它們返回一個具備 Mock 做用域的 Fixture 對象,這個做用域的範圍就是 Fixture setUp 和 cleanUp 之間,並在在做用域範圍內 Mock 對象是生效的。

應用 MockPatchObject 的示例

>>> class Fred:
...     value = 1
# 將 Class Fred 轉換爲一個 fixture

>>> fixture = fixtures.MockPatchObject(Fred, 'value', 2)
# 在 fixture 的上下文中使用 Mock Fred
>>> with fixture:
...     Fred().value
2
>>> Fred().value
1

應用 MockPatch 的示例

>>> fixture = fixtures.MockPatch('subprocess.Popen.returncode', 3)

testtools

testtools:第三方模塊,是 unittest 的擴展,對 unittest 進行了斷言之類的功能加強,讓測試代碼的編寫更加方便。

官方文檔https://testtools.readthedocs.io/en/latest/

加強項目

  • Better assertion methods
  • Matchers: better than assertion methods
  • More debugging info, when you need it
  • Extend unittest, but stay compatible and re-usable
  • Cross-Python compatibility

示例

from testtools import TestCase
from testtools.content import Content
from testtools.content_type import UTF8_TEXT
from testtools.matchers import Equals

from myproject import SillySquareServer

class TestSillySquareServer(TestCase):

    # 在運行單元測試用例以前執行
    def setUp(self):
        super(TestSillySquareServer, self).setUp()
        # 載入 SillySquareServer 的 Fixture setUp/cleanUp 到本地 setUp
        self.server = self.useFixture(SillySquareServer())
        # 設定在運行完單元測試用例以後執行的清理動做
        self.addCleanup(self.attach_log_file)

    def attach_log_file(self):
        self.addDetail(
            'log-file',
            Content(UTF8_TEXT,
                    lambda: open(self.server.logfile, 'r').readlines()))

    # 單元測試用例
    def test_server_is_cool(self):
        self.assertThat(self.server.temperature, Equals("cool"))

    # 單元測試用例
    def test_square(self):
        self.assertThat(self.server.silly_square_of(7), Equals(49))

可見,使用 testtools.TestCase 框架可以讓測試代碼實現變得更加規範而簡單。

testscenarios(場景)

testscenarios:第三方模塊,用於知足了 「場景測試」 的需求,是節省重複代碼的有效手段。

官方文檔https://pypi.org/project/testscenarios/

所謂 「場景測試」 就好比:測試一段支持不一樣數據庫驅動(MongoDB/MySQL/SQLite)的數據庫訪問代碼,那麼每一種數據庫驅動就是一個場景,一般的咱們會爲每種場景都編寫一個測試用例,但有了 testscenarios 模塊,就只須要編寫一個統一的測試用例便可。這是由於 testscenarios 能夠經過在單元測試類中設定 scenarios 類屬性來描述不一樣的場景。TestCease 就能夠經過 testscenarios 框架根據 scenarios 自動生成不一樣的單元測試用例,從而達到測試不一樣場景的目的。

It is the intent of testscenarios to make dynamically running a single test in multiple scenarios clear, easy to debug and work with even when the list of scenarios is dynamically generated.

scenarios 類屬性數據結構示例

>>> class MyTest(unittest.TestCase):
...
...     scenarios = [
...         ('scenario1', dict(param=1)),
...         ('scenario2', dict(param=2)),]

應用 testscenarios 編寫場景測試的示例

# Some test loaders support hooks like load_tests and test_suite. 
# Ensuring your tests have had scenario application done through 
# these hooks can be a good idea - it means that external test 
# runners (which support these hooks like nose, trial, tribunal) 
# will still run your scenarios.
# unittest 支持 load_tests hooks,加載定製的單元測試用例
# 這裏用來加載 testscenarios 框架的 scenarios 測試用例
load_tests = testscenarios.load_tests_apply_scenarios

class YamlParseExceptions(testtools.TestCase): 
    scenarios = [
        ('scanner', dict(raised_exception=yaml.scanner.ScannerError())),
        ('parser', dict(raised_exception=yaml.parser.ParserError())),
        ('reader', dict(raised_exception=yaml.reader.ReaderError('', '', '', '', ''))),
    ]
 
    def test_parse_to_value_exception(self):
        text = 'not important'
 
        with mock.patch.object(yaml, 'load') as yaml_loader:
            yaml_loader.side_effect = self.raised_exception
            self.assertRaises(ValueError,
                              template_format.parse, text)

上述示例 testtools.TestCease 會經過 testscenarios 框架自動生成 scanner、parser、reader 三個 scenario 對應的三個單元測試用例,在這些測試用例中的 raised_exception 具備不一樣的實現。

python-subunit

subunit:第三方模塊,是一種傳輸測試結果的數據流協議,有助於對接多種類型的數據分析工具。

官方文檔https://pypi.org/project/python-subunit/

當測試用例不少的時候,如何高效處理測試結果就顯得很重要了。subunit 協議可以將測試結果轉換成多種格式,能夠靈活對接多種數據分析工具。

Subunit supplies the following filters:

  • tap2subunit - convert perl’s TestAnythingProtocol to subunit.
  • subunit2csv - convert a subunit stream to csv.
  • subunit2disk - export a subunit stream to files on disk.
  • subunit2pyunit - convert a subunit stream to pyunit test results.
  • subunit2gtk - show a subunit stream in GTK.
  • subunit2junitxml - convert a subunit stream to JUnit’s XML format.
  • subunit-diff - compare two subunit streams.
  • subunit-filter - filter out tests from a subunit stream.
  • subunit-ls - list info about tests present in a subunit stream.
  • subunit-stats - generate a summary of a subunit stream.
  • subunit-tags - add or remove tags from a stream.

python-subunit 也提供了測試運行器

python -m subunit.run mypackage.tests.test_suite

使用 python-subunit 提供的數據流轉換工具

python -m subunit.run mypackage.tests.test_suite | subunit2pyunit

testrepository(倉庫)

testrepository:第三方模塊,提供了一個測試結果存儲庫,同時也提供了一些單元測試用例的管理方法。

官方文檔https://pypi.org/project/testrepository/

在自動化單元測試流程中引入 testrepository 能夠:

  • 顯示用例運行時間
  • 顯示運行失敗的用例
  • 從新運行上次運行失敗的用例

testrepository 使用流程

  • Create a config file
$ touch .testr.conf
  • Create a repository
$ testr init
  • Load a test run into the repository
$ testr load < testrun
  • Query the repository
$ testr stats $ testr last $ testr failing
  • Delete a repository
$ rm -rf .testrepository

查看 testrepository 提供的指令集:

[root@localhost ~]# testr commands
command     description
----------  --------------------------------------------------------------
commands    List available commands.
failing     Show the current failures known by the repository.
help        Get help on a command.
init        Create a new repository.
last        Show the last run loaded into a repository.
list-tests  Lists the tests for a project.
load        Load a subunit stream into a repository.
quickstart  Introductory documentation for testrepository.
run         Run the tests for a project and load them into testrepository.
slowest     Show the slowest tests from the last test run.
stats       Report stats about a repository.

testrepository 常與 python-subunit 結合使用,testrepository 調用 python-subunit 的測試運行器執行測試,並將測試結果經過 subunit 協議導入到 testrepository 存儲庫中。

stestr

stestr:第三方模塊,是 testrepository 的分支,實現了一個並行的測試運行器,旨在使用多進程來運行 unittest 的 Test Suites。是推薦的 testrepository 替代方案。stestr 與 testrepository 具備相同上層概念對象,但底層卻以不一樣的方式運做。雖然 stestr 不須要依賴 python-subunit 的測試運行器,但仍會使用 subunit 協議。

官方文檔https://stestr.readthedocs.io/en/latest/

stestr 使用流程

  • 建立 .testr.conf 配置文件,此文件告訴了 stestr 在何處查找測試用例以及運行這些測試用例的方式
[DEFAULT]
test_path=./project_source_dir/tests
  • 開始運行單元測試用例
stestr run

查看 stestr 提供的指令集:

[root@localhost ~]# stestr --help
...
Commands:
  complete       print bash completion command (cliff)
  failing        Show the current failures known by the repository
  help           print detailed help for another command (cliff)
  init           Create a new repository.
  last           Show the last run loaded into a repository.
  list           List the tests for a project. You can use a filter just like with the run command to see exactly what tests match
  load           Load a subunit stream into a repository.
  run            Run the tests for a project and store them into the repository.
  slowest        Show the slowest tests from the last test run.

coverage(覆蓋)

coverage:第三方模塊,用於統計單元測試用例的覆蓋率。

官方文檔https://coverage.readthedocs.io/en/v4.5.x/

coverage 本質用於統計代碼覆蓋了,即有多少代碼被執行了,而在單元測試場景中,coverage 就用於統計單元測試的覆蓋率,即測試單元在整個工程中的比率。

coverage 使用流程

  • Use coverage run to run your program and gather data
# if you usually do:
#
# $ python my_program.py arg1 arg2
#
# then instead do:

$ coverage run my_program.py arg1 arg2
blah blah ..your program's output.. blah blah
  • Use coverage report to report on the results
$ coverage report -m
Name                      Stmts   Miss  Cover   Missing
-------------------------------------------------------
my_program.py                20      4    80%   33-35, 39
my_other_module.py           56      6    89%   17-23
-------------------------------------------------------
TOTAL                        76     10    87%
  • For a nicer presentation, use coverage html to get annotated HTML listings detailing missed lines
$ coverage html

tox

tox:第三方模塊,用於管理和構建單元測試虛擬環境(virtualenv)。

官方文檔https://tox.readthedocs.io/en/latest/

一個 Python 工程,可能同時須要運行 Python 2.x 和 Python 3.x 環境下的單元測試。顯然,這些任務須要在不一樣的虛擬環境中執行。tox 經過配置文件 tox.ini 的定義來爲每一個任務構建不一樣的虛擬環境。

# content of: tox.ini , put in same dir as setup.py
[tox]
envlist = py27,py36

[testenv]
# install pytest in the virtualenv where commands will be executed
deps = pytest
commands =
    # NOTE: you can run any command line tool here - not just tests
    pytest

tox 運做流程圖
在這裏插入圖片描述

Nova 的單元測試分析(Rocky)

Nova 的 tox.ini 配置

[tox]
minversion = 2.1

# 定義虛擬環境清單
envlist = py{27,35},functional,pep8
skipsdist = True

# 默認配置 Section
# 其餘 Section 沒有配置的選項都從這裏取值
[testenv]
basepython = python3

# 指定採用開發者模型構建虛擬環境中的工程
# 因此不會拷貝代碼到 virtualenv 目錄中,只是作個連接
usedevelop = True
whitelist_externals =
  bash
  find
  rm
  env

# 表示構建環境時安裝 Python 工程要執行的命令,通常是使用 pip 安裝
# -c 指定了依賴包的版本上限
install_command = pip install -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} {opts} {packages}

# 列出在虛擬機環境中生效的環境變量
setenv =
  VIRTUAL_ENV={envdir}
  LANGUAGE=en_US
  LC_ALL=en_US.utf-8
  OS_STDOUT_CAPTURE=1
  OS_STDERR_CAPTURE=1
  OS_TEST_TIMEOUT=160
# TODO(stephenfin): Remove psycopg2 when minimum constraints is bumped to 2.8
  PYTHONWARNINGS = ignore::UserWarning:psycopg2

# 指定構建虛擬環境時須要安裝的第三方依賴包
deps = -r{toxinidir}/test-requirements.txt

# 指定要構建完虛擬環境以後要執行的指令
commands =
  find . -type f -name "*.pyc" -delete

passenv =
  OS_DEBUG GENERATE_HASHES
# there is also secret magic in subunit-trace which lets you run in a fail only
# mode. To do this define the TRACE_FAILONLY environmental variable.

# py27 虛擬環境設置 Section
[testenv:py27]
# TODO(efried): Remove this once https://github.com/tox-dev/tox/issues/425 is fixed.
# 指定 Python 版本
basepython = python2.7

# 指定要執行的指令
commands =
  {[testenv]commands}

  # 調用 stestr 開始執行單元測試
  # {posargs} 參數就是從 tox 指令選項參數傳遞進來的
  stestr run {posargs}

  # --combine 將運行結果寫入存儲庫
  # --no-discover 不執行自動的測試發現,僅執行指定的單元測試模塊
  # OSProfiler 用於爲每一個請求生成一個跟蹤
  env TEST_OSPROFILER=1 stestr run --combine --no-discover 'nova.tests.unit.test_profiler'

  # 顯示上次測試中運行最慢的單元測試用例
  stestr slowest

[testenv:py35]
# TODO(efried): Remove this once https://github.com/tox-dev/tox/issues/425 is fixed.
basepython = python3.5

# 指定要執行的指令
commands =
  {[testenv]commands}
  # 調用 stestr 開始執行單元測試
  stestr run {posargs}
  env TEST_OSPROFILER=1 stestr run --combine --no-discover 'nova.tests.unit.test_profiler'

[testenv:py36]
# TODO(efried): Remove this once https://github.com/tox-dev/tox/issues/425 is fixed.
basepython = python3.6
commands =
  {[testenv:py35]commands}

[testenv:py37]
# TODO(efried): Remove this once https://github.com/tox-dev/tox/issues/425 is fixed.
basepython = python3.7
commands =
  {[testenv:py35]commands}

# PEP8 虛擬環境設置 Section
[testenv:pep8]
description =
  Run style checks.
envdir = {toxworkdir}/shared
commands =

  # 對工程進行 flack8 靜態檢查
  bash tools/flake8wrap.sh {posargs}
  # Check that all JSON files don't have \r\n in line.
  bash -c "! find doc/ -type f -name *.json | xargs grep -U -n $'\r'"
  # Check that all included JSON files are valid JSON
  bash -c '! find doc/ -type f -name *.json | xargs -t -n1 python -m json.tool 2>&1 > /dev/null | grep -B1 -v ^python'

[testenv:fast8]
description =
  Run style checks on the changes made since HEAD~. For a full run including docs, use 'pep8'
envdir = {toxworkdir}/shared
commands =
  bash tools/flake8wrap.sh -HEAD

[testenv:functional]
# TODO(melwitt): This can be removed when functional tests are gating with
# python 3.x
# NOTE(cdent): For a while, we shared functional virtualenvs with the unit
# tests, to save some time. However, this conflicts with tox siblings in zuul,
# and we need siblings to make testing against master of other projects work.
basepython = python2.7
setenv = {[testenv]setenv}
# As nova functional tests import the PlacementFixture from the placement
# repository these tests are, by default, set up to run with latest master from
# the placement repo. In the gate, Zuul will clone the latest master from
# placement OR the version of placement the Depends-On in the commit message
# suggests. If you want to run the test locally with an un-merged placement
# change, modify this line locally to point to your dependency or pip install
# placement into the appropriate tox virtualenv. We express the requirement
# here instead of test-requirements because we do not want placement present
# during unit tests.
deps =
  -r{toxinidir}/test-requirements.txt
  git+https://git.openstack.org/openstack/placement#egg=openstack-placement
commands =
  {[testenv]commands}
# NOTE(cdent): The group_regex describes how stestr will group tests into the
# same process when running concurently. The following ensures that gabbi tests
# coming from the same YAML file are all in the same process. This is important
# because each YAML file represents an ordered sequence of HTTP requests. Note
# that tests which do not match this regex will not be grouped in any
# special way. See the following for more details.
# http://stestr.readthedocs.io/en/latest/MANUAL.html#grouping-tests
# https://gabbi.readthedocs.io/en/latest/#purpose

  # 調用 stestr 開始執行單元測試
  stestr --test-path=./nova/tests/functional --group_regex=nova\.tests\.functional\.api\.openstack\.placement\.test_placement_api(?:\.|_)([^_]+) run {posargs}
  stestr slowest

# TODO(gcb) Merge this into [testenv:functional] when functional tests are gating
# with python 3.5
[testenv:functional-py35]
basepython = python3.5
setenv = {[testenv]setenv}
deps = {[testenv:functional]deps}
commands =
  {[testenv:functional]commands}

[testenv:functional-py36]
basepython = python3.6
setenv = {[testenv]setenv}
deps = {[testenv:functional]deps}
commands =
  {[testenv:functional]commands}

[testenv:functional-py37]
basepython = python3.7
setenv = {[testenv]setenv}
deps = {[testenv:functional]deps}
commands =
  {[testenv:functional]commands}

[testenv:api-samples]
envdir = {toxworkdir}/shared
setenv =
  {[testenv]setenv}
  GENERATE_SAMPLES=True
  PYTHONHASHSEED=0
commands =
  {[testenv]commands}
  # 調用 stestr 開始執行單元測試
  stestr --test-path=./nova/tests/functional/api_sample_tests run {posargs}
  stestr slowest

[testenv:genconfig]
envdir = {toxworkdir}/shared
commands =
  # 生成 nova.conf 配置文件
  oslo-config-generator --config-file=etc/nova/nova-config-generator.conf

[testenv:genpolicy]
envdir = {toxworkdir}/shared
commands =
   # 生成 policy 配置文件
  oslopolicy-sample-generator --config-file=etc/nova/nova-policy-generator.conf

[testenv:genplacementpolicy]
envdir = {toxworkdir}/shared
commands =
  # 生成 placement policy 配置文件
  oslopolicy-sample-generator --config-file=etc/nova/placement-policy-generator.conf

[testenv:cover]
# TODO(stephenfin): Remove the PYTHON hack below in favour of a [coverage]
# section once we rely on coverage 4.3+
#
# https://bitbucket.org/ned/coveragepy/issues/519/
envdir = {toxworkdir}/shared
setenv =
  {[testenv]setenv}
  PYTHON=coverage run --source nova --parallel-mode
commands =
  {[testenv]commands}
  # 調用 coverage 生成單元測試覆蓋率報告
  coverage erase
  stestr run {posargs}
  coverage combine
  coverage html -d cover
  coverage xml -o cover/coverage.xml
  coverage report

[testenv:debug]
envdir = {toxworkdir}/shared
commands =
  {[testenv]commands}
  oslo_debug_helper {posargs}

[testenv:venv]
deps =
  -r{toxinidir}/requirements.txt
  -r{toxinidir}/test-requirements.txt
  -r{toxinidir}/doc/requirements.txt
commands =
  {posargs}

[testenv:docs]
description =
  Build main documentation.
deps = -r{toxinidir}/doc/requirements.txt
commands =
  rm -rf doc/build
  # Check that all JSON files don't have \r\n in line.
  bash -c "! find doc/ -type f -name *.json | xargs grep -U -n $'\r'"
  # Check that all included JSON files are valid JSON
  bash -c '! find doc/ -type f -name *.json | xargs -t -n1 python -m json.tool 2>&1 > /dev/null | grep -B1 -v ^python'

  # 使用 Sphinx 來構建本地文檔網站
  sphinx-build -W -b html -d doc/build/doctrees doc/source doc/build/html
  # Test the redirects. This must run after the main docs build
  whereto doc/build/html/.htaccess doc/test/redirect-tests.txt

[testenv:api-guide]
description =
  Generate the API guide. Called from CI scripts to test and publish to developer.openstack.org.
envdir = {toxworkdir}/docs
deps = {[testenv:docs]deps}
commands =
  rm -rf api-guide/build
  sphinx-build -W -b html -d api-guide/build/doctrees api-guide/source api-guide/build/html

[testenv:api-ref]
description =
  Generate the API ref. Called from CI scripts to test and publish to developer.openstack.org.
envdir = {toxworkdir}/docs
deps = {[testenv:docs]deps}
commands =
  rm -rf api-ref/build
  sphinx-build -W -b html -d api-ref/build/doctrees api-ref/source api-ref/build/html

[testenv:releasenotes]
description =
  Generate release notes.
envdir = {toxworkdir}/docs
deps = {[testenv:docs]deps}
commands =
  rm -rf releasenotes/build
  sphinx-build -W -b html -d releasenotes/build/doctrees releasenotes/source releasenotes/build/html

[testenv:all-docs]
description =
  Build all documentation including API guides and refs.
envdir = {toxworkdir}/docs
deps = -r{toxinidir}/doc/requirements.txt
commands =
  {[testenv:docs]commands}
  {[testenv:api-guide]commands}
  {[testenv:api-ref]commands}
  {[testenv:releasenotes]commands}

[testenv:bandit]
# NOTE(browne): This is required for the integration test job of the bandit
# project. Please do not remove.
envdir = {toxworkdir}/shared
commands = bandit -r nova -x tests -n 5 -ll

# Python 代碼靜態檢查
[flake8]
# E125 is deliberately excluded. See
# https://github.com/jcrocholl/pep8/issues/126. It's just wrong.
#
# Most of the whitespace related rules (E12* and E131) are excluded
# because while they are often useful guidelines, strict adherence to
# them ends up causing some really odd code formatting and forced
# extra line breaks. Updating code to enforce these will be a hard sell.
#
# H405 is another one that is good as a guideline, but sometimes
# multiline doc strings just don't have a natural summary
# line. Rejecting code for this reason is wrong.
#
# E251 Skipped due to https://github.com/jcrocholl/pep8/issues/301
enable-extensions = H106,H203,H904
ignore = E121,E122,E123,E124,E125,E126,E127,E128,E129,E131,E251,H405
exclude =  .venv,.git,.tox,dist,*lib/python*,*egg,build,tools/xenserver*,releasenotes
# To get a list of functions that are more complex than 25, set max-complexity
# to 25 and run 'tox -epep8'.
# 34 is currently the most complex thing we have
# TODO(jogo): get this number down to 25 or so
max-complexity=35

[hacking]
local-check-factory = nova.hacking.checks.factory
import_exceptions = nova.i18n

[testenv:bindep]
# Do not install any requirements. We want this to be fast and work even if
# system dependencies are missing, since it's used to tell you what system
# dependencies are missing! This also means that bindep must be installed
# separately, outside of the requirements files, and develop mode disabled
# explicitly to avoid unnecessarily installing the checked-out repo too (this
# further relies on "tox.skipsdist = True" above).
usedevelop = False
deps = bindep
commands =
  bindep test

[testenv:lower-constraints]
deps =
  -c{toxinidir}/lower-constraints.txt
  -r{toxinidir}/test-requirements.txt
  -r{toxinidir}/requirements.txt
commands =
  {[testenv]commands}
  stestr run {posargs}

從配置內容能夠看出當咱們運行單元測試的時候,是經過執行指令 stestr run {posargs} 來觸發的,而描述單元測試執行細節的 stestr 配置文件的內容以下:

[root@localhost nova]# cat .stestr.conf
[DEFAULT]
test_path=./nova/tests/unit
top_dir=./

若是使用 testrepository,那麼配置文件多是這樣的:

[root@localhost nova]# cat .testr.conf
[DEFAULT]
test_command=OS_STDOUT_CAPTURE=${OS_STDOUT_CAPTURE:-1} \
             OS_STDERR_CAPTURE=${OS_STDERR_CAPTURE:-1} \
             OS_TEST_TIMEOUT=${OS_TEST_TIMEOUT:-160} \
             ${PYTHON:-python} -m subunit.run discover -t ./ ${OS_TEST_PATH:-./nova/tests} $LISTOPT $IDOPTION

test_id_option=--load-list $IDFILE
test_list_option=--list
# NOTE(cdent): The group_regex describes how testrepository will
# group tests into the same process when running concurently. The
# following insures that gabbi tests coming from the same YAML file
# are all in the same process. This is important because each YAML
# file represents an ordered sequence of HTTP requests. Note that
# tests which do not match this regex will not be grouped in any
# special way. See the following for more details.
# http://testrepository.readthedocs.io/en/latest/MANUAL.html#grouping-tests
# https://gabbi.readthedocs.io/en/latest/#purpose
group_regex=(gabbi\.(?:driver|suitemaker)\.test_placement_api_([^_]+))

可見,testrepository 調用了 python-subunit 的測試運行器,而 stestr 則不須要。但不管如何,它們最終都執行了 nova/tests 目錄下爲單元測試用例。

小結 Nova 單元測試工做流程

  1. 使用 unittest, mock, testtools, fixtures, testscenarios 等工具編寫 Python 工程的單元測試代碼。
  2. 使用 tox 來管理單元測試虛擬運行環境。
  3. tox.ini 定義了 testrepository 或 stestr 執行指令來啓動測試流程。
  4. testrepository 調用 subunit 來執行測試用例並輸出測試結果到存儲庫,或者是 stestr 直接執行測試用例並存儲測試結果。
  5. tox.ini 定義了 coverage 執行指令生成單元測試覆蓋率報告。

執行 py27 單元測試

執行指令:

tox -epy27

日誌分析

# 建立 py27 虛擬環境
py27 create: /opt/stack/nova/.tox/py27

# 安裝測試依賴包,主要是上述單元測試工具
py27 installdeps: -r/opt/stack/nova/test-requirements.txt

# 安裝 Python 工程
py27 develop-inst: /opt/stack/nova
py27 installed: DEPRECATION: Python 2.7 will reach the end of its life on January 1st, 2020. Please upgrade your Python as Python 2.7 won't be maintained after that date. A future version of pip will drop support for Python 2.7.,alembic==1.0.8,amqp==2.4.2,appdirs==1.4.3,asn1crypto==0.24.0,atomicwrites==1.3.0,attrs==19.1.0,automaton==1.16.0,...,

# 運行測試以前設定 PYTHONHASHSEED
py27 run-test-pre: PYTHONHASHSEED='3303221241'

# 運行測試,執行指令刪除 *.pyc 文件
py27 runtests: commands[0] | find . -type f -name '*.pyc' -delete

# 執行 stestr run,執行 ./nova/tests/unit 下的單元測試用例
py27 runtests: commands[1] | stestr run

# {2} - stestr worker id
# nova.tests.unit.api.openstack.compute.test_agents.AgentsTestV21.test_agents_create_without_architecture - 單元測試用例
# [0.241984s] - 執行時間
# ok - 執行成功
{2} nova.tests.unit.api.openstack.compute.test_agents.AgentsTestV21.test_agents_create_without_architecture [0.241984s] ... ok
	
...

======
Totals
======
Ran: 17506 tests in 282.0000 sec.
 - Passed: 17440
 - Skipped: 65
 - Expected Fail: 1
 - Unexpected Success: 0
 - Failed: 0
Sum of execute time for each test: 4053.6704 sec.

# stestr 多進程工做的負載均衡結果
==============
Worker Balance
==============
 - Worker 0 (1095 tests) => 0:04:19.881048
 - Worker 1 (1093 tests) => 0:04:17.439649
 - Worker 2 (1094 tests) => 0:04:06.930933
 - Worker 3 (1094 tests) => 0:04:14.145450
 - Worker 4 (1094 tests) => 0:04:39.898363
 - Worker 5 (1094 tests) => 0:04:19.573067
 - Worker 6 (1094 tests) => 0:04:19.100025
 - Worker 7 (1095 tests) => 0:04:20.019838
 - Worker 8 (1092 tests) => 0:04:11.370670
 - Worker 9 (1095 tests) => 0:04:09.487689
 - Worker 10 (1095 tests) => 0:04:14.567449
 - Worker 11 (1096 tests) => 0:04:20.263249
 - Worker 12 (1092 tests) => 0:04:02.847126
 - Worker 13 (1097 tests) => 0:04:04.662348
 - Worker 14 (1094 tests) => 0:04:16.163928
 - Worker 15 (1092 tests) => 0:03:56.793120

# 執行 OSProfiler 單元測試指令
py27 runtests: commands[2] | env TEST_OSPROFILER=1 stestr run --combine --no-discover nova.tests.unit.test_profiler
{0} nova.tests.unit.test_profiler.TestProfiler.test_all_public_methods_are_traced [0.549399s] ... ok

======
Totals
======
Ran: 1 tests in 0.0000 sec.
 - Passed: 1
 - Skipped: 0
 - Expected Fail: 0
 - Unexpected Success: 0
 - Failed: 0
Sum of execute time for each test: 0.5494 sec.

==============
Worker Balance
==============
 - Worker 0 (1 tests) => 0:00:00.549399

# 執行 stestr slowest 指令輸出執行最慢的測試用例
py27 runtests: commands[3] | stestr slowest
Test id                                                                                                                                                    Runtime (s)
---------------------------------------------------------------------------------------------------------------------------------------------------------  -----------
nova.tests.unit.db.test_migrations.TestNovaMigrationsSQLite.test_walk_versions                                                                             32.981
nova.tests.unit.test_fixtures.TestDatabaseAtVersionFixture.test_fixture_schema_version                                                                     11.045
nova.tests.unit.api.openstack.compute.test_availability_zone.ServersControllerCreateTestV21.test_create_instance_with_invalid_availability_zone_too_short  10.151
nova.tests.unit.api.openstack.compute.test_disk_config.DiskConfigTestCaseV21.test_create_server_with_auto_disk_config                                       9.632
nova.tests.unit.api.openstack.compute.test_flavor_manage.FlavorManagerPolicyEnforcementV21.test_delete_policy_rbac_change_to_default_action_rule            9.402
nova.tests.unit.api.openstack.compute.test_flavors.DisabledFlavorsWithRealDBTestV21.test_index_should_list_disabled_flavors_to_admin                        9.401
nova.tests.unit.api.openstack.compute.test_access_ips.AccessIPsAPIValidationTestV21.test_create_server_with_invalid_access_ipv6                             9.238
nova.tests.unit.api.openstack.compute.test_availability_zone.ServersControllerCreateTestV21.test_create_instance_with_invalid_availability_zone_not_str     9.168
nova.tests.unit.api.openstack.compute.test_access_ips.AccessIPsAPIValidationTestV21.test_rebuild_server_with_invalid_access_ipv6                            9.152
nova.tests.unit.virt.libvirt.storage.test_rbd.RbdTestCase.test_cleanup_volumes_fail_snapshots                                                               9.114
___________________________________________________________________________________________________________________________________ summary ____________________________________________________________________________________________________________________________________

  py27: commands succeeded
  congratulations :)

經過日誌分析可見,tox 工具啓動 py27 單元測試的核心指令有 3 條:

  1. stestr run
  2. env TEST_OSPROFILER=1 stestr run --combine --no-discover nova.tests.unit.test_profiler
  3. stestr slowest

這裏咱們主要關注第一條指令,它涉及到 Nova 的單元測試用例是怎麼實現的問題。

代碼實現分析

下面咱們以 GET /servers/{server_uuid} 的測試單元爲例。

調試指令:

cd /opt/stack/nova/; stestr run --combine --no-discover 'nova.tests.unit.api.openstack.compute.test_serversV21.ServersControllerTest.test_get_server_by_uuid'

NOTE:實際上你或許應該在對應的虛擬環境中執行調試。e.g.

$ source .tox/py27/bin/activate
$ stestr run --combine --no-discover "neutron.tests.unit.scheduler.test_dhcp_agent_scheduler.TestNetworksFailover.test_filter_bindings"

代碼分析:

# /opt/stack/nova/nova/tests/unit/api/openstack/compute/test_serversV21.py

class ServersControllerTest(ControllerTest):
    
    def req(self, url, use_admin_context=False):
        return fakes.HTTPRequest.blank(url,
                                       use_admin_context=use_admin_context,
                                       version=self.wsgi_api_version)
    ...
    def test_get_server_by_uuid(self):
        # 生成 request 對象
        req = self.req('/fake/servers/%s' % FAKE_UUID)
        # self.controller 是 nova.api.openstack.compute.servers.ServersController 實例對象
        # nova.api.openstack.compute.servers.ServersController.show() 是測試單元
        res_dict = self.controller.show(req, FAKE_UUID)
        self.assertEqual(res_dict['server']['id'], FAKE_UUID)

按照正常的黑盒測試思路,能不能獲取 Server,發個請求就知道了。固然了,這樣作的前提是後端服務正常的狀況下,但運行單元測試的環境顯然沒有後端服務進程,因此咱們就須要 Fake(欺騙)出一些 「後端服務進程」 出來,fakes.HTTPRequest 的含義正是如此。在測試用例 test_get_server_by_uuid() 中,只要輸入指定的 FAKE_UUID,而後返回指定的 res_dict,經過了斷言的判斷,那麼 GET /servers/{server_uuid} 這個測試用例咱們就認爲是正確的。

(Pdb) FAKE_UUID
'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'

(Pdb) pp self.controller.show(req, FAKE_UUID)
{'server': {'OS-DCF:diskConfig': 'MANUAL',
            'OS-EXT-AZ:availability_zone': u'nova',
            'OS-EXT-SRV-ATTR:host': None,
            'OS-EXT-SRV-ATTR:hypervisor_hostname': None,
            'OS-EXT-SRV-ATTR:instance_name': 'instance-00000002',
            'OS-EXT-STS:power_state': 1,
            'OS-EXT-STS:task_state': None,
            'OS-EXT-STS:vm_state': u'active',
            'OS-SRV-USG:launched_at': None,
            'OS-SRV-USG:terminated_at': None,
            'accessIPv4': '',
            'accessIPv6': '',
            'addresses': OrderedDict([(u'test1', [{'OS-EXT-IPS-MAC:mac_addr': u'aa:aa:aa:aa:aa:aa', 'version': 4, 'addr': u'192.168.1.100', 'OS-EXT-IPS:type': u'fixed'}, {'OS-EXT-IPS-MAC:mac_addr': u'aa:aa:aa:aa:aa:aa', 'version': 6, 'addr': u'2001:db8:0:1::1', 'OS-EXT-IPS:type': u'fixed'}])]),
            'config_drive': None,
            'created': '2010-10-10T12:00:00Z',
            'flavor': {'id': '2',
                       'links': [{'href': 'http://localhost/fake/flavors/2',
                                  'rel': 'bookmark'}]},
            'hostId': '',
            'id': 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
            'image': {'id': '10',
                      'links': [{'href': 'http://localhost/fake/images/10',
                                 'rel': 'bookmark'}]},
            'key_name': u'',
            'links': [{'href': 'http://localhost/v2/fake/servers/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
                       'rel': 'self'},
                      {'href': 'http://localhost/fake/servers/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
                       'rel': 'bookmark'}],
            'metadata': {'seq': u'2'},
            'name': u'server2',
            'os-extended-volumes:volumes_attached': [{'id': u'some_volume_1'},
                                                     {'id': u'some_volume_2'}],
            'progress': 0,
            'security_groups': [{'name': u'fake-0-0'}, {'name': u'fake-0-1'}],
            'status': 'ACTIVE',
            'tenant_id': u'fake_project',
            'updated': '2010-11-11T11:00:00Z',
            'user_id': u'fake_user'}}

若是你嘗試斷點調試下去的話,你會發現測試單元的程序流會定格在這個地方:

# /opt/stack/nova/nova/api/openstack/common.py

def get_instance(compute_api, context, instance_id, expected_attrs=None,
                 cell_down_support=False):
    """Fetch an instance from the compute API, handling error checking."""
    try:
        return compute_api.get(context, instance_id,
                               expected_attrs=expected_attrs,
                               cell_down_support=cell_down_support)
    except exception.InstanceNotFound as e:
        raise exc.HTTPNotFound(explanation=e.format_message())

此時的 compute_api.get 再也不是 nova.compute.api:API.get() 方法了,而是一個 _AutospecMagicMock 對象,它的 side_effect 是 nova.unit.api.openstack.fakes:fake_compute_get._return_server_obj 函數:

# /opt/stack/nova/nova/tests/unit/api/openstack/fakes.py

def fake_compute_get(**kwargs):
    def _return_server_obj(context, *a, **kw):
        return stub_instance_obj(context, **kwargs)
    return _return_server_obj

...
def stub_instance_obj(ctxt, *args, **kwargs):
    db_inst = stub_instance(*args, **kwargs)
    expected = ['metadata', 'system_metadata', 'flavor',
                'info_cache', 'security_groups', 'tags']
    inst = objects.Instance._from_db_object(ctxt, objects.Instance(),
                                            db_inst,
                                            expected_attrs=expected)
    inst.fault = None
    if db_inst["services"] is not None:
        # This ensures services there if one wanted so
        inst.services = db_inst["services"]

    return inst

顯然的,這是一次 Mock,在 ServersControllerTest 的父類 ControllerTest 的 setUp 中完成:

# /opt/stack/nova/nova/tests/unit/api/openstack/compute/test_serversV21.py

class ControllerTest(test.TestCase):

    def setUp(self):
        super(ControllerTest, self).setUp()
        ...
        return_server = fakes.fake_compute_get(id=2, availability_zone='nova',
                                               launched_at=None,
                                               terminated_at=None,
                                               security_groups=security_groups,
                                               task_state=None,
                                               vm_state=vm_states.ACTIVE,
                                               power_state=1)
        ...
        self.mock_get = self.useFixture(fixtures.MockPatchObject(
            compute_api.API, 'get', side_effect=return_server)).mock

在 setUp 中經過 fixtures.MockPatchObject 將 compute_api.API.get 方法 Mock 成了 fakes._return_server_obj 函數,並最終返回僞造的數據。這就是一個完整的單元測試用例套路了。

參考文檔

http://www.choudan.net/2013/08/12/OpenStack-Small-Tests.html
https://blog.csdn.net/quqi99/article/details/8533071

相關文章
相關標籤/搜索