Mock的基本概念和方法

晨曦之光 发布于 2012/03/09 14:15
阅读 881
收藏 1

本博客(http://blog.csdn.net/livelylittlefish )贴出作者(三二一@小鱼)相关研究、学习内容所做的笔记,欢迎广大朋友指正!

Content

0. 序言

1. 本文议题

2. 应该做什么?

3. 如何做?

3.1 方案一

(1) 建立模拟文件

(2) 修改业务逻辑中的调用

(3) 修改make文件

(4) 讨论

3.2 方案二

(1) 建立模拟文件

(2) 基本思想

(3) 修改make文件

(4) 讨论

(5) 该方案的变种

4. 小结

 

0. 序言

 

在软件开发中,我们不可避免的要调用一些外部或者系统级别的接口,然而,我们在测试时,也许这些接口或环境并不存在。比如在对我们自己的模块做单元测试时,发现自己的模块依赖的别的模块或接口还没有建立好,如何测试?

 

Mock概念应运而生,最开始在Java领域,后来各种语言或开发领域均引入该概念。

 

Mock实际上就是一种模拟和控制外部或者系统级别对象或接口的方法。因此,我们在做测试时,尤其是单元测试或覆盖测试时,不必与真实环境交互即可完成对自己的模块业务逻辑的测试,或许自己的模块需要依赖外部环境。

 

因此,我们可以总结

Mock的本质是:模拟(mock)你的(代码),来测我的(代码)

在这里,别人的(代码),或者与硬件相关的(代码),或者暂时未完成的(代码),统称为你的(代码)

 

关于单元测试,各种软件工程书籍,http://en.wikipedia.org/wiki/Unit_testing,及其链接有较详细的解释。

关于Mock对象,可参考《测试驱动开发-Test-Driven Development》第7笔记http://www.mockobjects.comhttp://en.wikipedia.org/wiki/Mock_object,等有较详细的解释。

 

1. 本文议题

 

在本文中,笔者将以文件操作为例,讲述基本的mock概念和方法。本例中,你的代码your_file.h/.c如下。

/*

 * your_file.h

 */

#ifndef _YOUR_FILE_H_

#define _YOUR_FILE_H_

 

#include

 

FILE* your_file_open(char *fname);

void your_file_close(FILE* fp);

 

#endif

Your_file.c是你的代码本来应该有的功能,如打开和关闭文件。

/*

 * your_file.c

 */

#include "your_file.h"

 

FILE* your_file_open(char *fname)

{

    FILE *fp = NULL;

 

    fp = fopen(fname, "r");

    if (fp == NULL)

    {

        printf("Fail to open file!/n");

        return ;

    }

 

    printf("Succeed!/n");

    return fp;

}

 

void your_file_close(FILE* fp)

{

    fclose(fp);

}

首先,做如下假设:

(1) 由于某种原因,这个.c文件(your_file.c)还有问题;

(2) 或者,这个.c文件(your_file.c)还没有完成;

(3) 除此以外,我的业务逻辑代码(my_business.c)依赖your_file.c/.h

(4) 而且,此时我们需要对my_business.c做单元测试,例如,要测试其中的某几个函数等;

 

那么,在这种情况下,我们应该如何测试自己的业务逻辑?即如何测试my_business.c文件?——这将是本文的主要内容。

 

my_business.c业务逻辑如下。为了方便,将main()放在该文件中,实际应用中,main()应该在main.c或者别的启动文件中。

/*

 * my bussiness

 */

 

#include "your_file.h"

#include

 

int read_file()

{

    FILE* fp = your_file_open("data.txt");

    //assert(fp != 0);

 

    /*

     * here my business start, for example, read data from the file.

     * for test, only print the fp.

     */

    printf("%s, %d: file handle = 0x%x/n", __FUNCTION__, __LINE__, (unsigned int)fp);

 

    your_file_close(fp);

    return 0;

}

 

int main(/* int argc, char **argv */)

{

    read_file();

    return 0;

}

注:本文实验对Win32平台和Linux平台均适用。对于make文件Linux平台为makefilewin32平台为make.bat

 

如果代码所在目录下有data.txt文件,其运行结果如下。

# ./my_bussiness

Succeed!

read_file, 16: file handle = 0x8ba2008

2. 应该做什么?

 

在假设的情况下,现在的问题是,your_file.c可能还没有完成或者其他原因不能使用,而且还要对my_business.c中的read_file()进行测试,且my_business.c依赖your_file.c,怎么办?

 

Mock基本概念可知,我们要做的就是模拟(mock)your_file.c文件的功能。

 

3. 如何做?

 

3.1 方案一

 

方法:模拟your_file.h/.c文件,并修改其中的函数名

 

(1) 建立模拟文件

 

模拟your_file.h/.c文件,将其功能的简单实现放在mock_your_file.h/.c文件中,如下。

/*

 * mock_your_file.h

 */

#ifndef _MOCK_YOUR_FILE_H_

#define _MOCK_YOUR_FILE_H_

 

#include

 

FILE* mock_your_file_open(char *fname);

void mock_your_file_close(FILE* fp);

 

#endif

可以看到,我们将该函数名改了。

/*

 * mock_your_file.c

 */

#include "mock_your_file.h"

 

FILE* mock_your_file_open(char *fname)

{

    printf("Succeed!/n");

    return (FILE*)0x94f9008;

}

 

void mock_your_file_close(FILE* fp)

{

    fp = 0;

}

可以看到,在打开文件函数中,我们只是返回一个硬编码的指针。现在,mock_your_file.c是一个单独的模块,用来模拟your_file.c的功能。

 

(2) 修改业务逻辑中的调用

 

函数名改了,我们需要修改my_business.c中的调用。如下。

/*

 * my bussiness

 */

 

#include "mock_your_file.h"  //该包含文件也要修改

 

void read_file()

{

    FILE* fp = mock_your_file_open("data.txt");  //新的函数名

    //assert(fp != 0);

 

    /*

     * here my business start, for example, read data from the file.

     * for test, only print the fp.

     */

    printf("%s, %d: file handle = 0x%x/n", __FUNCTION__, __LINE__, (unsigned int)fp);

 

    mock_your_file_close(fp);  //新的函数名

}

 

int main(/* int argc, char **argv */)

{

    read_file();

    return 0;

}

(3) 修改make文件

 

当然,还需要修改makefile文件。如下。

CXX = gcc

CXXFLAGS += -g -Wall -Wextra

 

TESTS = my_bussiness

 

all : $(TESTS)

 

clean :

rm -f $(TESTS) *.o

 

my_bussiness.o: my_bussiness.c

$(CXX) $(CXXFLAGS) -c $^

 

#your_file.o: your_file.c

mock_your_file.o: mock_your_file.c

$(CXX) $(CXXFLAGS) -c $^

 

$(TESTS): my_bussiness.o mock_your_file.o

$(CXX) $(CXXFLAGS) $^ -o $@

可以看到,以前的your_file.c就不再使用了,换成mock文件。

 

如果是Win32平台,其make.bat文件也要修改。如下。

@echo off

 

echo start to compile all examples

echo.

 

cl /wd 4530 /nologo my_bussiness.c mock_your_file.c

echo.

 

del *.obj

 

echo done. bye.

pause

至此,就达到了模拟your_file.h/.c的目的。

 

(4) 讨论

 

实际上,这是一个较为笨重的方法,因为该方案需要修改的东西太多,比如要修改文件名,函数名,还要修改my_business.c文件中的调用,以及make文件,比较麻烦。

 

一个稍微简单点且不需要修改这么多内容的方法:在模拟文件mock_your_file.h/.c文件中不修改函数名,那么my_business.c就不需要改动,只修改make文件即可。

 

你甚至可以通过目录隔离的方式,不修改模拟文件名、函数名、make文件等,唯一要做的仅仅是修改模拟的函数的内容即可。但这导致代码可能有两套,维护也麻烦。

 

等等,这些方法都是比较肤浅的方法,属于体力活,但实现简单。那么,有没有稍微好一些的方法呢?

 

3.2 方案二

 

方法:使用编译预处理的宏定义,让将fopen函数换个指向,即实际上模拟your_open_file()调用的fopen函数。

 

(1) 建立模拟文件

 

研究your_open_file()函数的代码发现,其调用的fopen()函数返回的FILE*实际上作为your_open_file()函数的返回值返回。那么,能不能模拟fopen()函数呢?——Of course!

 

该方案重新编写的mock文件如下。

/*

 * mock_your_file.h

 */

#ifndef _MOCK_YOUR_FILE_H_

#define _MOCK_YOUR_FILE_H_

 

#include

 

FILE* mock_fopen(const char *fname, const char* option);

void mock_fclose(FILE* fp);

 

#endif

/*

 * mock_your_file.c

 */

#include "mock_your_file.h"

 

FILE* mock_fopen(const char *fname, const char* option)

{

    return (FILE*)0x94f9008;

}

 

void mock_fclose(FILE* fp)

{

    fp = 0;

}

mock_fopen()函数模拟fopen(),直接返回FILE*

 

其他的文件不需要修改,且your_file.h/.c文件仍然使用(区别于方案一),但要修改make文件。

 

(2) 基本思想

 

该方案的基本思想是:使用编译预处理的宏定义功能进行(符号)常量定义,即将fopen看作一个符号常量,定义该常量的值为模拟函数mock_fopen因为mock_your_file.c也会被编译,因此,链接时your_file.c中对fopen的调用便转为对mock_your_file.c中的mock_fopen的调用。

 

What a good idea!

 

(3) 修改make文件

 

该方案的make文件,Linux平台如下。

CXX = gcc

CXXFLAGS += -g -Wall -Wextra

 

TESTS = my_business

 

MOCK_FLAG = -Dfopen=mock_fopen -Dfclose=mock_fclose  #定义符号常量

 

all : $(TESTS)

 

clean :

rm -f $(TESTS) *.o

 

your_file.o: your_file.c

$(CXX) $(CXXFLAGS) $(MOCK_FLAG) -c $^  #编译your_file.c时使用该常量

 

mock_your_file.o: mock_your_file.c

$(CXX) $(CXXFLAGS) -c $^

 

my_business.o: my_business.c

$(CXX) $(CXXFLAGS) -c $^

 

$(TESTS): your_file.o mock_your_file.o my_business.o

$(CXX) $(CXXFLAGS) $^ -o $@

可以看出,3个源文件均参与编译、链接,区别于方案一(your_file.c不再参与)

 

win32平台的make.bat文件为,

@echo off

 

echo start to compile all examples

echo.

 

cl /wd 4996 /nologo /Dfopen=mock_fopen /Dfclose=mock_fclose my_business.c mock_your_file.c your_file.c

echo.

 

del *.obj

 

echo done. bye.

pause

(4) 讨论

 

该方案比较于方案一提出的各种“肤浅”方案,要巧妙的多。其关键之处就是利用编译器的编译预处理的宏定义功能,定义符号常量。将fopen看作符号常量,其值定义为mock_fopen,链接时对符号的resolve处理,即会将对fopen的调用转为对mock_fopen的调用。

 

your_file.c编译后的.o文件的符号表中也能看出端倪。

# objdump -t your_file.o

 

your_file.o:     file format elf32-i386

 

SYMBOL TABLE:

00000000 l    df *ABS*  00000000 your_file.c

00000000 l    d  .text  00000000 .text

00000000 l    d  .data  00000000 .data

00000000 l    d  .bss   00000000 .bss

00000000 l    d  .debug_abbrev  00000000 .debug_abbrev

00000000 l    d  .debug_info    00000000 .debug_info

00000000 l    d  .debug_line    00000000 .debug_line

00000000 l    d  .rodata        00000000 .rodata

00000000 l    d  .debug_frame   00000000 .debug_frame

00000000 l    d  .debug_loc     00000000 .debug_loc

00000000 l    d  .debug_pubnames        00000000 .debug_pubnames

00000000 l    d  .debug_aranges 00000000 .debug_aranges

00000000 l    d  .debug_str     00000000 .debug_str

00000000 l    d  .note.GNU-stack        00000000 .note.GNU-stack

00000000 l    d  .comment       00000000 .comment

00000000 g     F .text  00000055 your_file_open

00000000         *UND*  00000000 mock_fopen

00000000         *UND*  00000000 puts

00000055 g     F .text  00000013 your_file_close

00000000         *UND*  00000000 mock_fclose

如果没有MOCK_FLAG,其编译后的符号表和上述符号表的唯一差别就是这两个符号,分别为fopenfclose

 

(5) 该方案的变种

 

将该符号定义放在Your_file.c文件中,即-D命令行方式的另一种方式。

/*

 * your_file.c

 */

#include "your_file.h"

#include "mock_your_file.h"

#define fopen mock_fopen

#define fclose mock_fclose

 

FILE* your_file_open(char *fname)

{

    FILE *fp = NULL;

 

    fp = fopen(fname, "r");

    if (fp == NULL)

    {

        printf("Fail to open file!/n");

        return 0;

    }

 

    printf("Succeed!/n");

    return fp;

}

 

void your_file_close(FILE* fp)

{

    fclose(fp);

}

1:该变种方法的make文件,需要将makefile/make.bat中的MOCK_FLAG及其使用均删除。

2:参考(3)makefile,编译时只是对your_file.c使用了MOCK_FLAG,因此,只能将宏定义放在your_file.c文件。

3:单步执行会发现,对fopen的调用被resolve到对mock_fopen的调用;同样地,对fclose的调用被resolve到对mock_fclose的调用。

4:若将该宏定义放在my_business.c文件中,则达不到目的,为什么?读者可自行思考。

 

4. 小结

 

本文通过例子讲述了Mock的基本概念和方法,并提出了两种mock方案,比较而言,方案一较为肤浅,但实现简单;虽然方案二相比方案一利用了编译预处理的宏定义的技巧,但若文件过多,mock文件也会多,从而导致make文件的维护工作增加,或者要添加很多宏定义,也难以维护。

 

那么有没有一种比较好的方法,能自动产生一些文件或者目录,供测试使用呢?答案是“有,一定有,因为,这个世界从来不缺聪明的发明人”。笔者将在“Mock的基本概念和方法()”一文讲解使用CmockUnity等工具的方法。

 

Reference

http://www.mockobjects.com

http://en.wikipedia.org/wiki/Mock_object

http://en.wikipedia.org/wiki/Unit_testing

驱动测试开发>,7


Technorati 标签:


原文链接:http://blog.csdn.net/livelylittlefish/article/details/6361359
加载中
返回顶部
顶部