• <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>

            陳碩的Blog

            C++ 工程實(shí)踐(6):?jiǎn)卧獪y(cè)試如何 mock 系統(tǒng)調(diào)用

            陳碩 (giantchen_AT_gmail)

            Blog.csdn.net/Solstice

            陳碩關(guān)于 C++ 工程實(shí)踐的系列文章: http://blog.csdn.net/Solstice/category/802325.aspx

            陳碩博客文章合集下載: http://blog.csdn.net/Solstice/archive/2011/02/24/6206154.aspx

            本作品采用“Creative Commons 署名-非商業(yè)性使用-禁止演繹 3.0 Unported 許可協(xié)議(cc by-nc-nd)”進(jìn)行許可。http://creativecommons.org/licenses/by-nc-nd/3.0/

            摘要:本文討論了在編寫單元測(cè)試時(shí) mock 系統(tǒng)調(diào)用(以及其他第三方庫(kù))的幾種做法。

            本文只考慮 Linux x86/amd64 平臺(tái)。

            陳碩在《分布式程序的自動(dòng)化回歸測(cè)試》 http://blog.csdn.net/Solstice/archive/2011/04/25/6359748.aspx 一文中曾經(jīng)談到單元測(cè)試在分布式程序開(kāi)發(fā)中的優(yōu)缺點(diǎn)(好吧,主要是缺點(diǎn))。但是,在某些情況下,單元測(cè)試是很有必要的,在測(cè)試 failure 場(chǎng)景的時(shí)候尤顯重要,比如:

            • 在開(kāi)發(fā)存儲(chǔ)系統(tǒng)時(shí),模擬 read(2)/write(2) 返回 EIO 錯(cuò)誤(有可能是磁盤寫滿了,有可能是磁盤出壞道讀不出數(shù)據(jù))。
            • 在開(kāi)發(fā)網(wǎng)絡(luò)庫(kù)的時(shí)候,模擬 write(2) 返回 EPIPE 錯(cuò)誤(對(duì)方意外斷開(kāi)連接)。
            • 在開(kāi)發(fā)網(wǎng)絡(luò)庫(kù)的時(shí)候,模擬自連接 (self-connection),網(wǎng)絡(luò)庫(kù)應(yīng)該用 getsockname(2) 和 getpeername(2) 判斷是否是自連接,然后斷開(kāi)之。
            • 在開(kāi)發(fā)網(wǎng)絡(luò)庫(kù)的時(shí)候,模擬本地 ephemeral port 用完,connect(2) 返回 EAGAIN 臨時(shí)錯(cuò)誤。
            • 讓 gethostbyname(2) 返回我們預(yù)設(shè)的值,防止單元測(cè)試給公司的 DNS server 帶來(lái)太大壓力。

            這些 test case 恐怕很難用前文提到的 test harness 來(lái)測(cè)試,該單元測(cè)試上場(chǎng)了。現(xiàn)在的問(wèn)題是,如何 mock 這些系統(tǒng)函數(shù)?或者換句話說(shuō),如何把對(duì)系統(tǒng)函數(shù)的依賴注入到被測(cè)程序中?

            系統(tǒng)函數(shù)的依賴注入

            在Michael Feathers 的《修改代碼的藝術(shù) / Working Effectively with Legacy Code》一書(shū)第 4.3.2 節(jié)中,作者介紹了鏈接期接縫(link seam),正好可以解決我們的問(wèn)題。另外,在 Stack Overflow 的一個(gè)帖子里也總結(jié)了幾種做法:http://stackoverflow.com/questions/2924440/advice-on-mocking-system-calls

            如果程序(庫(kù))在編寫的時(shí)候就考慮了可測(cè)試性,那么用不到上面的 hack 手段,我們可以從設(shè)計(jì)上解決依賴注入的問(wèn)題。這里提供兩個(gè)思路。

            其一,采用傳統(tǒng)的面向?qū)ο蟮氖址ǎ柚\(yùn)行期的遲綁定實(shí)現(xiàn)注入與替換。自己寫一個(gè) System interface,把程序里用到的 open、close、read、write、connect、bind、listen、accept、gethostname、getpeername、getsockname 等等函數(shù)統(tǒng)統(tǒng)用虛函數(shù)封裝一層。然后在代碼里不要直接調(diào)用 open(),而是調(diào)用 System::instance().open()。

            這樣代碼主動(dòng)把控制權(quán)交給了 System interface,我們可以在這里動(dòng)動(dòng)手腳。在寫單元測(cè)試的時(shí)候,把這個(gè) singleton instance 替換為我們的 mock object,這樣就能模擬各種 error code。

            其二,采用編譯期或鏈接期的遲綁定。注意到在第一種做法中,運(yùn)行期多態(tài)是不必要的,因?yàn)槌绦驈纳剿乐粫?huì)用到一個(gè) implementation object。為此付出虛函數(shù)調(diào)用的代價(jià)似乎有些不值。(其實(shí),跟系統(tǒng)調(diào)用比起來(lái),虛函數(shù)這點(diǎn)開(kāi)銷可忽略不計(jì)。)

            我們可以寫一個(gè) system namespace 頭文件,在其中聲明 read() 和 write() 等普通函數(shù),然后在 .cc 文件里轉(zhuǎn)發(fā)給對(duì)應(yīng)系統(tǒng)的系統(tǒng)函數(shù) ::read() 和 ::write() 等。

            // SocketsOps.h
            namespace sockets
            {
            int connect(int sockfd, const struct sockaddr_in& addr);
            }
            // SocketsOps.cc
            int sockets::connect(int sockfd, const struct sockaddr_in& addr)
            {
            return ::connect(sockfd, sockaddr_cast(&addr), sizeof addr);
            }
            
            此處的代碼來(lái)自 muduo 網(wǎng)絡(luò)庫(kù)

            http://code.google.com/p/muduo/source/browse/trunk/muduo/net/SocketsOps.h
            http://code.google.com/p/muduo/source/browse/trunk/muduo/net/SocketsOps.cc

            有了這么一層間接性,就可以在編寫單元測(cè)試的時(shí)候動(dòng)動(dòng)手腳,鏈接我們的 stub 實(shí)現(xiàn),以達(dá)到替換實(shí)現(xiàn)的目的:

            // MockSocketsOps.cc
            int sockets::connect(int sockfd, const struct sockaddr_in& addr)
            {
            errno = EAGAIN;
            return -1;
            }

            C++ 一個(gè)程序只能有一個(gè) main() 入口,所以要先把程序做成 library,再用單元測(cè)試代碼鏈接這個(gè) library。假設(shè)有一個(gè) mynetcat 程序,為了編寫 C++ 單元測(cè)試,我們把它拆成兩部分,library 和 main(),源文件分別是 mynetcat.cc 和 main.cc。

            在編譯普通程序的時(shí)候:

            g++ main.cc mynetcat.cc SocketsOps.cc -o mynetcat
            

            在編譯單元測(cè)試時(shí)這么寫:

            g++ test.cc mynetcat.cc MockSocketsOps.cc -o test

            以上是最簡(jiǎn)單的例子,在實(shí)際開(kāi)發(fā)中可以讓 stub 功能更強(qiáng)大一些,比如根據(jù)不同的 test case 返回不同的錯(cuò)誤。這么做無(wú)需用到虛函數(shù),代碼寫起來(lái)也比較簡(jiǎn)潔,只用前綴 sockets:: 即可。例如應(yīng)用程序的代碼里寫 sockets::connect(fd, addr)。

            muduo 目前還沒(méi)有單元測(cè)試,只是預(yù)留了這些 stubs。

            namespace 的好處在于它不是封閉的,我們可以隨時(shí)打開(kāi)往里添加新的函數(shù),而不用改動(dòng)原來(lái)的頭文件(該文件的控制權(quán)可能不在我們手里)。這也是以 non-member non-friend 函數(shù)為接口的優(yōu)點(diǎn)。


            以上兩種做法還有一個(gè)好處,即只 mock 我們關(guān)心的部分代碼。如果程序用到了 SQLite 或 Berkeley DB 這些會(huì)訪問(wèn)本地文件系統(tǒng)的第三方庫(kù),那么我們的 System interface 或 system namespace 不會(huì)攔截這些第三方庫(kù)的 open(2)、close(2)、read(2)、write(2) 等系統(tǒng)調(diào)用。

            鏈接期墊片 (link seams)

            如果程序在一開(kāi)始編碼的時(shí)候沒(méi)有考慮單元測(cè)試,那么又該如何注入 mock 系統(tǒng)調(diào)用呢?上面第二種做法已經(jīng)給出了答案,那就是使用 link seam (鏈接期墊片)。

            比方說(shuō)要 mock connect(2) 函數(shù),那么我們自己在單元測(cè)試程序里實(shí)現(xiàn)一個(gè) connect() 函數(shù),在鏈接的時(shí)候,會(huì)優(yōu)先采用我們自己定義的函數(shù)。(這對(duì)動(dòng)態(tài)鏈接是成立的,如果是靜態(tài)鏈接,會(huì)報(bào)  multiple definition 錯(cuò)誤。好在絕大多數(shù)情況下 libc 是動(dòng)態(tài)鏈接的。)

            typedef int (*connect_func_t)(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
            connect_func_t connect_func = dlsym(RTDL_NEXT, "connect");
            bool mock_connect;
            int mock_connect_errno;
            // mock connect
            extern "C" int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen)
            {
            if (mock_connect) {
            errno = mock_connect_errno;
            return errno == 0 ? 0 : -1;
            } else {
            return connect_func(sockfd, addr, addrlen);
            }
            }
            


            如果程序真的要調(diào)用 connect(2) 怎么辦?在我們自己的 mock connect(2) 里不能再調(diào)用 connect() 了,否則會(huì)出現(xiàn)無(wú)限遞歸。為了防止這種情況,我們用 dlsym(RTDL_NEXT, "connect") 獲得 connect(2) 系統(tǒng)函數(shù)的真實(shí)地址,然后通過(guò)函數(shù)指針 connect_func 來(lái)調(diào)用它。

            例子:ZooKeeper 的 C client library

            ZooKeeper 的 C client library 正是采用了 link seams 來(lái)編寫單元測(cè)試,代碼見(jiàn):

            http://svn.apache.org/repos/asf/zookeeper/tags/release-3.3.3/src/c/tests/LibCMocks.h
            http://svn.apache.org/repos/asf/zookeeper/tags/release-3.3.3/src/c/tests/LibCMocks.cc

            其他手法

            Stack Overflow 的帖子里還提到一個(gè)做法,可以方便地替換動(dòng)態(tài)庫(kù)里的函數(shù),即使用 ld 的 --wrap 參數(shù),
            文檔里說(shuō)得很清楚,這里不再贅述。

                   --wrap=symbol
            Use a wrapper function for symbol.  Any undefined reference to
            symbol will be resolved to "__wrap_symbol".  Any undefined
            reference to "__real_symbol" will be resolved to symbol.
            This can be used to provide a wrapper for a system function.  The
            wrapper function should be called "__wrap_symbol".  If it wishes to
            call the system function, it should call "__real_symbol".
            Here is a trivial example:
            void *
            __wrap_malloc (size_t c)
            {
            printf ("malloc called with %zu\n", c);
            return __real_malloc (c);
            }
            If you link other code with this file using --wrap malloc, then all
            calls to "malloc" will call the function "__wrap_malloc" instead.
            The call to "__real_malloc" in "__wrap_malloc" will call the real
            "malloc" function.
            You may wish to provide a "__real_malloc" function as well, so that
            links without the --wrap option will succeed.  If you do this, you
            should not put the definition of "__real_malloc" in the same file
            as "__wrap_malloc"; if you do, the assembler may resolve the call
            before the linker has a chance to wrap it to "malloc".
            

            第三方 C++ 庫(kù)

            link seam 同樣適用于第三方 C++ 庫(kù)

            比方說(shuō)公司某個(gè)基礎(chǔ)庫(kù)團(tuán)隊(duì)提供了了 File class,但是這個(gè) class 沒(méi)有使用虛函數(shù),我們無(wú)法通過(guò) sub-classing 的辦法來(lái)實(shí)現(xiàn) mock object。

            class File : boost::noncopyable
            {
            public:
            File(const char* filename);
            ~File();
            int readn(void* data, int len);
            int writen(const void* data, int len);
            size_t getSize() const;
            private:
            };

            如果需要為用到 File class 的程序編寫單元測(cè)試,那么我們可以自己定義其成員函數(shù)的實(shí)現(xiàn),這樣可以注入任何我們想要的結(jié)果。

            // MockFile.cc
            int File::readn(void* data, int len)
            {
            return -1;
            }
            

            (這個(gè)做法對(duì)動(dòng)態(tài)庫(kù)是可行的,靜態(tài)庫(kù)會(huì)報(bào)錯(cuò)。我們要么讓對(duì)方提供專供單元測(cè)試的動(dòng)態(tài)庫(kù),要么拿過(guò)源碼來(lái)自己編譯一個(gè)。)


            Java 也有類似的做法,在 class path 里替換我們自己的 stub jar 文件,以實(shí)現(xiàn) link seam。不過(guò) Java 有動(dòng)態(tài)代理,很少用得著 link seam 來(lái)實(shí)現(xiàn)依賴注入。

            posted on 2011-05-16 00:18 陳碩 閱讀(3565) 評(píng)論(4)  編輯 收藏 引用

            評(píng)論

            # re: C++ 工程實(shí)踐(6):?jiǎn)卧獪y(cè)試如何 mock 系統(tǒng)調(diào)用 2011-05-16 09:05 beat by dre

            我學(xué)的是軟件專業(yè) 但現(xiàn)在真的做著很多與軟件無(wú)關(guān)的事情 我會(huì)回來(lái)的 我付出三年學(xué)習(xí)的東西我會(huì)改變我自己  回復(fù)  更多評(píng)論   

            # beat by dre 2011-05-16 10:35 beat by dre

            I really enjoy my visits to your blog....  回復(fù)  更多評(píng)論   

            # re: C++ 工程實(shí)踐(6):?jiǎn)卧獪y(cè)試如何 mock 系統(tǒng)調(diào)用 2011-05-16 12:29 陳梓瀚(vczh)

            直接往一塊軟盤寫,很容易就囧掉的啦。  回復(fù)  更多評(píng)論   

            # re: C++ 工程實(shí)踐(6):?jiǎn)卧獪y(cè)試如何 mock 系統(tǒng)調(diào)用 2011-05-16 17:50 goedkope nike schoenen

            When you present your work, you present your self!thank you for the

            informative post and keep up the good work  回復(fù)  更多評(píng)論   


            只有注冊(cè)用戶登錄后才能發(fā)表評(píng)論。
            網(wǎng)站導(dǎo)航: 博客園   IT新聞   BlogJava   博問(wèn)   Chat2DB   管理


            <2025年7月>
            293012345
            6789101112
            13141516171819
            20212223242526
            272829303112
            3456789

            導(dǎo)航

            統(tǒng)計(jì)

            常用鏈接

            隨筆分類

            隨筆檔案

            相冊(cè)

            搜索

            最新評(píng)論

            閱讀排行榜

            評(píng)論排行榜

            亚洲欧美日韩中文久久 | 模特私拍国产精品久久| 激情综合色综合久久综合| 品成人欧美大片久久国产欧美... 品成人欧美大片久久国产欧美 | 亚洲国产精品综合久久一线| 久久久久久久久久久| 久久精品国产免费| 久久久WWW免费人成精品| 久久青青草原亚洲av无码app| 欧美一区二区精品久久| 伊色综合久久之综合久久| 久久亚洲春色中文字幕久久久| 97精品国产97久久久久久免费| 国产精品久久久久蜜芽| 亚洲国产精品人久久| 亚洲精品乱码久久久久久| 女同久久| 91精品国产高清久久久久久国产嫩草 | 国产精品免费福利久久| 无码人妻少妇久久中文字幕| 国产精品久久久久久久久鸭| 国产69精品久久久久观看软件 | 久久精品国产精品亚洲下载| 97超级碰碰碰久久久久| 中文字幕久久亚洲一区| 久久久久国色AV免费看图片| 免费观看成人久久网免费观看| 久久AV高清无码| 性欧美丰满熟妇XXXX性久久久 | 欧美日韩精品久久久免费观看| 久久久91精品国产一区二区三区 | 久久99这里只有精品国产| 久久久青草青青亚洲国产免观| 亚洲精品美女久久久久99| 亚洲精品第一综合99久久 | 国产成人精品久久亚洲| 波多野结衣中文字幕久久 | 久久久久免费精品国产| 国产一区二区精品久久凹凸| 久久夜色精品国产亚洲| 91久久国产视频|