>

自由软件精神——“自由、开放、分享”。自由软件自诞生之日起,就秉承了学术自由的思想,信奉科学无国界,知识应该全人类共享。

ubuntu精神——人道待人,天下共享连接人人的信念。具有 ubuntu 精神的人心胸开阔,乐于助人,见贤思齐而不忌妒贤能......

torando单元测试

本文首先以tornado hello world为例子,说下如何进行tornado单元测试;然后利用mock对mongodb数据库操作进行模拟,采用motor的find_one函数作为mock模拟的例子。

首先写一个tornado的hello world:hello_world.py

__author__ = 'bone-lee'

# from db_class import DBDemo

import tornado.ioloop
import tornado.web
import tornado.gen

class MainHandler(tornado.web.RequestHandler):
    @tornado.web.asynchronous
    def get(self):
        self.write("Hello, world")
        self.finish()

# class DBViewHandler(tornado.web.RequestHandler):
#     @tornado.web.asynchronous
#     @tornado.gen.coroutine
#     def get(self):
#         db_data=yield DBDemo.get_data()
#         self.write("all db data:"+str(db_data))
#         self.finish()

application = tornado.web.Application([
    (r"/", MainHandler),
    # (r"/view", DBViewHandler),
])

if __name__ == "__main__":
    application.listen(8888)
    tornado.ioloop.IOLoop.instance().start()

接下来写针对其进行的测试:test_tornado_hello_world.py

from hello_world import MainHandler
from tornado.web import Application
from tornado import testing

class MyHTTPTest(testing.AsyncHTTPTestCase):
    def get_app(self):
        return Application([('/', MainHandler)])

    def test_homepage(self):
        # The following two lines are equivalent to
        #   response = self.fetch('/')
        # but are shown in full here to demonstrate explicit use
        # of self.stop and self.wait.
        self.http_client.fetch(self.get_url('/'), self.stop)
        response = self.wait()
        # test contents of response
        self.assertEqual("Hello, world", response.body)

这个例子要看懂应该比较简单,tornado单元测试的简单文档见:http://www.tornadoweb.org/en/branch2.3/testing.html,值得说明的是,如果大家不好好去熟悉pyunit这个框架,那么tornaod的AsyncHTTPTestCase(继承自pyunit test case)要想用得很熟很难!因为tornado官方的文档实在太简陋了!

接下来说下如何利用mock进行涉及到数据库的测试!

简单说下mock:就是用来模拟一个函数的返回,比如说一个涉及数据库的find_one函数,使用mock可以伪造一个“和他同名”且"函形参一致"的函数,但是这个函数的返回值是一个常数!而不会去进行真实的数据库操作而涉及tcp通信)!

在此强调一点,在一般的单元测试里,是不会进行一些耗时的(如数据库、网络、文件系统之类)操作的,这些耗时的操作都是通过mock实现!当真正涉及上述耗时调用测试时,那就是集成测试了,一般不在单元测试的范畴之内,虽然单元测试框架也可以做集成测试!

大家在看下面的例子之前,一定要去熟悉下mock的文档:http://www.voidspace.org.uk/python/mock/getting-started.html

并且最重要的,不要漏掉下面几个博客文档,因为大家必然会mock 一个tornado http request来进行测试http handler:

http://www.toptal.com/python/an-introduction-to-mocking-in-python

http://cramer.io/2014/05/20/mocking-requests-with-responses/

http://fgimian.github.io/blog/2014/04/10/using-the-python-mock-library-to-fake-regular-functions-during-tests/

https://www.chicagodjango.com/blog/quick-introduction-mock/

http://blog.isotoma.com/2014/05/using-mock-patch-in-automated-unit-testing/

好了,接下来我直接给出数据库测试demo,数据库程序:db_class.py

__author__ = 'bone-lee'
import motor
from tornado import gen

class DBDemo(object):
    uri = 'mongodb://localhost/test_fbt'
    db = motor.MotorClient(uri).test_db

    @classmethod
    def set_db(cls,db):
        cls.db=db

    @classmethod
    @gen.coroutine
    def gen_data(cls):
        for i in range(10):
            yield cls.db.test_col.insert({"i":i})

    @classmethod
    @gen.coroutine
    def clear(cls):
        yield cls.db.test_col.remove({})

    @classmethod
    @gen.coroutine
    def get_data(cls):
        cursor = cls.db.test_col.find({},{'_id':0})
        all_data = yield cursor.to_list(None)
        raise gen.Return(all_data)

    @classmethod
    @gen.coroutine
    def do_sth_with_db(cls):
        found = yield cls.db.test_col.find_one({},{'_id':0})
        # print "found:",found
        if found:
            found["is_male"]=1
            raise gen.Return(found)
        else:
            raise gen.Return(None)

 

测试文件:test_motor.py

下面的代码使用了真实的数据库进行测试,所以测试的时候需要本地开mongod!下面的代码其实应算是一个集成测试!

 

# -*- coding: utf-8 -*-
from db_class import DBDemo

from motor import MotorClient
from tornado import testing

class DBTestCase(testing.AsyncTestCase):
    def setUp(self):
        super(DBTestCase, self).setUp()
        db=MotorClient('localhost', 27017, io_loop=self.io_loop).test_db
        DBDemo.set_db(db)

    def tearDown(self):
        super(DBTestCase,self).tearDown()

    @testing.gen_test
    def test_db_gen_data(self):
        yield DBDemo.clear()
        yield DBDemo.gen_data()
        data = yield DBDemo.get_data()
        self.assertEqual(len(data), 10)
        yield DBDemo.clear()
        self.stop()

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

 

使用mock进行测试的文件:test_mock_motor.py

# -*- coding: utf-8 -*-
from db_class import DBDemo

from motor import MotorClient
from tornado import testing,gen
import mock
from tornado.concurrent import Future

# 演示path __name__ 用
@gen.coroutine
def db_find():
    # do something with db and return the result
    # ...
    raise gen.Return({"name":"bone","age":22})

class MockDBTestCase(testing.AsyncTestCase):
    # @testing.gen_test
    # def test_pass_callback(self):
    #     @gen.coroutine
    #     def f():
    #         raise gen.Return(42)
    #     result = yield gen.Task(f)
    #     self.assertEqual(result, 42)
    #     self.stop()

    # mock 本文件内的函数
    # 只是为了演示下path __name__
    @mock.patch(__name__ + '.db_find')
    @testing.gen_test
    def test_mock(self,mock_db_find):
        future_1 = Future()
        future_1.set_result({"name":"bone","age":11})
        mock_db_find.return_value = future_1
        result = yield db_find()
        self.assertEqual(result, {"name":"bone","age":11})
        self.stop()

    #mock motor数据库find_one操作
    @mock.patch('db_class.motor.MotorCollection.find_one',autospec=True)
    @testing.gen_test
    def test_db_find_mock(self,mock_motor_find):
        future_1 = Future()
        future_1.set_result({"name":"jack","age":11})
        mock_motor_find.return_value = future_1
        result = yield DBDemo.do_sth_with_db()
        self.assertEqual(result, {"name":"jack","age":11,"is_male":1})
        self.stop()

    # 为了达到测试覆盖率,必须mock一个find_one为None的结果
    @mock.patch('db_class.motor.MotorCollection.find_one',autospec=True)
    @testing.gen_test
    def test_db_find_mock2(self,mock_motor_find):
        future_1 = Future()
        future_1.set_result(None)
        mock_motor_find.return_value = future_1
        result = yield DBDemo.do_sth_with_db()
        self.assertEqual(result, None)
        self.stop()

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

 

上述针对db_class中DBDemo类的测试覆盖率是100%!

值得说明的是:

 

注意:为了覆盖do_sth_with_db中的if分支:

        if found:
            found["is_male"]=1
            raise gen.Return(found)
        else:
            raise gen.Return(None)

而写了test_db_find_mock,test_db_find_mock2二个测试函数,一个mock返回了一个伪造的正常结果,{"name":"jack","age":11},而另外一个返回了None!

如果连接真实的数据库进行测试,大家必然需要在本地开启mongod服务,并且伪造一些测试数据,为了测试find_one为None的情形还需要clear数据库,最后在测试完毕还需要删除该测试数据库!

而单元测试使用mock来绕过这些麻烦的东西!大家可能有疑问,万一自己find_one的传入参数错误(比如函数少了一个参数),虽然mock成功返回了模拟数据,但在真实环境却导致数据库调用失败,mock能够检测出这类错误吗?答案是不能!这些错误只能开发者自己去规避,或者集成测试发现!mock的主要职责是:以一个模拟值来返回那些耗时的如数据库、网络API调用,从而验证你除开那些API调用之外的代码逻辑!

大家如果有什么疑问,可以随时留言交流!

 

 

 

 

Meta

发布: 二月 27, 2015

作者: Bone Lee 李智华

评论:  

字数统计: 1473

下一篇: Google Python风格指南

上一篇: tornado gen_test含义

Bookmark and Share

Tags

blog DB mongodb python test tornado

文章链接

  1. http://www.tornadoweb.org/en/branch2.3/testing.html
  2. Getting Started with Mock — Mock 1.0.1 documentation
  3. Mocking in Python: A Guide to Better Unit Tests | ...
  4. Page not found · GitHub Pages
  5. Using the Python mock library to fake regular functions during ...
  6. https://www.chicagodjango.com/blog/quick-introduction-mock/
  7. Using mock.patch in automated unit testing | Isotoma: Our blog
comments powered by Disqus

返回顶部