如何对ViewModel进行单元测试

使用MVVM架构来开发APP的好处之一就是便于测试业务逻辑,由于ViewModel不涉及界面,就不用考虑Mock对象了,测试起来也方便的多。

对ViewModel进行单元测试的一般流程

创建ViewModel -> 把输入数据交给ViewModel -> ViewModel把输入数据进行业务逻辑处理,加工成输出数据 -> 测试ViewModel的输出数据是否正确

进行单元测试前需做一些配置,可参考:
http://liuduoios.github.io/2015/10/24/Swift-2-%E4%B8%AD%E7%9A%84%E5%8D%95%E5%85%83%E6%B5%8B%E8%AF%95%E5%85%B3%E9%94%AE%E5%AD%97-testable/

下面拿一个简单的例子来说明

APP启动后界面如下:

这个例子中含有两个ViewModel类,一个是列表的ListViewModel,其中含有根据接口返回数据来计算Cell行数的业务逻辑,内容如下:
这里数据绑定和变化监听用了开源库SwiftBondhttps://github.com/SwiftBond/Bond

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import UIKit
import Bond

class ListViewModel: NSObject {

var numberOfRows: Int {
return self.items.value?.count ?? 0
}

var items: Observable<[Item]?> = Observable(nil)

func requestData() {
let configuration = NSURLSessionConfiguration.defaultSessionConfiguration()
let session = NSURLSession(configuration: configuration)
session.dataTaskWithURL(NSURL(string: "http://www.google.com")!) { (data, response, error) -> Void in
let jsonStr = String(data: data!, encoding: NSUTF8StringEncoding)
let jsonData = jsonStr?.dataUsingEncoding(NSUTF8StringEncoding)
do {
var items = [Item]()

let dic = try NSJSONSerialization.JSONObjectWithData(jsonData!, options: NSJSONReadingOptions(rawValue: 0)) as! [String: AnyObject]
let itemDics = dic["data"] as! [[String: String]]
for itemDic in itemDics {
items.append(Item(dic: itemDic))
}

self.items.next(items)
} catch {

}
}.resume()
}

func cellViewModelAtIndexPath(indexPath: NSIndexPath) -> CellViewModel? {
guard let items = items.value else { return nil }
if indexPath.row < items.count {
return CellViewModel(item: items[indexPath.row])
} else {
return nil
}
}

}

另一个是Cell的CellViewModel,其中含有把title变为大写的业务逻辑,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import UIKit

class CellViewModel: NSObject {

init(item: Item) {
super.init()
self.item = item
}

var item: Item?

var title: String? {
return item?.title?.uppercaseString
}
var subtitle: String? {
return item?.subtitle
}

}

测试ListViewModel

这里设计到网络请求数据,因此要用到一个stub开源库,这里使用了Mockingjay
对于stub开源库的选择:
如果你的项目是纯Swift的,推荐使用Mockingjayhttps://github.com/kylef/Mockingjay
如果你的项目是Objective-C或者OC和Swift混编的,推荐使用OHHTTPStubshttps://github.com/AliSoftware/OHHTTPStubs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import XCTest
import Mockingjay
import Bond
@testable import ViewModelUnitTest

class ListViewModelTests: XCTestCase {

override func setUp() {
super.setUp()
// Put setup code here. This method is called before the invocation of each test method in the class.
}

override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
super.tearDown()
}

func testNumberOfRows() {
let path = NSBundle(forClass: self.dynamicType).pathForResource("Data", ofType: "json")!
let data = NSData(contentsOfFile: path)!
stub(everything, builder: jsonData(data))

let expectation = expectationWithDescription("get items data")

let viewModel = ListViewModel()
viewModel.items.observeNew({ (items) -> () in
expectation.fulfill()
})
viewModel.requestData()

waitForExpectationsWithTimeout(10) { (error) -> Void in
if error != nil { XCTFail("time out") }
XCTAssertEqual(viewModel.numberOfRows, 2, "行数等于2")
}
}

}

测试CellViewModel

不涉及网络请求,直接对业务逻辑进行测试就好了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import XCTest
@testable import ViewModelUnitTest

class CellViewModelTests: XCTestCase {

override func setUp() {
super.setUp()
// Put setup code here. This method is called before the invocation of each test method in the class.
}

override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
super.tearDown()
}

func testTitle() {
let item = Item(dic: ["name": "abc", "subname": "def"])
let viewModel = CellViewModel(item: item)
XCTAssertEqual(viewModel.title, "ABC", "title是否变为大写")
}

}