iOS 测试驱动开发

2018-02-26  本文已影响143人  貘鸣

iOS 测试驱动开发

测试驱动开发方法的优点:

测试驱动开发实践中的注意事项:

1 实践指南概览

开始时, 所有人都会问这个问题: 在测试驱动开发的实践中, 应该对什么样的代码写测试? 答案是: 对所有的代码写测试. 原因很简单: 写实现代码的唯一驱动力是—测试.

但是在实践中, 这样的实践有时是非常困难的, 比如: 一个按钮的颜色和位置需要被测试吗? 视图的层级需要被测试吗? 当然不是, 因为它们和 app 的功能是不相关的, 即视图内容往往是在 UI 测试中进行的.

单元测试仅仅是针对控制器角色和模型角色上的测试.

另外, 不要在单元测试中进行集成测试, 日常开发过程中执行的单元测试应该是小且快的, 而集成测试通常是在每天晚上的集成环节去进行, 它们可以是比较慢的. 这样就需要知道如何在 Xcode 中针对当前需要来运行不同的测试.

2 完整例子(TODO-List APP)

在 TDD 实践的开始, 要把思想调整过来, 即: 在拿到一个需求并进行分析后, 下一步要想的不是如何去实现这个功能, 而是想如何对这个功能进行测试.

在测试中去明确这个功能应完成什么样的动作, 而不是去明确这个功能应该如何实现. 这样的话, 无论功能实现如何修改, 也不会影响到测试代码的书写, 最多只是影响到测试的执行结果. TDD 的思维过程必须经过长时间练习来掌握, 从而形成一种习惯.

余下的部分来完成一款 ToDo-List APP, 使用 TDD 开发方法. 这款 APP 包含如下内容:

2.1 APP 描述

启动 APP 后, 用户首先看到的是任务列表, 即所有的任务集合. 在列表中的每一条任务都包含有: 任务标题, 任务描述(可选), 到期时间.

新的任务可以通过点击右上角的加号添加, 加号位于导航栏上.

用户可以选择任务标记为已完成, 这样任务就会自动放入已完成部分. 并且可以将已完成的任务再次标记为未完成. 用户还可以把任务全部删除.

另外点击一个任务后, 可以进入任务的详情页.

用户故事:(一次描述一件简单的要求)

通过这个列表整理出需求, 然后提取出所需功能并进行实现.

2.2 APP 的结构设计

下图即该 APP 的结构设计:


APP 总体结构

其中 ToDoItemTableViewDataProvider 作为 TableView 的代理和数据源使用.

在开发过程中, 一般都是按功能模块来进行开发的, 不过这里为了便于描述, 这里按照角色定位来逐个实现.

2.3 APP 模型的实现

首先实现如下三个类型:

下面就来看如何用 TDD 来实现这三个类型.

首先是 ToDoItem 结构体, 很多人都想知道这个结构体有什么可以测试的?

在 Xcode 中, 每一个单元测试文件中的测试都是集成自一个 XCTestCase, 表明它是一个测试用例.

为了对实现进行驱动, 就要找到可以失败的单元测试. 要判断单元测试是否是必要的, 首要条件就是它能否驱动实现.

Xcode 中有一个分屏的小技巧, 按住 option 键然后点击一个文件, 那个文件就自动出现在分屏界面中了.

当进行测试时, 如果有 sut 对象(System Under Test 对象), 如果多个单元测试中都用到同样的对象, 则可以将它在 setup 中实例化, 然后在 tearDown 中销毁.

在修改了测试之后, 有一个小技巧来保证测试的效果不变, 即可以暂时性改变实现来达到目的.

2.4 APP 视图控制器的实现

iOS 中的 ViewController 只是作为视图层的内容, 不要让它作为 MVC 中的控制器来使用, 否则就会造成写出巨大的且不易维护不易理解的控制器.

但这里仍然是把控制器作为模型和视图的胶水, 只不过将一些业务从其中剥离, 独立到其他的类中去(比如 tableView 的数据源和代理).

而分离后, 视图控制器和其他类之间的交流可以通过接口进行.

这一节需要实现的类型有如下:

2.4.1 TDD 实现列表视图控制器

用 TDD 方法来实现视图控制器时, 如果没有接触过 TDD 的话, 很难想象如何对视图控制器进行测试驱动开发.

首先想一下这个视图控制器的功能, 它的功能是将视图显示给用户. 在使用时, 需要保证控制器中存在一个 tableView, 然后在 viewDidLoad 之后, 它被正确设置. 故首先保证存在 tableView:

    func test_TableViewIsNotNilAfterViewDidLoad() {
        let sut = ItemListViewController()
        // 这个方法会触发调用 ViewDidLoad, 千万不要手动调用 ViewDidLoad.
        sut.loadViewIfNeeded()
        XCTAssertNotNil(sut.tableView)
    }

关于 UI 的构建: 如果是小型工程, 建议使用 SB, 大型工程建议代码编写 UI. 原因在于 SB 更加快速, 但灵活性不足, 代码更灵活, 但效率更低.

上面讨论 UI 的构建方式, 是因为在编写单元测试时, 如果用 SB 构建 UI 的话, 就需要使用 SB 来初始化 ViewController, 而不是直接调用 ViewController 的无参构造方法. 这个工程中使用 SB 来构建 UI, 故单元测试需要修改为如下:

    func test_TableViewIsNotNilAfterViewDidLoad() {
        let storyBoard = UIStoryboard(name: "Main", bundle: nil)
        let viewController = storyBoard.instantiateViewController(withIdentifier: "ItemListViewController")
        let sut = viewController as? ItemListViewController
        // 这个会触发调用 ViewDidLoad
        sut?.loadViewIfNeeded()
        XCTAssertNotNil(sut?.tableView)
    }

而想要把数据源和代理独立出去, 则写一个单元测试来驱动.

tableView 在查询过一次 numberOfSection 或者 numberOfRow 后, 都会把数值缓存起来使用, 如果没有调用 reloadData 的话, 即使数据源数据更新, tableView 中的数据也不会改变.

2.4.2 Fake Object

为了测试 cell 是否是从 cell 缓存队列中取出的, 需要首先了解单元测试中一个非常重要的概念: fake object.

fake object 用于分隔模块, 即替代被测模块的依赖. fake object 作为当前被测模块的依赖, 替代实际代码中的依赖, 这样就可以在测试代码中对 fake object 进行控制, 而无需去创建或获取外部依赖(有时这样的外部依赖在单元测试中是无法直接获取的).

在测试中建立 fake object, 控制它们的行为, 这样的话, 单元测试中始终关注的就都是当前被测单元.

主要有如下三种类型的 fake object:

2.5 使用 Mock 对象

需要写单元测试保证 cell 是否是从重用队列中取出的. 重用操作通过 dequeue 方法完成. 而测试的重点就是看 dequeue 方法是否被调用了.

另外在 iOS 中配置 cell 的一个通用方式是在 cell 中实现一个自定义的 configCell(with:) 方法.

这里测试的是数据源对象, 故其他对象都可以通过 Mock 的方式模拟.

在 mock 对象中相应的方法中, 不只可以标记方法是否被调用, 另外还可以进行更多的验证.

在写单元测试时, 一个主要的技能训练点就是明确测试点是什么, 明确失败点是什么. 这样才能驱动实现.

待续...

上一篇下一篇

猜你喜欢

热点阅读