在这个架构下我们主要讨论两个模块的单元测试,一个是网络模块,一个Reactor模块。
1.网络层单元测试
做网络请求测试时,我们希望给定一个测试数据时,就能同步返回这个数据。不需要异步的去服务器上获取。Moya框架提供了一个SampleData来专门用来单元测试使用,就不需要我们去Mock一个网络对象了。
在定义的Moya的Target文件里面设置sampleData属性。
var sampleData: Data {
switch self {
case .getAllProducts:
return "".data(using: .utf8)!
default:
return "".data(using: .utf8)!
}
}
然后设置一下MoyaProvider的stubClosure。StubBehavior是一个枚举值。
- .never没有Stub,
- .immediate 同步返回SampleData里面设置的数据,
- .delayed(seconds:) 延迟时间返回SampleData里面设置的数据
我们这里当然设置成MoyaProvider.immediatelyStub,发起Request之后就会立即返回sampleData里面的数据。
let provider = MoyaProvider<NetworkTarget>(stubClosure: MoyaProvider.immediatelyStub)
provider.rx.request(target)
接下来我们就可以在单元测试文件里面写测试Case了。
为了方便测试我们写了个服务层,例如HomeService就是关于Home界面的网络请求。测试home界面相关网络请求我们只需要测试HomeService这个类就可以了。
比如我们想测试一个404错误请求的Case:
func testToError_notFound() {
//endpointClosure 自定义成我们想测试404error
let endpointClosure = { (target: NetworkTarget) -> Endpoint in
let url = URL(target: target).absoluteString
return Endpoint(url: url, sampleResponseClosure: {.networkError(NSError(domain: "not fount", code: 404, userInfo: nil))}, method: target.method, task: target.task, httpHeaderFields: target.headers)
}
//创建一个立即返回Data的Provider
let provider = MoyaProvider<NetworkTarget>(endpointClosure: endpointClosure,stubClosure: MoyaProvider.immediatelyStub)
let netwoking = Network(provider: provider)
var netError: NetworkError? = nil
ServiceManager(networking: netwoking)
.homeService
.getAllProducts(page: 0)
.subscribe(onSuccess: { (weatherData) in
}, onError: { (error) in
//拿到返回的错误信息
netError = NetworkError(error: error)
})
.disposed(by: disposeBag)
//与预期的结果做对比
XCTAssertEqual(netError, NetworkError.notFound)
}
RxBlocking
Rx提供一个RxBlocking框架,专门用来做单元测试,RxBlocking将阻塞当前线程一直到观察者序列(observable)终止,toBlocking()就是RxBlocking提供的一个方法,它可以把原始的Observable变成一个BlockingObservable。这个BlockingObservable可以阻断当前线程,让我们用它提供的方法等待特定的事件发生。其中常用的三个方法是:
- toArray()把Observable<T>中发生的所有事件,转换成一个[T]。这个方法只适用于有限序列,我们就可以用数组的形式观察到Observable中的所有值;
- first(),得到Observable中第一个事件的值;
- last(),得到Observable中最后一个事件的值;
用toBlocking的方式写一个返回正确Data的Case:
func testData_allProductInfos() {
//endpointClosure 自定义成我们相反会的正确data
let endpointClosure = { (target: NetworkTarget) -> Endpoint in
let url = URL(target: target).absoluteString
return Endpoint(url: url, sampleResponseClosure: {.networkResponse(200, target.sampleData)}, method: target.method, task: target.task, httpHeaderFields: target.headers)
}
let provider = MoyaProvider<NetworkTarget>(endpointClosure: endpointClosure,stubClosure: MoyaProvider.immediatelyStub)
let netwoking = Network(provider: provider)
var response:[ProductInfo]?
do {
response = try ServiceManager(networking: netwoking)
.homeService
.getAllProducts(page: 0)
.toBlocking()
.first()
} catch {
}
//预期的数据模型
let data = CommonTools.shareInstance.loadDataFromBundle(ofName: "AllProductInfo", ext: "json")
let dictionary = try? JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any]
let mesageDic = dictionary!["data"] as? [[String: Any]]
let models = Mapper<ProductInfo>().mapArray(JSONObject: mesageDic)
//用返回的结果与预期的数据模型做对比
XCTAssertEqual(response, models)
}
2.对Reactor单元测试
项目中我们把业务逻辑代和数据都放到Reactor中,所以当我们想测试一个模块时,只需要测试它的Reactor。比如想要测试Home界面,我们直接测试HomeReactor。
我们以PeiPeiPay项目首页作为一个例子:
如图所示我们在HomeReactor中会把网络请求的模型数据转换为Cell需要显示的数据:
//cell直接显示的数据模型
struct GoodListCellData: Equatable {
var name: String?
var imageUrl: String?
var info: String?
var salePrice: String?
var repertory: String?
var originalPrice: NSAttributedString?
static func == (lhs: GoodListCellData, rhs: GoodListCellData) -> Bool {
return lhs.name == rhs.name
&& lhs.imageUrl == rhs.imageUrl
&& lhs.info == rhs.info
&& lhs.salePrice == rhs.salePrice
&& lhs.repertory == rhs.repertory
&& lhs.originalPrice == rhs.originalPrice
}
}
这样的话我们只需要测试GoodListCellData的数据是否正确就能确定cell上显示的数据是否正确了。
首先自定义了一个Json数据
{
"data" : [{
"name" : "黑巧克力",
"image_url" : "https://s2.ax1x.com/2019/04/19/Epj1HI.png",
"item_code" : "1",
"sale_price" : 20.1,
"cost_price" : 30.1,
"count" : 20,
"note" : "好吃又好玩",
"category" : "食品",
}]
}
Tests文件的setUp发起网络请求
override func setUp() {
super.setUp()
let provider = MoyaProvider<NetworkTarget>(stubClosure: MoyaProvider.immediatelyStub)
let networking = Network(provider: provider)
reactor = HomeViewReactor(serviceManager: ServiceManager(networking: networking))
reactor.action.onNext(.downRefresh(searchName: ""))
}
然后我们就能写测试Case来验证Reactor里面的GoodListCellData数据和我们预期的json数据是否一致。
//验证商品名
func testAllProductItems_name() {
XCTAssertEqual(reactor.currentState.goodListSectionModel[0].data[0].name, "黑巧克力")
}
//验证售价
func testAllProductItems_salePrice() {
XCTAssertEqual(reactor.currentState.goodListSectionModel[0].data[0].salePrice, "售价:20.1")
}
//验证原价
func testAllProductItems_originalPrice() {
let original = CommonTools.shareInstance.addlineToLabelText(text: "原价:30.1")
XCTAssertEqual(reactor.currentState.goodListSectionModel[0].data[0].originalPrice, original)
}
在此框架下我们如果对所有Server和Reactor都进行了单元测试,其实已经能覆盖大部分的测试。
使用Xcode可以查看测试覆盖率
编写好测试用例之后,我们来看如何查看这些用例覆盖的代码范围。
- 选择Test Scheme;
- 切换到Options tab;
- 选中Gather coverage for;
- 切换到some targets;
- 在下面的Targets列表中,添加测试Target;
然后就能看的的测试覆盖率
以上就是对现有框架下UnitTest的简单实践。