接口的另一用法-构建单元测试的Mock对象

本文介绍如何在C#中使用Mock对象进行单元测试,通过创建Mock对象模拟真实对象的行为,以便于测试依赖这些对象的代码。文中详细展示了使用DotNetMock框架创建ILogger接口的Mock对象的过程。
      Interface在C#中的使用频率是非常高的,为了满足面向对象的需求,接口声明了对象所具有的行为,利用其可以实现具有可扩展性的类,例如可以通过返回接口,返回具体的类,而隐藏类的实现细节,并且仅仅暴露该接口所支持的属性和方法。又或者可以通过接口传递参数,利用多态实现系统的可扩展性。这里,我想演示的是,使用接口创建Mock对象。
首先解释下什么是Mock对象,这就好比拍电影的时候的替身演员,比如要完成一些高难度的动作,需要具有特殊技能的人代替真正的演员。Mock对象就是这个意思。在实际单元测试中,所需要的对象可能创建代价特别高或者不稳定,比如数据库连接,或者网络连接,这样一些不确定的因素在测试中会影响测试结果,在做团队开发的时候我们常常也可以听到这样的抱怨:“把服务器打开,我这边要测试..。”实际上测试的并不是底层连接,而是返回的数据是否能在被测的方法中正常运行。呵呵,或许您又有办法了,在方法里面注释掉从数据库读数据的部分,给变量手动赋值,Well good idea. 那么面对成千上万的代码,你完成了测试你还得把测试代码擦干净。 Oh, 下次测试怎么办?再加?#%#◎◎×。另外一点,有时候你想测的代码引用了还没开发出来的对象,仅仅是用它的接口。由此,你更需要用Mock来测试了。
 
下面说说怎么做:
1.我们可以按照这个原理自己手写Mock对象,但是我们也可以使用工具,这里使用一个开源Mock框架DotNetMock(http://sourceforge.net/projects/dotnetmock)。
2.   首先,你应该估计出Mock对象是什么样子了。我们可以手动的写写这个对象。
假设有个类,它提供记录日志的功能,如下:
 
public class DataAccessor     {         ILogger _logger;         public DataAccessor(ILogger logger)         {             _logger = logger;         }         public void LogData()         {             _logger.SetName("DataAccessor");             _logger.Log("new log1");             _logger.Log("new log2");                      }     }
我们创建了ILogger确定接口方法。并作为构造函数参数传入。ILogger接口如下
 
public class Logger : ILogger     {         #region ILogger Members         public void SetName(string name)         {             return;         }         public void Log(string logVal)         {             return;         }         #endregion     }
 
接着,写我们的MockLogger,我们把它放置在我们的测试类中,并放在一个不同的文件里,千万不要和产品代码放在一起从而导致客户的抱怨。然后,为了使用这个开源框架,我们添加引用DotNetMock, MockLogger不仅继承于ILogger,还继承于MockObject:
 
public class MockLogger : MockObject, ILogger     {         private ExpectationValue _name = new ExpectationValue("name");         private ExpectationArrayList _msgs = new ExpectationArrayList("msgs");         public string ExpectedName         {             set { _name.Expected = value; }         }         public void AddExpectedMsg(string msg)         {             _msgs.AddExpected(msg);         }         #region ILogger Members         public void SetName(string name)         {             _name.Actual = name;         }         public void Log(string logVal)         {             _msgs.AddActual(logVal);         }         #endregion     }
按照这里的需要,我们用DotNetMock 提供的ExpectationValue和ExpectationArrayList用来存放Logger的名称以及它记录的字符串集合。并添加了两个方法。
 
最后,我们在Test框架中使用这个测试类,这里用的是NUnit:
 
   [TestFixture, Category("Full")]     public class TestClass     {         [Test]         public void TestLog()         {             MockLogger logger = new MockLogger();             DataAccessor acc = new DataAccessor(logger);             acc.LogData();             logger.ExpectedName = "DataAccessor";             logger.AddExpectedMsg("new log1");             logger.AddExpectedMsg("new log2");             logger.Verify();         }     }
 
那个Verify()方法帮我们完成了测试。OK, 我们的替身做好了,也测试了。可能真正的Logger还没开发呢,但是我们已经可以测试DataAccessor的方法了。同样的,我们可以创建数据库连接等Mock对象,但是等等,其实我们可以更好的使用这个DotNetMock框架,试试使用DotNetMock.Framework下的Data中已经有的Mock对象吧。
 
另外,可能你会问那么如果需要Mock的对象有几千个方法,我只用其中几个,那我不疯掉?其实有一种叫做动态Mock的对象是不需要你全部写满的。这里我就不介绍了,有兴趣的自己找资料吧。所有的信息均参照于《单元测试之道-C#版》,上面有更好的讲解,这里看到他的例子,想想接口还能有这样的好处,所以就写了这个文章,呵呵。
在 C++ 单元测试中,使用 mock 对象构建测试用例并返回模拟结果是验证代码行为、隔离外部依赖的重要手段。Google Test(GTest)本身提供了基础的断言和测试框架,而 Google Mock(GMock)则在此基础上扩展了 mock 功能,使得开发者能够定义对象的行为并验证其交互。 ### 使用 GMock 构建 mock 对象并返回模拟结果 #### 1. 定义接口类(抽象类) 为了使用 GMock,首先需要定义接口类(抽象类),其中包含需要 mock 的方法。例如: ```cpp class Database { public: virtual ~Database() = default; virtual int fetchRecord(int id) = 0; }; ``` #### 2. 创建 mock 类 接着,使用 `MOCK_METHOD` 宏定义 mock 方法。例如: ```cpp #include <gmock/gmock.h> class MockDatabase : public Database { public: MOCK_METHOD(int, fetchRecord, (int id), (override)); }; ``` #### 3. 设置 mock 行为并执行测试测试用例中,可以使用 `EXPECT_CALL` 宏定义 mock 方法的行为,包括返回值、调用次数等。例如: ```cpp #include <gtest/gtest.h> #include <gmock/gmock.h> using ::testing::Return; TEST(DatabaseTest, FetchRecordReturnsExpectedValue) { MockDatabase mock_db; EXPECT_CALL(mock_db, fetchRecord(1)).WillOnce(Return(100)); int result = mock_db.fetchRecord(1); EXPECT_EQ(result, 100); } ``` #### 4. 验证调用次数与顺序 除了返回值,还可以验证方法是否被正确调用。例如,验证方法被调用次: ```cpp EXPECT_CALL(mock_db, fetchRecord(2)).Times(1); ``` 若需验证多个调用的顺序,可使用 `InSequence`: ```cpp { ::testing::InSequence seq; EXPECT_CALL(mock_db, fetchRecord(1)).WillOnce(Return(100)); EXPECT_CALL(mock_db, fetchRecord(2)).WillOnce(Return(200)); } ``` #### 5. 结合测试夹具复用 mock 设置逻辑 若多个测试用例需要共享 mock 对象的初始化逻辑,可以结合测试夹具使用: ```cpp class DatabaseTestFixture : public ::testing::Test { protected: MockDatabase mock_db; }; TEST_F(DatabaseTestFixture, FetchRecordReturns100) { EXPECT_CALL(mock_db, fetchRecord(1)).WillOnce(Return(100)); EXPECT_EQ(mock_db.fetchRecord(1), 100); } TEST_F(DatabaseTestFixture, FetchRecordReturns200) { EXPECT_CALL(mock_db, fetchRecord(2)).WillOnce(Return(200)); EXPECT_EQ(mock_db.fetchRecord(2), 200); } ``` ### 总结 通过上述方式,可以在 C++ 单元测试中使用 mock 对象模拟外部依赖的行为,从而更有效地测试核心逻辑。GMock 提供了强大的机制来定义 mock 方法、设置预期行为并验证调用过程,使得测试更加灵活和可靠[^2]。 ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值