CuriousY A world with wonder

Mock patching for 'from/import' statement in Python

| Comment

最近研究了下Python的mock库,实际用的时候碰到了这样的坑:

我有三个py文件分别放了测试代码,被测试的代码以及被测试代码中调用的方法:

mylib.py:

class Foo(object):
    def print_foo(self):
        return 'foo'

myfunc.py:

from mylib import Foo

def some_function():
    instance = Foo()
    return instance.print_foo()

test_myfunc.py:

from myfunc import some_function

def test_some_function():
    # Will create a MagicMock object to replace class `Foo`
    with patch("mylib.Foo") as MockFoo:
        mock_instance = MockFoo.return_value
        mock_instance.print_foo.return_value = 'bar'
        result = some_function()
        assert result == 'bar'

然而实际运行时,some_function中的Foo类仍然是mylib.Foo对象,而不是我生成的MagicMock对象,也就是说mock在这里并没有起到替换被测代码中对象的作用。

Why

原因其实和Python的import机制有关(之前也碰到过类似的问题):

通过from/import来import的对象其实是在此模块中创建了一个import对象的引用来指向这个对象

所以上述代码中myfunc.py中的Foo其实在全局空间是myfunc.Foo,而test_myfunc.py中我是对mylib.Foo进行了patch,所以实际看上去并没有替换被测代码中的Foo对象。

Solution

正确地打好patch即可:

from myfunc import some_function

def test_some_function():
    # Will create a MagicMock object to replace class `Foo`
    with patch("myfunc.Foo") as MockFoo:
        mock_instance = MockFoo.return_value
        mock_instance.print_foo.return_value = 'bar'
        result = some_function()
        assert result == 'bar'

Conclusion

使用mock的patch时一定要注意where to patch的问题。

Play Python Library之mock

| Comment

Why use mock

mock,库如其名,是一个用来伪造对象的库 (🙃)。

想象一个这样的场景:

你开发了一个比较复杂的程序,它每次都需要读取外部一个数据库中的数据,后续的操作都会根据这个读取的数据来进行。

面对这样的程序,你会如何写测试的代码?

是将依赖的数据库信息在测试代码中也定义一份?还是伪造一份数据库中的数据,然后hack掉读数据的部分,直接用伪造的数据用于后续操作的测试?

OK,以上两种方式都是可以达到测试的效果的。BUT,前者的测试代码会依赖外部的数据库,如果数据库服务器发生变化,那么测试代码也要跟着变;而且如果涉及数据库的写入,为了不影响开发环境的数据库,可能需要单独搭建一个测试用的数据库;即使搭建了测试用的数据库,如果开发用的数据库配置发生变化,测试这边也得跟着变,想想就好麻烦。而后者使用Hack的方式来取代掉外部的数据库虽然可行,但测试代码会变得难以维护。

解决这个问题还有一个出路,那就是用mock库中的MagicMock对象来模拟和伪造一个数据库对象,它帮你干了你要干的hack的活,用这种方式要比直接hack源代码要更易于维护。

Play with mock

MagicMock

简单提几点:

  • 访问MagicMock类型的对象中未定义的属性会创建一个新MagicMock并绑定到这个属性。
  • MagicMock类型有一些特殊的属性:
    • 通过设定MagicMock类型对象的return_value属性,会把这个属性的值绑定为__call__方法的返回值。
    • 通过设定MagicMock类型对象的side_effect属性,可以达到使__call__方法根据输入来返回不同值的效果。
Read more

记坑:Redirect page after submit

| Comment

写了一个表单页面,想要用户点击完提价按钮后跳转到另外一个页面。这个按钮的js就是一个简单的ajax请求:

$("#register").click(function (event) {
    event.preventDefault();
    var request = $.ajax({
        url: "/_backend/register",
        type: 'POST',
        contentType: "application/json",
        dataType: "json",
        data: JSON.stringify({
            output_method: $("#output_method").val(),
            send_speed: $("#send_speed").val(),
            receive_email: $("#receive_email").val()
        })
    });
});

后端是Flask写的,大概是这样的:

@app.route('/_backend/register', methods=['POST'])
def register_splunk():
    data = request.json
    user_id = session.get('user_id')
    task_id = str(time.time()).replace('.', '')
    target_context = {'send_speed': int(data['send_speed']), 'receive_email': data['receive_email']}
    SERVICE.create_task(task_id, task_script_name=data['output_method'], task_interval=-1,
                             other_context=target_context, production_task=False)
    return redirect(url_for('/task_details/{0}'.format(task_id)))

然而实际运行时并没有发生页面跳转,而是点击完提交按钮后什么都没有发生(因为添加了event.preventDefault())。

Read more

Python中的信号机制初窥

| Comment

Start with an example

A piece of code

先看一段代码:

from multiprocessing import Process, Manager
from time import sleep

def f(process_number):
    try:
        print "starting thread: ", process_number
        while True:
            print process_number
            sleep(3)
    except KeyboardInterrupt:
        print "Keyboard interrupt in process: ", process_number
    finally:
        print "cleaning up thread", process_number

if __name__ == '__main__':
    processes = []
    manager = Manager()
    for i in xrange(4):
        p = Process(target=f, args=(i,))
        p.start()
        processes.append(p)

    try:
        for process in processes:
            process.join()
    except KeyboardInterrupt:
        print "Keyboard interrupt in main"
    finally:
        print "Cleaning up Main"

运行后按ctrl-c,可以看到:

^C
Keyboard interrupt in process:  3
Keyboard interrupt in process:  0
Keyboard interrupt in process:  2
cleaning up thread 3
cleaning up thread 0
cleaning up thread 2
Keyboard interrupt in process:  1
cleaning up thread 1
Keyboard interrupt in main
Cleaning up Main

可以看到这里利用KeyboardInterrupt使所有进程都得以比较优雅地退出。这种退出进程的方法确实值得学习,但它仅限于进程是通过ctrl-c的方式结束的情况,假如我调用的是Process.terminate()方法来结束这些进程的话,该如何优雅地退出呢?

Read more

The 'url_for' in the Flask

| Comment

一直没太明白为啥Flask中import站内的资源需要写成这样:

<link rel="stylesheet" type="text/css" href="{{url_for('static',filename='css/main.css')}}">

直到最近用Jinja2Twisted写一个web server,才发现这样做是有道理的。

一开始我是直接import相对路径的:

<link rel="stylesheet" type="text/css" href="static/css/main.css">

这样看着很舒服。BUT,接下去就发现问题了。

假设我访问的是http://localhost:5000,import这些资源是没有问题的,因为相对路径解析下来正好是http://localhost:5000/static/css/main.css。但如果访问http://localhost:5000/pathA,会发现路径解析为了http://localhost:5000/pathA/static/css/main.css,也就是说相对路径永远是加在当前访问的路径后面。

OK,那我就用绝对路径吧,把上述import改写为:

<link rel="stylesheet" type="text/css" href="http://myhostname:5000/static/css/main.css">

这样肯定是没有问题的,但问题是这里的hostname和port都被hard code到了页面里,比较蠢。

所以Flask就提供了一个url_for方法,用来把相对路径转换为绝对路径,再import站内资源。又因为html本身是静态的,所以需要把url_for方法放在Jinja2的模板里面,经过处理后把绝对路径再填到html里面。

| Page 20 of 25 |