Python mock的時候偷換對象

最近做發(fā)短信的service的時候,與短信相關的測試需要mock护奈,于是碰到以下問題。
例如有三個模塊a和b和test哥纫。

# a.py:

def send_sms():
    # 調(diào)用運營商的短信接口
    print 'send_sms'


# b.py:

from a import send_sms

def func():
    status_code = send_sms()
    return status_code

# test.py:

from b import func

def dummy_send_sms():
    print 'send_sms'

test_assert(func() == status_success)

a模塊負責發(fā)短信霉旗,b是具體的業(yè)務痴奏,我們想測試b的業(yè)務,但是我們可不想在測試的時候發(fā)短信厌秒。于是我們想在測試的時候读拆,把b模塊func里面對send_sms的調(diào)用改成對一個mock函數(shù)的調(diào)用,如test模塊的dummy_send_sms函數(shù)鸵闪。

先說結(jié)論檐晕,這里要這么干:
# test.py:
import sys
from b import func

def dummy_send_sms():
    print 'send_sms'

sys.modules['b'].__dict__['send_sms'] = dummy_send_sms
# 如果b模塊是import a,然后a.send_sms的話就要這樣
# sys.modules['a'].__dict__['send_sms'] = dummy_send_sms

test_assert(func() == status_success)

或者使用Python的mock庫的patch蚌讼。
可以這么干辟灰,有兩個原因:
首先,Python里面模塊就是一個Python對象篡石,所以我們可以隨時通過篡改這個模塊對象的成員來篡改模塊里面符號與對象的對應關系芥喇。
然后,Python里面對名字的resolution是在運行的時候發(fā)生的凰萨。

具體來說
函數(shù)調(diào)用的字節(jié)碼如下:

LOAD_NAME  0 (send_sms) 
# 0 表示符號表第0個元素继控,也就是字符串"send_sms",然后把"send_sms"對應的對象壓到棧里面(注意Python是基于堆棧的虛擬機胖眷,這里的棧跟調(diào)用棧并不完全一樣)
CALL_FUNCTION 0
# 0表示0個參數(shù)武通,所調(diào)用的函數(shù)就是棧上面的由"send_sms"查到的對象

拿到"send_sms"這個參數(shù),CALL_FUNCTION這條字節(jié)碼怎么去執(zhí)行函數(shù)呢珊搀?
# /Python/ceval.c

TARGET(LOAD_NAME) {
    PyObject *name = GETITEM(names, oparg);
    PyObject *locals = f->f_locals;
    PyObject *v;
    if (locals == NULL) {
        PyErr_Format(PyExc_SystemError,
                     "no locals when loading %R", name);
        goto error;
    }
    if (PyDict_CheckExact(locals)) {
        v = PyDict_GetItem(locals, name);
        Py_XINCREF(v);
    }
    else {
        v = PyObject_GetItem(locals, name);
        if (v == NULL && _PyErr_OCCURRED()) {
            if (!PyErr_ExceptionMatches(PyExc_KeyError))
                goto error;
            PyErr_Clear();
        }
    }
    if (v == NULL) {
        v = PyDict_GetItem(f->f_globals, name);
        Py_XINCREF(v);
        if (v == NULL) {
            if (PyDict_CheckExact(f->f_builtins)) {
                v = PyDict_GetItem(f->f_builtins, name);
                if (v == NULL) {
                    format_exc_check_arg(
                                PyExc_NameError,
                                NAME_ERROR_MSG, name);
                    goto error;
                }
                Py_INCREF(v);
            }
            else {
                v = PyObject_GetItem(f->f_builtins, name);
                if (v == NULL) {
                    if (PyErr_ExceptionMatches(PyExc_KeyError))
                        format_exc_check_arg(
                                    PyExc_NameError,
                                    NAME_ERROR_MSG, name);
                    goto error;
                }
            }
        }
    }
    PUSH(v);
    DISPATCH();
}

這段代碼首先拿出name(也就是"send_sms"這個字符串)冶忱,然后就在ff_localsf_globals境析,f_builtins 里面找相應的對象朗和。
這個f就是當前的棧幀PyFrameObject。

typedef struct _frame {
    PyObject_VAR_HEAD
    # ...
    PyObject *f_builtins;       /* builtin symbol table (PyDictObject) */
    PyObject *f_globals;        /* global symbol table (PyDictObject) */
    PyObject *f_locals;         /* local symbol table (any mapping) */
    # ...
} PyFrameObject;

所以由名字找出對象簿晓,是在LOAD_NAME這條指令運行的時候才計算的,這樣就為我們的篡改留了機會千埃。
所以憔儿,當我們運行了sys.modules['b'].__dict__['send_sms'] = dummy_send_sms之后,LOAD_NAME之后根據(jù)"send_sms"找到的對象就是我們我們篡改的dummy_send_sms放可。

其實谒臼,由于PyFunctionObject擁有個func_globals的指針指向所在模塊的符號表:

typedef struct {
    PyObject_HEAD
    PyObject *func_code;    /* A code object */
    PyObject *func_globals; /* A dictionary (other mappings won't do) */
    PyObject *func_defaults;    /* NULL or a tuple */
    PyObject *func_closure; /* NULL or a tuple of cell objects */
    PyObject *func_doc;     /* The __doc__ attribute, can be anything */
    PyObject *func_name;    /* The __name__ attribute, a string object */
    PyObject *func_dict;    /* The __dict__ attribute, a dict or NULL */
    PyObject *func_weakreflist; /* List of weak references */
    PyObject *func_module;  /* The __module__ attribute, can be anything */

    /* Invariant:
     *     func_closure contains the bindings for func_code->co_freevars, so
     *     PyTuple_Size(func_closure) == PyCode_GetNumFree(func_code)
     *     (func_closure may be NULL if PyCode_GetNumFree(func_code) == 0).
     */
} PyFunctionObject;

所以還可以這么干
# test.py:
import sys
from b import func

def dummy_send_sms():
    print 'send_sms'

# 可以直接從函數(shù)的 func_globals 指針修改符號表
b_globals = getattr(func, '__globals__')
b_globals['send_sms'] = dummy_send_sms

test_assert(func() == status_success)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市耀里,隨后出現(xiàn)的幾起案子蜈缤,更是在濱河造成了極大的恐慌,老刑警劉巖冯挎,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件底哥,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機趾徽,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進店門续滋,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人孵奶,你說我怎么就攤上這事疲酌。” “怎么了建炫?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵颜凯,是天一觀的道長得糜。 經(jīng)常有香客問我,道長粥诫,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任卢鹦,我火速辦了婚禮臀脏,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘冀自。我一直安慰自己揉稚,他們只是感情好,可當我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布熬粗。 她就那樣靜靜地躺著搀玖,像睡著了一般。 火紅的嫁衣襯著肌膚如雪驻呐。 梳的紋絲不亂的頭發(fā)上灌诅,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天,我揣著相機與錄音含末,去河邊找鬼猜拾。 笑死,一個胖子當著我的面吹牛佣盒,可吹牛的內(nèi)容都是我干的挎袜。 我是一名探鬼主播,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼肥惭,長吁一口氣:“原來是場噩夢啊……” “哼盯仪!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起蜜葱,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤全景,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后牵囤,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體爸黄,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡滞伟,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了馆纳。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片诗良。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖鲁驶,靈堂內(nèi)的尸體忽然破棺而出鉴裹,到底是詐尸還是另有隱情,我是刑警寧澤钥弯,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布径荔,位于F島的核電站,受9級特大地震影響脆霎,放射性物質(zhì)發(fā)生泄漏总处。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一睛蛛、第九天 我趴在偏房一處隱蔽的房頂上張望鹦马。 院中可真熱鬧,春花似錦忆肾、人聲如沸荸频。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽旭从。三九已至,卻和暖如春场仲,著一層夾襖步出監(jiān)牢的瞬間和悦,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工渠缕, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留鸽素,地道東北人。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓亦鳞,卻偏偏與公主長得像付鹿,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子蚜迅,可洞房花燭夜當晚...
    茶點故事閱讀 44,577評論 2 353

推薦閱讀更多精彩內(nèi)容

  • ¥開啟¥ 【iAPP實現(xiàn)進入界面執(zhí)行逐一顯】 〖2017-08-25 15:22:14〗 《//首先開一個線程,因...
    小菜c閱讀 6,397評論 0 17
  • 兩本不錯的書: 《Python參考手冊》:對Python各個標準模塊俊抵,特性介紹的比較詳細谁不。 《Python核心編程...
    靜熙老師哈哈哈閱讀 3,360評論 0 80
  • http://python.jobbole.com/85231/ 關于專業(yè)技能寫完項目接著寫寫一名3年工作經(jīng)驗的J...
    燕京博士閱讀 7,571評論 1 118
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,070評論 25 707
  • 10.13 哈哈 這個網(wǎng)速我喜歡 無線網(wǎng)卡到了 從此網(wǎng)速嗖嗖嗖的 哈哈哈哈 2017-10-12 我發(fā)現(xiàn)有些淘寶客...
    顧晴晞閱讀 331評論 0 0