Making Fun of Things with OCMock

本文介绍OCMock的使用方法,包括安装、概念理解、创建模拟对象、设置期望、返回值与参数等。重点在于如何在Cocoa测试中有效利用模拟对象,简化测试过程并提高代码质量。

关于OCMock的使用介绍,以备不时之需,转自:
http://alexvollmer.com/posts/2010/06/28/making-fun-of-things-with-ocmock/

Making Fun of Things with OCMock

OK, I've done plenty of ranting about the state of TDD in Cocoa development. So instead of inflicting more whining on your ears, I've decided to figure out how to get better at this. This is the first in a series of posts I'll be doing about testing in Cocoa. We're going to look at OCMock, a framework for creating and using mock objects in your Cocoa tests.

This isn't the only tutorial out there on OCMock, but hopefully it's the clearest. I found the documentation on the site to be a bit, ahem, lacking. As a result I ended up figuring out how to use it by going through the source code1.

Installation

First, you need to download the OCMock project. Before Xcode 3.2.3, you used to be able to simply add theOCMock.framework to the "Link Binary With Libraries" build phase of your unit tests. This appears to have stopped working, so you have two options: you can include the OCMock source Xcode project and declare a build dependency on its static library target, or you can build the static library once and put the following header files somewhere accessible to your Xcode build:

  • NSNotificationCenter+OCMAdditions.h
  • OCMArg.h
  • OCMConstraint.h
  • OCMock.h
  • OCMockObject.h
  • OCMockRecorder.h

I didn't want to rebuild the library over and over so I built the static library once and put the libOCMock.a andOCMock.h files in a directory in my project and added that directory to the LIBRARY_SEARCH_PATHS in the unit-test target configuration 2.

Underlying Concepts

I'm assuming that you already know what mock objects are and how to use them. I'm not going to explain their value here, or the design changes you end up making to accommodate them. Mock objects are simply one of many tools you can employ in your testing. They have their pros and cons so let experience and intuition be your guide. Instead, I want to focus on how to use OCMock.

The general recipe for using mocks in unit-tests is:

  1. Create the mock object
  2. Specify the expected invocations and return values
  3. Associate the mock object with the code under test
  4. Execute the code under test
  5. Validate that your assertions are correct

It's worth spending some time looking at the test cases that come with the OCMock source code. The coverage is quite good so it's a great demonstration of OCMock's capabilities.

Making Mocks

To create a mock object, use one of the factory methods available on the OCMockObject class: +mockForClass:,+mockForProtocol: or +partialMockForObject:. There are also two variants of this called "nice" mocks, which you can get by calling either +niceMockForClass: or +niceMockForProtocol:. The difference between a "nice" mock and regular (mean? stand-offish?) mock, is how they behave when they receive an unexpected method invocation. A "nice" mock will simply ignore the unexpected invocation, whereas a regular mock will raise aNSException.

Factory MethodDescription
+mockForClass:Create a mock based on the given class
+mockForProtocol:Create a mock based on the given protocol
+niceMockForClass:Create a "nice" mock based on the given class
+niceMockForProtocol:Create a "nice" mock based on the given protocol
+partialMockForObject:Create a mock based on the given object
+observerMock:Create a notification observer (more on this later)

If you're like me and you like to keep your builds free of compiler warnings, declare the returned object to be of type id to shut the compiler up.

id myMock = [OCMockObject mockForClass:[MyClass class]];

The +partialMockForObject: method allows you to turn an existing object into a mock. This can be useful in cases where a collaborating object has several of its methods invoked, but you only want to override one or two. The obvious dangers here are that, 1) your collaborating class probably violates the Single-Responsibility Principleand 2) your tests now indirectly rely on the subtleties of which methods you mocked and which you didn't. Use at your own peril.

Setting Expectations

The object returned by any of these factory methods allows you to both setup expectations, invoke methods and verify its configuration. OCMock does a clever little trick to distinguish between invocation configuration and handling method invocations. Calling either the -expect or -stub method will return an object that you can use to setup your expectations. The cool thing about this object is that you setup your expectations by (more or less) invoking the methods on it that you want your class under test to invoke.

Let's imagine that we have a simple stock-portfolio management application. Our portfolio model is encapsulated by the AVStockPortfolio class. An instance of that class is given an object that implements the AVQuoteServiceprotocol, which is our gateway to a real-live stock quote service. When we test the AVStockPortfolio class, we don't want to be encumbered with a "production" implementation of AVQuoteService. Instead we want to craft a stand-in object to exercise and validate the portfolio class.

Our first test is to see that the portfolio class initiates a connection with the quote service in its init method:

- (void)testInit {
  id mockService = [OCMockObject mockForProtocol:@protocol(AVQuoteService)];
  [[mockService expect] initiateConnection];
  
  AVStockPortfolio *portfolio = [[AVStockPortfolio alloc] initWithService:mockService];
  
  [mockService verify];
}

When the -expect method is called, it returns a "trampoline" object that captures additional method calls dynamically. Any methods invoked on the service that I haven't explicitly configured will raise an exception. I can also check that the methods I've configured were all invoked by calling the mock object's -verify method.

You can also configure your mock object to act as a more forgiving "stub" for particular methods by using -stubinstead of -expect 3. Admittedly, the ability to turn a so-called "mock" object into a "stub" for particular method invocations muddies the nomenclature a bit, but it can be handy to have this ability at times.

Return Values

If methods on your mocks need to return values, you have a variety of methods to call on the object returned by -expect or -stub. Any method we call on this object that isn't one of the built-in OCMock methods, is captured dynamically as an expected method invocation.

MethodExplanation
-andReturn:Return the given object
-andReturnValue:Return a non-object value (wrapped in a NSValue)
-andThrow:Throw the given exception
-andPost:Post the given notification
-andCall:onObject:Call the selector on the given object
-andDo:Invoke the given block (only on OS X 10.6 or iOS 4)

Since each of these methods returns the same expectation object, you can chain these method calls, where it makes sense. For example to post a notification and return a specified value we could do this:

NSNotification *notfication = [NSNotification notificationWithName:@"foo" object:nil];
[[[mock expect] andPost:notfication] andReturn:@"FOOBAR"] doSomethingMagical];

The -andDo: option is one of my favorites since it uses blocks and blocks are the most awesome addition to Objective-C in a long time. You can do a lot of validation locally within a block which keeps the code very succinct and clean:

- (void)testSellSharesInStock {
  id quoteService = [[OCMockObject] mockForProtocol:@protocol(AVQuoteService)];
  [[[quoteService expect] andDo:^(NSInvocation *invocation) {
    // validate arguments, set return value on the invocation object
  }] priceForStock:@"AAPL"];
  
  AVStockPortfolio *portfolio = [[AVStockPortfolio alloc] initWithService:quoteService];
  [portfolio sellShares:100 inStock:@"AAPL"];
  
  // other validations and assertions
  
  [quoteService verify];
}

Arguments & Return Values

Basic method expectations are useful, but we also want to validate the arguments given to methods. Back to our stock portfolio, imagine that when we buy a stock, we know that part of that process is to ask for the current price of a stock from a stock service, so we want to assert that when we sell a particular stock, our portfolio object queries the quote service for the latest price for the same stock symbol:

- (void)testBuyShares {
  id mockService = [OCMockObject mockForProtocol:@protocol(AVQuoteService)];
  [[mockService expect] andReturn:[NSNumber numberWithFloat:214.57]] priceQuoteForSymbol:@"AAPL"];
  
  AVPortfolio *portfolio = [[AVPortfolio alloc] initWithQuoteService:mockService];
  [portfolio buyShares:100 inStock:@"AAPL"];
  
  // other validation and assertions

  [mockService verify];
}

In this case, we're declaring the expected argument to be the string literal @"AAPL". We've also configured the method with a return value, in this case a NSNumber instance with the value 214.57. However, OCMock can do more. You can use any of the following OCMArg class methods in place of a real argument when setting up your method expectations:

OCMArg methodDescription
+anyAny argument is accepted.
+anyPointerAccepts any pointer
+isNilThe given argument must be nil
+isNotNilThe given argument must not be nil
+isNotEqual:Given argument is not object-equivalent with expectation
+checkWithSelector:onObject:Check the argument with the given action/target pair
+checkWithBlock:Check the argument with the given block (OS X 10.6 or iOS 4)

OCMock also provides a few handy macros for argument matching:

MacroDescription
OCMOCK_ANY()Equivalent to [OCMArg any]
OCMOCK_VALUE(value)A quick way to match a non-object argument
CONSTRAINT(selector)Validate with a given selector on self
CONSTRAINTV(selector,value)Validate with a given selector on self and an additional argument

But Wait…There's More!

One nice little feature of OCMock is its handy support for testing notifications. To create a mock for notifications, call the +observerMock factory method on the OCMockObject class. This will return a mock that works differently from the mocks/stubs described earlier. Instead of setting up method invocation expectations, you can set up notification expectations which are useful when you want to assert that your class under test posts notifications. Let's say that we want our aforementioned portfolio class to post notifications whenever stocks are bought or sold:

- (void)testSellSharesInStock {
  id mock = [OCMockObject observerMock];
  // OCMock adds a custom methods to NSNotificationCenter via a category
  [[NSNotificationCenter defaultCenter] addMockObserver:mock
                                                   name:AVStockSoldNotification
                                                 object:nil];
                                               
  [[mock expect] notificationWithName:AVStockSoldNotification object:[OCMArg any]];

  AVPortfolio *portfolio = [self createPortfolio]; // made-up factory method
  [portfolio sellShares:100 inStock:@"AAPL"];

  [mock verify];
}

If the AVStockSoldNotification isn't posted by the time we call verify, the mock will raise an exception. Note that you need to add the mock as an observer, which you can do by the handy -addMockObserver:name:object:category method on NSNotificationCenter provided by OCMock.

Execution & Validation

Any method invoked on your mock object will be checked against the configured invocations. Any unexpected invocations will result in an exception being thrown (unless it's a stub or the mock is in "nice" mode). Also, any configured invocations that did not happen will trigger an exception when the mock's -verify method is called. As a matter of habit, you want to call this at the end of your test.

Conclusion

Using mocks, stubs and test-doubles in your testing is a handy way to keep your tests lean and focused on one class at a time. This is especially helpful when you have dependent classes that require a lot of setup that you would rather avoid in your test-harness (e.g. Core Data).

The downside to testing with mocks and stubs is that you lose some test coverage for the objects you're replacing with test-doubles. This approach probably works best when your code is interacting with objects from other libraries that you don't want to deal with in your tests.

Footnotes

  1.  I know it seems like a drag, but it's hard to overestimate the value of reading the original source code for any tool that you use. You'll be amazed at how much more of an intimate understanding it gives you of the tool and the rationale behind its design.
  2.  I forked the OCMock codebase and have started to make a few tweaks to it. Feel free to take a look at it onGitHub.
  3.  If you aren't familiar with mocks vs. stubs, Martin Fowler has written the canonical text on the differences.

采用PyQt5框架与Python编程语言构建图书信息管理平台 本项目基于Python编程环境,结合PyQt5图形界面开发库,设计实现了一套完整的图书信息管理解决方案。该系统主要面向图书馆、书店等机构的日常运营需求,通过模块化设计实现了图书信息的标准化管理流程。 系统架构采用典型的三层设计模式,包含数据存储层、业务逻辑层和用户界面层。数据持久化方案支持SQLite轻量级数据库与MySQL企业级数据库的双重配置选项,通过统一的数据库操作接口实现数据存取隔离。在数据建模方面,设计了包含图书基本信息、读者档案、借阅记录等核心数据实体,各实体间通过主外键约束建立关联关系。 核心功能模块包含六大子系统: 1. 图书编目管理:支持国际标准书号、中国图书馆分类法等专业元数据的规范化著录,提供批量导入与单条录入两种数据采集方式 2. 库存动态监控:实时追踪在架数量、借出状态、预约队列等流通指标,设置库存预警阈值自动提醒补货 3. 读者服务管理:建立完整的读者信用评价体系,记录借阅历史与违规行为,实施差异化借阅权限管理 4. 流通业务处理:涵盖借书登记、归还处理、续借申请、逾期计算等标准业务流程,支持射频识别技术设备集成 5. 统计报表生成:按日/月/年周期自动生成流通统计、热门图书排行、读者活跃度等多维度分析图表 6. 系统维护配置:提供用户权限分级管理、数据备份恢复、操作日志审计等管理功能 在技术实现层面,界面设计遵循Material Design设计规范,采用QSS样式表实现视觉定制化。通过信号槽机制实现前后端数据双向绑定,运用多线程处理技术保障界面响应流畅度。数据验证机制包含前端格式校验与后端业务规则双重保障,关键操作均设有二次确认流程。 该系统适用于中小型图书管理场景,通过可扩展的插件架构支持功能模块的灵活组合。开发过程中特别注重代码的可维护性,采用面向对象编程范式实现高内聚低耦合的组件设计,为后续功能迭代奠定技术基础。 资源来源于网络分享,仅用于学习交流使用,请勿用于商业,如有侵权请联系我删除!
《基于SSM架构的学籍数据管理平台技术解析》 在当代数字化教育背景下,数据管理平台已成为教育机构运营的核心支撑。本系统以SSM技术组合为基础架构,构建了一套完整的学籍信息处理体系,通过系统化的技术方案实现教育数据的规范化管理与智能分析。以下从架构设计、技术实现与功能模块三个维度展开说明。 一、系统架构设计 该平台采用分层式架构设计,充分体现模块化与可维护性特征。Spring框架作为核心容器,通过依赖注入机制实现组件解耦;SpringMVC架构负责前端请求的路由与响应处理;MyBatis数据层框架则封装了数据库交互过程,通过映射配置简化SQL操作。三层架构协同工作,形成高内聚低耦合的技术体系。 二、技术实现要点 1. Spring容器:基于控制反转原则管理业务对象生命周期,结合面向切面编程实现事务控制与日志管理 2. SpringMVC模块:采用模型-视图-控制器设计范式,规范Web层开发流程,支持RESTful接口设计 3. MyBatis组件:通过XML配置实现对象关系映射,提供动态SQL生成机制,显著减少冗余编码 三、核心功能模块 1. 学籍信息维护:实现学员基本资料的增删改查操作,涵盖学籍编号、个人信息、所属院系等关键字段 2. 学业成绩管理:支持课程分数录入与批量处理,提供多维度统计分析功能 3. 教学组织管理:建立班级体系与学员关联关系,实现分级数据管理 4. 权限控制机制:基于角色访问控制模型,划分管理员、教职工、学员三级操作权限 5. 系统审计功能:完整记录用户操作轨迹,构建安全追踪体系 四、系统开发方法论 在项目生命周期中,采用结构化开发流程。前期通过需求调研确定系统边界,中期完成数据库范式设计与接口规范制定,后期采用迭代开发模式配合自动化测试,确保系统交付质量。 五、技术演进展望 当前系统虽未集成智能算法,但为未来升级预留了扩展接口。可预见的技术演进方向包括:基于学习行为数据的智能预警、个性化学习路径推荐等深度应用场景。 综上所述,该平台通过SSM技术体系实现了教育管理数据的标准化处理,既展示了现代软件开发范式的实践价值,也为教育信息化建设提供了可复用的技术方案。这种系统化的问题解决思路,充分体现了软件工程方法在教育领域的应用潜力。 资源来源于网络分享,仅用于学习交流使用,请勿用于商业,如有侵权请联系我删除!
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值