单元测试学习

By liuzhijun, 2014-05-07, 分类: Python技术

fudge, tornado

单元测试是个好东西,但是如果代码中多处有数据库访问(读/写),或者代码中包含一些复杂的对象,真实环境中难以被触发的对象的时候,该如何写单元测试呢?

使用模拟对象机制测试python代码,模拟对象(mock object)可以取代真实对象的位置,用于测试一些与真实对象进行交互或依赖真实对象的功能,模拟对象的目的就是创建一个轻量级的,可控制的对象来代替测试中需要的真实对象,模拟真实对象的行为和功能,方便测试。

Stub和Mock以及Fake的理解

Stub: For replacing a method with code that returns a specified result

简单来说就是可以用stub去fake(伪造)一个方法,阻断原来方法的调用

Mock: A stub with an expectations that the method gets called

简单来说mock就是stub + expectation, 说它是stub是因为它也可以像stub一样伪造方法,阻断对原来方法的调用, expectation是说它不仅伪造了这个方法,还期望你(必须)调用这个方法,如果没有被调用到,这个test就fail了

Fake: objects actually have working implementations, but usually take some shortcut which makes them not suitable for production

简单来说就是一个真实对象的一个轻量级的完整实现

mock对象的使用范畴: 真实对象具有不可确定的行为,产生不可预测的结果(如:天气预报) 真实对象很难被创建 真实对象的某些行为很难被触发

Fudge

Fudge是一个类似于Java中的JMock的纯python的mock测试模块,主要功能就是可以伪造对象,替换代码中真实的对象,来完成测试。fudge主要用来模拟那些在应用中不容易构造或者比较复杂的对象(如项目中涉及mongodb或者redis模块,使用fudge后在测试的时候可以不需要真正的redis环境就能测试代码),从而使测试顺利进行。

如何使用fudge

import twitter_oauth   #pip install twitter_oauth

consumer_key = '***'
consumer_secret = '***'
oauth_token = "***"
oauth_token_secret = '***'


def post_msg_to_twitter(msg):
    # create GetOauth instance
    get_oauth_obj = twitter_oauth.GetOauth(consumer_key, consumer_secret)
    # create Api instance
    api = twitter_oauth.Api(consumer_key, consumer_secret, oauth_token, oauth_token_secret)
    # post update
    api.post_update(u'Hello, Twitter:' + msg)
    print("send:%s" % msg)

因为twitter_oauth是独立的模块,因此只要调用了正确的方法,post_msg_to_twitter方法就一定能正确执行。Twitter在大陆没法直接请求访问,那怎么测试知道它没有问题呢? 使用fudge就能完成我们的任务,把twitter相关的对象伪造(fake)出来,只要我们自己的业务逻辑测试正确,那么测试就通过。

import fudge


@fudge.patch('twitter_oauth.GetOauth', 'twitter_oauth.Api')
def test_post_msg_to_twitter(msg, FakeGetOauth, FakeApi):
    FakeGetOauth.expects_call() \
        .with_args('***', '***')

    FakeApi.expects_call() \
        .with_args('***', '***', '***', '***') \
        .returns_fake() \
        .expects('post_update').with_args(u'Hello, Twitter:okey')

    post_msg_to_twitter(msg)


if __name__ == '__main__':
    test_post_msg_to_twitter('okey')

fudge模块

fudge

更多参考:fudge

fudge.inspector

fudge.inspector.ValueInspector实例可以作为一种更具表现力的对象(Value inspector)传递给fudge.Fake.with_args()方法,为了更方便记忆ValueInspector实例简称为arg

from fudge.inspector import arg
image = fudge.Fake('image').expects('save').with_args(arg.endswith('.jpg'))

上面的测试代码就表示传递给save方法的参数必须是以.jpg结尾的值,否则测试没法通过

更多参考:fudge.inspector

fudge.patcher

tornado.test

由于python的单元测试模块式同步的,测试tornado中的异步代码有三种方式

  1. 使用类似tornado.gen的yield生成器 tornado.testing.gen_test.
    class MyTestCase(AsyncTestCase):
    @tornado.testing.gen_test
    def test_http_fetch(self):
        client = AsyncHTTPClient(self.io_loop)
        response = yield client.fetch("http://www.tornadoweb.org")
        # Test contents of response
        self.assertIn("FriendFeed", response.body)
    
    1. 手工方式调用self.stop,self.wait

      class MyTestCase2(AsyncTestCase): def test_http_fetch(self): client = AsyncHTTPClient(self.io_loop) client.fetch("http://www.tornadoweb.org/", self.stop) response = self.wait() # Test contents of response self.assertIn("FriendFeed", response.body) 3. 回调函数的方式:

      class MyTestCase3(AsyncTestCase): def test_http_fetch(self): client = AsyncHTTPClient(self.io_loop) client.fetch("http://www.tornadoweb.org/", self.handle_fetch) self.wait() def handle_fetch(self, response): #此处产生的异常会通过stack+context传播到self.wait方法中去 self.assertIn("FriendFeed", response.body) self.stop()

后两者的原理是一样的,wait方法会一直运行IOLoop,直到stop方法调用或者超时(timeout默认是5's)2中的fetch的第二个参数self.stop相当于3中的self.handle_fetch,都是一个回调函数,区别就在于把sotp当成回调函数时,响应内容就会通过self.wait()函数返回,而像3中一样写一个自定义的回调函数,响应内容就会作为参数传递给该函数。可以详细查看下tornado.testing.py这个文件中的stop和wait方法。

默认情况下,每个单元会构造一个新的IOLoop实例,这个IOLoop是在构造HTTP clients/servers的时候使用。如果测试需要一个全局的IOLoop,那么就需要重写get_new_ioloop方法。 源码:

def setUp(self):
    super(AsyncTestCase, self).setUp()
    self.io_loop = self.get_new_ioloop()
    self.io_loop.make_current()

def get_new_ioloop(self):
        """Creates a new `.IOLoop` for this test.  May be overridden in
        subclasses for tests that require a specific `.IOLoop` (usually
        the singleton `.IOLoop.instance()`).
        获取全局IOLoop时,调用IOLoop.instance()这个单例方法即可
        """
        return IOLoop()

fakeredis

fakeredisredis-py的实现,模拟redis服务器通信的模块。应用场景只有一个:写单元测试。


关注公众号「Python之禅」(id:vttalk)获取最新文章 python之禅