Python单元测试:unit testing best practices

96
九遐
2019.01.31 16:11* 字数 867

Python单元测试:unit testing best practices
Python单元测试:unit test code coverage

常用工具

单元测试

默认情况下,unittest会查找所有以test开头的test method,比如test_sum()或者testSum(). 如果找不到,会提示说没有要运行的test case.

示例,被测模块代码

#src/person.py

class Calculator:
    def sum(self, a, b):
        return a + b

单元测试代码

# test/calcalatorTest.py

from unittest import TestCase
from src.calculator import Calculator
import unittest
class calculatorTest(TestCase):
    def setUp(self):
        self.calc = Calculator()

    def test_sum(self):
        answer = self.calc.sum(2, 4)
        self.assertEqual(answer, 6)

def suite():
    suite = unittest.TestSuite()
    suite.addTests(
        unittest.TestLoader().loadTestsFromTestCase(calculatorTest)
    )

    return suite


if __name__ == "__main__":
    unittest.TextTestRunner(verbosity=2).run(suite())

如果是在pycharm中,可以直接右键执行。如果是命令行,可以使用如下方法:

Administrator@SC-201810221720 MINGW64 /f/jira2
$ python -m unittest test/calculatorTest.py
E
======================================================================
ERROR: calculatorTest (unittest.loader._FailedTest)
----------------------------------------------------------------------
ImportError: Failed to import test module: calculatorTest
Traceback (most recent call last):
  File "C:\Users\Administrator\AppData\Local\Programs\Python\Python37\lib\unittest\loader.py", line 154, in loadTestsFromName
    module = __import__(module_name)
ModuleNotFoundError: No module named 'test.calculatorTest'


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

FAILED (errors=1)

Administrator@SC-201810221720 MINGW64 /f/jira2
$ ls test
__pycache__/  calculatorTest.py  personTest.py

Administrator@SC-201810221720 MINGW64 /f/jira2
$ touch ./test/__init__.py

Administrator@SC-201810221720 MINGW64 /f/jira2
$ ls ./test
__init__.py  __pycache__/  calculatorTest.py  personTest.py

Administrator@SC-201810221720 MINGW64 /f/jira2
$ python -m unittest test/calculatorTest.py
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK
Administrator@SC-201810221720 MINGW64 /f/jira2
$ python -m unittest test.calculatorTest
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

批量执行test目录下的所有测试用例:
python -m unittest其实等同于python -m unittest discover。但python -m unittest discover默认查找的文件名是test*.py,所以,要么按照惯例,测试文件名都命名为test*.py, 要么使用discover的时候加上-p参数自定义查找的文件名格式。

Administrator@SC-201810221720 MINGW64 /f/jira2
$ ls test
__init__.py  __pycache__/  calculatorTest.py  personTest.py

Administrator@SC-201810221720 MINGW64 /f/jira2
$ python -m unittest discover test

----------------------------------------------------------------------
Ran 0 tests in 0.000s

OK

Administrator@SC-201810221720 MINGW64 /f/jira2
$ python -m unittest discover test -p '*Test.py'
....
----------------------------------------------------------------------
Ran 4 tests in 0.001s

OK

unittest参数化

unittest默认不支持参数化,但是可以通过其他方式来达到这种目的:

  1. 重写unittest.TestCase的构造函数,参考链接
  2. 使用nose-parameterized: https://github.com/wolever/parameterized
  3. 使用nose和ddt: https://github.com/datadriventests/ddt
  4. 使用ParamUnittest:https://github.com/rik0/ParamUnittest

unittest.skip()方法

image.png

Mock的用法

  • mock: 从Python 3.3开始,mock模块已经被合并到标准库中,被命名为unittest.mock,可以直接import进来使用.
  • HTTPretty: Python 的 HTTP 请求 mock 工具, 不支持py3
  • httmock: 针对 Python 2.6+ 和 3.2+ 生成 伪造请求的库。
# src/person.py
class Person:
    def __init__(self):
        self.__age = 10

    @property
    def city(self):
        return 'shanghai'

    def get_fullname(self, first_name, last_name):
        return first_name + ' ' + last_name

    @classmethod
    def get_age(self):
        return self.__age

    @staticmethod
    def get_class_name():
        return Person.__name__

单元测试代码

import unittest
from src.person import *
from unittest.mock import *


class personTest(unittest.TestCase):
    def setUp(self):
        pass

#instance method, 实例方法
    def test_get_fullname(self):
        p = Person()
        self.assertEqual(p.get_fullname('zhang','san'), 'zhang san')

    def test_get_fullname2(self):
        p = Person()
        # mock掉get_fullname,让它返回'James Harden'
        p.get_fullname = Mock(return_value='li si')
        self.assertEqual(p.get_fullname(), 'li si') #调用p.get_fullname时没有给任何的参数,但是依然可以工作。

    @patch('src.person.Person.get_fullname', return_value='li si')
    def test_get_fullname3(self, get_fullname):
        self.assertEqual(get_fullname(), 'li si')

    @patch('src.person.Person.get_fullname')
    def test_get_fullname4(self, mock_get_fullname):
        mock_get_fullname.return_value = 'li si'
        p = Person()
        self.assertEqual(p.get_fullname(), 'li si')


#类方法
    @patch('src.person.Person.get_age')
    def test_classmethodTest(self, mock_get_age):
        mock_get_age.return_value = 100
        self.assertEqual(Person.get_age(), 100)

#静态方法,使用patch
    @patch('src.person.Person.get_class_name')
    def test_staticmethodTest1(self, mock_get_class_name):
        mock_get_class_name.return_value = 'Guy'
        self.assertEqual(Person.get_class_name(), 'Guy')

# 静态方法,使用patch
    mock_get_class_name = Mock(return_value='Guy')
    # 在patch中给出定义好的Mock的对象,好处是定义好的对象可以复用
    @patch('src.person.Person.get_class_name', mock_get_class_name)
    def test_should_get_class_name11(self):
        self.assertEqual(Person.get_class_name(), 'Guy')


# 静态方法,使用patch.object
        @patch.object(Person, 'get_class_name')
        def test_staticmethodTest2(self, mock_get_class_name):
            mock_get_class_name.return_value = 'Guy'
            self.assertEqual(Person.get_class_name(), 'Guy')

# 静态方法,使用patch.object
        mock_get_class_name = Mock(return_value='Guy')
        # 使用patch.object来mock,好处是Person类不是以字符串形式给出的
        @patch.object(Person, 'get_class_name', mock_get_class_name)
        def test_should_get_class_name4(self, ):
            self.assertEqual(Person.get_class_name(), 'Guy')

# 静态方法,使用作用域
        # 作用域之外,依然返回真实值
        def test_should_get_class_name3(self, ):
            mock_get_class_name = Mock(return_value='Guy')
            with patch('your.package.module.Person.get_class_name', mock_get_class_name):
                self.assertEqual(Person.get_class_name(), 'Guy')

            self.assertEqual(Person.get_class_name(), 'Person')

#属性
    @patch('src.person.Person.city', new_callable=PropertyMock)
    def test_propertyTest(self, mock_city):
        mock_city.return_value = 'beijing'
        self.assertEqual(Person.city, 'beijing')

#检查是否调用
    def test_should_validate_method_calling(self):
        p = Person()
        p.get_fullname = Mock(return_value='James Harden')

        # 没调用过
        p.get_fullname.assert_not_called()  # Python 3.5

        p.get_fullname('1', '2')

        # 调用过任意次数
        p.get_fullname.assert_called()  # Python 3.6
        # 只调用过一次, 不管参数
        p.get_fullname.assert_called_once()  # Python 3.6
        # 只调用过一次,并且符合指定的参数
        p.get_fullname.assert_called_once_with('1', '2')

        p.get_fullname('3', '4')
        # 只要调用过即可,必须指定参数
        p.get_fullname.assert_any_call('1', '2')

        # 重置mock,重置之后相当于没有调用过
        p.get_fullname.reset_mock()
        p.get_fullname.assert_not_called()

        # Mock对象里除了return_value, side_effect属性外,
        # called表示是否调用过,call_count可以返回调用的次数
        self.assertEqual(p.get_fullname.called, False)
        self.assertEqual(p.get_fullname.call_count, 0)

        p.get_fullname('1', '2')
        p.get_fullname('3', '4')
        self.assertEqual(p.get_fullname.called, True)
        self.assertEqual(p.get_fullname.call_count, 2)

#校验参数个数,再返回固定值。如果想校验参数需要用create_autospec模块方法替代Mock类
    def test_should_get_fullname(self):
        p = Person()

        p.get_fullname = create_autospec(p.get_fullname, return_value='James Harden')

        # 随便给两个参数,依然会返回mock的值
        self.assertEqual(p.get_fullname('1', '2'), 'James Harden')

        # 如果参数个数不对,会报错TypeError: missing a required argument: 'last_name'
        # p.get_fullname('1')
        self.assertRaises(TypeError, p.get_fullname, "1" )

#使用side_effect, 依次返回指定值
    def test_should_get_age(self):
        p = Person()
        p.get_age = Mock(side_effect=[10, 11, 12])

        self.assertEqual(p.get_age(), 10)
        self.assertEqual(p.get_age(), 11)
        self.assertEqual(p.get_age(), 12)

# 使用side_effect, 返回一个新的自定义函数或者一个匿名函数
        @patch('src.person.Person.get_age', side_effect=lambda : 77)  #返回一个无参数的匿名函数
        def test_should_get_age(self):
            self.assertEqual(Person.get_age(), 77)

#根据参数不同,返回不同的值
    def test_should_get_fullname(self):
        p = Person()

        values = {('James', 'Harden'): 'James Harden', ('Tracy', 'Grady'): 'Tracy Grady'}
        p.get_fullname = Mock(side_effect=lambda x, y: values[(x, y)])

        self.assertEqual(p.get_fullname('James', 'Harden'), 'James Harden')
        self.assertEqual(p.get_fullname('Tracy', 'Grady'), 'Tracy Grady')

#抛出异常
    def test_should_raise_exception(self):
        p = Person()
        p.get_age = Mock(side_effect=TypeError('integer type'))
        # 只要调就会抛出异常
        self.assertRaises(TypeError, p.get_age)


def suite():
    suite = unittest.TestSuite()
    suite.addTests(
        unittest.TestLoader().loadTestsFromTestCase(personTest)
    )

    return suite


if __name__ == "__main__":
    unittest.TextTestRunner(verbosity=2).run(suite())
mock链式调用

在django里,我们经常需要mock数据库,而访问数据库时经常是链式调用,看个例子。

def get_person(name):
    return Person.objects.filter(name=name).order_by('age')

有个模块方法,返回数据库中所有指定name的人员,并按age排序

mock掉整个数据库访问

@patch('your.package.module.Person.objects.filter')
def test_should_get_person(self, mock_filter):
    # 先得到一个filter的Mock对象,再在return_value中设置一个Mock对象,此时不需要自己再创建
    mock_filter.return_value.order_by.return_value = None
    
    self.assertIsNone(get_person())
例子: mock掉被测函数调用的其他函数

hello.py

class hello():
    def __init__(self):
        pass

    def getHello(self):
        return "hello"

world.py

class world():
    def __init__(self):
        pass

    def getWorld(self):
        return "world"

helloworld.py

from src.hello import hello
from src.world import world

class helloworld():
    def __init__(self):
        pass
    def helloworld(self):

        h = hello()
        w = world()
        hw = h.getHello()+ " " + w.getWorld()
        return hw

helloworld.py中的helloworld方法对应的单元测试

import unittest
from src.helloworld import *
from unittest.mock import *
import src.hello
import src.world
from unittest import mock

class helloworldTest(unittest.TestCase):
    def setUp(self):
        pass

    @patch.object(hello, 'getHello')
    @patch.object(world, 'getWorld')
    def test_get(self, mock_getWorld, mock_getHello):
        hw = helloworld()
        mock_getHello.return_value="hi"
        mock_getWorld.return_value="bob"
        print(hw.helloworld())
        self.assertEqual(hw.helloworld(), 'hi bob')

    @patch('src.hello.hello.getHello')
    @patch('src.world.world.getWorld')
    def test_get2(self,  mock_world, mock_hello):
        mock_hello.return_value = "lalala"
        mock_world.return_value = "bob"
        hw = helloworld()
        print(hw.helloworld())
        self.assertEqual(hw.helloworld(), 'lalala bob')

#备注: @mock.patch和@patch是一样的
    @mock.patch('src.hello.hello.getHello')
    @mock.patch('src.world.world.getWorld')
    def test_get22(self,  mock_world, mock_hello):
        mock_hello.return_value = "lalala"
        mock_world.return_value = "bob"
        hw = helloworld()
        print(hw.helloworld())
        self.assertEqual(hw.helloworld(), 'lalala bob')

    @patch('src.hello.hello.getHello', MagicMock(return_value = 'lalala'))
    @patch('src.world.world.getWorld', MagicMock(return_value = 'bob'))
    def test_get3(self):
        hw = helloworld()
        print(hw.helloworld())
        self.assertEqual(hw.helloworld(), 'lalala bob')

#备注,下面的代码直接在测试方法内部使用Mock无法达到类似上面的效果
    # def test_get4(self):
    #     h = hello()
    #     h.getHello = Mock(return_value='hi')
    #     w = world()
    #     w.getWorld = Mock(return_value='bob')
    #     hw = helloworld()
    #     self.assertEqual(hw.helloworld(), 'hi bob')

def suite():
    suite = unittest.TestSuite()
    suite.addTests(
        unittest.TestLoader().loadTestsFromTestCase(helloworldTest)
    )

    return suite


if __name__ == "__main__":
    unittest.TextTestRunner(verbosity=2).run(suite())


例子: mock掉被测函数调用的RESTFUL API

restclient.py

import requests
import json
class restclient():
    def __init__(self):
        pass

    def getsomething(self):
        print()
        res = requests.get("http://127.0.0.1:5000/empDB/employee")
        #res.text = {"emps":[{"id":"101","name":"John","title":"Technical Leader"},{"id":"201","name":"Tom","title":"Sr Software Engineer"}]}
        return int(json.loads(res.text)['emps'][0]['id']) + 100

restclientTest.py

import unittest
from unittest.mock import *

import requests
from src.restclient import *


class restclientTest(unittest.TestCase):
    def setUp(self):
        pass

    def test_getsomething_without_mock(self):
        rc = restclient()
        self.assertEqual(rc.getsomething(), 201)


    @patch.object(requests, 'get')
    def test_getsomething_with_mock(self, mockget):
        rc = restclient()
        mockresponse = Mock()
        mockget.return_value = mockresponse
        mockresponse.text = '{"emps":[{"id":"201","name":"John","title":"Technical Leader"},{"id":"201","name":"Tom","title":"Sr Software Engineer"}]}'
        print(rc.getsomething())
        self.assertEqual(rc.getsomething(), 301)



def suite():
    suite = unittest.TestSuite()
    suite.addTests(
        unittest.TestLoader().loadTestsFromTestCase(restclientTest)
    )

    return suite


if __name__ == "__main__":
    unittest.TextTestRunner(verbosity=2).run(suite())

Mock和MagicMock的区别

参考

https://docs.python.org/3.7/library/unittest.mock-examples.html
https://segmentfault.com/a/1190000002965620
https://blog.csdn.net/peiyao456/article/details/77075173
https://blog.csdn.net/wenph2008/article/details/46862771
https://github.com/PacktPublishing/-Hands-on-Test-Driven-Development-with-Python
https://github.com/hjwp/book-example
Test-Driven Development with Python, 2nd Edition
http://flask.pocoo.org/docs/1.0/testing/
https://pybit.es/simple-flask-api.html
https://www.patricksoftwareblog.com/unit-testing-a-flask-application/
https://blog.miguelgrinberg.com/post/unit-testing-asyncio-code
https://treyhunner.com/2014/10/the-many-flavors-of-mock-dot-patch/
https://chase-seibert.github.io/blog/2015/06/25/python-mocking-cookbook.html#
https://www.phizzle.space/python/2017/03/31/practical-unit-testing-mocks-python-3.html
http://www.voidspace.org.uk/python/weblog/arch_d7_2010_10_02.shtml
https://stackoverflow.com/questions/34308511/python-mocking-chained-function-calls
https://stackoverflow.com/questions/7665682/python-mock-object-with-method-called-multiple-times

Books

https://greenteapress.com/wp/think-python/
https://python.swaroopch.com/preface.html
https://github.com/in28minutes/learn-programming-with-python-
https://github.com/PacktPublishing/Learn-Python-Programming-Second-Edition
https://github.com/PacktPublishing/Learn-Python-in-7-Days
https://github.com/EbookFoundation/free-programming-books/blob/master/free-programming-books.md#flask

Unit Test
Web note ad 1