Skip to content

Commit 6fb4dc9

Browse files
committed
Add content
1 parent 8faf5f2 commit 6fb4dc9

File tree

2 files changed

+332
-0
lines changed

2 files changed

+332
-0
lines changed

chapter6/mvvm_in_practice.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,61 @@ self.title = [self.viewModel.initialPhotoModel photoName];
305305

306306
```
307307
308+
该`model`和`photoImage`属性的用法已经解释过了。`photoName`事实上作为属性在代码库的其他地方被用来设置一些东西,类似于分页视图控制器的标题这样。你可以下载Github的代码库了解详情。我们来看一下实现:
309+
310+
```Objective-C
311+
#import "FRPPhotoViewModel.h"
312+
313+
//Utilities
314+
#import "FRPPhotoImporter.h"
315+
#import "FRPPhotoModel.h"
316+
317+
@interface FRPPhotoViewModel ()
318+
319+
@property (nonatomic, strong) UIImage *photoImage;
320+
@property (nonatomic, assign, getter = isLoading) BOOL loading;
321+
322+
@end
323+
324+
@implementation FRPPhotoViewModel
325+
326+
- (instancetype)initWithModel:(FRPPhotoModel *)photoModel {
327+
self = [super initWithModel:photoModel];
328+
if(!self) return nil;
329+
330+
@weakify(self);
331+
[self.didBeComeActiveSignal subscribeNext:^(id x) {
332+
@strongify(self);
333+
self.loading = YES;
334+
[[FRPPhotoImporter fetchPhotoDetails:self.model] subscribeError:^(NSError *error) {
335+
NSLog(@"Could not fetch photo details: %@",error);
336+
} completed:^{
337+
self.loading = NO;
338+
NSLog(@"Fetched photoDetails.");
339+
}];
340+
}];
341+
342+
RAC(self, photoImage) = [RACObserve(self.model, fullsizedData) map:^id (id value) {
343+
return [UIImage imageWithData:value];
344+
}];
345+
346+
return self;
347+
}
348+
349+
- (NSString *)photoName {
350+
return self.model.photoName;
351+
}
352+
353+
@end
354+
355+
356+
```
357+
358+
`didBecomeActive`信号订阅带有"函数副作用"的加载照片详情包括它的高清图片的数据。然后`photoImage`属性与模型的映射结果绑定。
359+
360+
使用`didBecomeActiveSignal`这种方法来启动一些像网络操作这样昂贵的任务,远远优于我们早前在初始化方法中启动他们的方法。
361+
362+
这就是在本书中我们将要涉及的全部内容,更多详情请参考[functional reactive pixels](https://github.com/ashfurrow/FunctionalReactivePixels),这个代码库包含了更多的在图片详情视图控制器和登陆视图控制器中使用视图模型的例子。这些Demo将向你展示如何有效地使用`ReactiveCocoa`执行网络操作和使用`RACCommands`响应用户界面交互。
308363

309364

310365

chapter6/testing_viewModels.md

Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,278 @@
11
# 测试ViewModels
2+
3+
本书的最后一节,我们谈谈测试,尤其是单元测试。在iOS的开发社区里,这是一个有争议的话题,这也是为什么我要把它放在最后的原因。理想的情况下。你应该在编写视图模型的同时为它编写单元测试。然而学习如何使用这种新的模式来编码已经很困难,尝试去测试这些你没有吃透的东西,多你来说压力太大,所以我把它放在了最后(学到这里我相信你已经理解了这种编码方式)。
4+
5+
当然我也注意到,并不是每个人都以相同的方式来测试,或者能够测试到相同的程度。我有.Net编程背景,在.net中使用mocks来测试系统的实现细节是最平常不过的了。其他平台背景的开发者较少使用mocks来做,甚至从来没有这样的经验。本节我只将我的单元测试方法分享给大家,如果你觉得合适就采用。
6+
7+
确保你的`Podfile`文件包含下面这些库:
8+
9+
```Objective-C
10+
target "FRPTests" do
11+
12+
pod 'ReactiveCocoa', '2.1.4'
13+
pod 'ReactiveViewModel', '0.1.1'
14+
pod 'libextobjc', '0.3'
15+
pod '500px-iOS-api', '1.0.5'
16+
pod 'Specta', '~> 0.2.1'
17+
pod 'Expecta', '~> 0.2'
18+
pod 'OCMock', '~> 2.2.2'
19+
20+
end
21+
22+
```
23+
24+
然后运行`pod install`.
25+
26+
首先我们来看看`FRPFullSizePhotoViewModel`,因为它最具Objective-C风范(没有太多ReactiveCocoa).
27+
28+
```Objective-C
29+
@interface FRPFullSizePhotoViewModel ()
30+
//Private access
31+
@property (nonatomic, assign) NSInteger initialPhotoIndex;
32+
33+
@end
34+
35+
@implementation FRPFullSizePhotoViewModel
36+
37+
- (instancetype)initWithPhotoArray:(NSArray *)photoArray initialPhotoIndex:(NSInteger)initialPhotoIndex {
38+
self = [self initWithModel:photoArray];
39+
if(!self) return nil;
40+
41+
self.initialPhotoIndex = initialPhotoIndex;
42+
43+
return self;
44+
}
45+
46+
- (NSString *)initialPhotoName {
47+
return [self.model[self.initialPhotoIndex] photoName];
48+
}
49+
50+
- (FRPPhotoModel *)photoModelAtIndex:(NSInteger)index {
51+
if(index < 0 || index > self.model.count - 1) {
52+
//Index was out of bounds, return nil
53+
return nil;
54+
}
55+
else {
56+
return self.model[index];
57+
}
58+
}
59+
60+
@end
61+
62+
```
63+
好了,我们先来测试这个初始化方法,然后在转移到其他两个方法上。
64+
65+
我们想印证初始化我们的视图模型时,它的两个属性`model`和`initialPhotoIndex`被正确地赋值了。
66+
67+
```Objective-C
68+
#import <Specta/Specta.h>
69+
#define EXP_SHORTHAND
70+
#import <Expecta/Expecta.h>
71+
#import <OCMock/OCMock.h>
72+
#import "FRPPhotoModel.h"
73+
74+
#import "FRPFullSizePhotoViewModel.h"
75+
76+
SpecBegin(FRPFullSizePhotoViewModel)
77+
78+
describe(@"FRPFullSizePhotoModel", ^{
79+
it (@"Should assign correct attributes when initialized", ^{
80+
NSArray *model = @[];
81+
NSInteger initialPhotoIndex = 1337;
82+
83+
FRPFullSizePhotoViewModel *viewModel =\
84+
[[FRPFullSizePhotoViewModel alloc] initWithPhotoArray:model
85+
initialPhotoIndex: initialPhotoIndex];
86+
87+
expect(model).to.equal(viewModel.model);
88+
expect(initialPhotoIndex).to.equal(viewModel.initialPhotoIndex);
89+
90+
});
91+
});
92+
93+
SpecEnd
94+
95+
```
96+
在该代码段顶部,我们导入了一些头文件,包括一个奇怪的预定义`EXP_SHORTHAND`,我们把他放在那里以便于可以使用类似`expect()`这样的shorthand matchers(速记匹配)的语法。然后我们引入我们的私有接口`SpecBegin(...)/SpecEnd`来为我们正在测试的视图模型屏蔽编译警告,最后的部分就是我们的单元测试本身。`Specta`的测试规范相当简单,你可以阅读更多的关于这方面的信息,但本书不会深入讲解它的一些细节。总之你的测试始于`SpecBegin`并终止于`SpecEnd`,测试例程用类似于`@"应该。。。",^{ 预测正常的情况应该如何 }`写在中间。
97+
98+
好了,停止模拟器中正在运行的应用,按下`cmd+U`快捷键,你就可以运行这段单元测试了。如果一切正常,你就能通过测试。
99+
100+
接下来我们来看看`photoModelAtIndex:`方法
101+
102+
```Objective-C
103+
- (FRPPhotoModel *)photoModelAtIndex:(NSInteger)index {
104+
if(index < 0 || index > self.model.count - 1 ) {
105+
// Index was out of bounds ,return nil
106+
return nil;
107+
}
108+
else {
109+
return self.model[ index ];
110+
}
111+
}
112+
```
113+
这里面没有太多的业务逻辑,但是我们看到其他地方都要使用它,所以我们的测试应该是健壮的。
114+
115+
```Objective-C
116+
it(@"Should return nil for an out-of-bounds photo index", ^{
117+
NSArray *model = @[[NSobject new]];
118+
NSInteger initialPhotoIndex = 0;
119+
120+
FRPFullSizePhotoViewModel *viewModel = \
121+
[[FRPFullSizePhotoViewModel alloc] initWithPhotoArray:model initialPhotoIndex:initialPhotoIndex];
122+
123+
id subzeroModel = [viewModel photoModelAtIndex:-1];
124+
expect(subzeroModel).to.beNil();
125+
126+
id aboveBoundsModel = [viewModel photoModelAtIndex:model.count];
127+
expect(aboveBoundsModel).to.beNil();
128+
});
129+
130+
it(@"Should return the correct model for photoModelAtIndex:",^{
131+
id photoModel = [NSObject new];
132+
NSArray *model = @[photoModel];
133+
NSInteger initialPhotoIndex = 0;
134+
135+
FRPFullSizePhotoViewModel *viewModel = \
136+
[[FRPFullSizePhotoViewModel alloc] initWithPhotoArray:model initialPhotoIndex:initialPhotoIndex];
137+
138+
id returnModel = [viewModel photoModelAtIndex:0];
139+
expect(returnModel).to.equal(photoModel);
140+
141+
});
142+
143+
```
144+
太棒了!我们这个新的测试保证了我们的代码具有完全的代码覆盖率。它检测了`photoModelAtIndex:`参数的三种可能的情况:少于0、在作用范围内以及越界。
145+
146+
最后,我们来看下`initialPhotoName`方法:
147+
148+
```Objective-C
149+
- (NSString *)initialPhotoName {
150+
return [self.model[self.initialPhotoIndex] photoName];
151+
}
152+
153+
```
154+
方法看起来很简单,但实际上这里面包含了更深层级的东西。恰当地重构一些代码并为它写一点不一样的更小的测试代码,来严格地测试这个方法。
155+
156+
```Objective-C
157+
- (NSString *)initialPhotoName {
158+
FRPPhotoModel *photoModel = [self initialPhotoModel];
159+
return [photoModel photoName];
160+
}
161+
162+
- (FRPPhotoModel *)initialPhotoModel {
163+
return [self photoModelAtIndex:self.initialPhotoIndex];
164+
}
165+
166+
```
167+
168+
这更清晰简单了,一个方法确切地只做一件事情,就像一棵树的树皮,层层叠叠相互依存。只要我们一路下来所有的代码都测试,那么最后我们就可以很确切地保证代码的健壮性。
169+
170+
`initialPhotoModel`是一个私有方法,所以测试它我们需要在测试文件中申明它。
171+
172+
```Objective-C
173+
@interface FRPFullSizePhotoViewModel ()
174+
175+
- (FRPPhotoModel *)initialPhotoModel;
176+
177+
@end
178+
```
179+
180+
你看到的所有我们的测试代码都非常简单。
181+
182+
```Objective-C
183+
it (@"Should return the correct initial photo model", ^{
184+
NSArray *model = @[[NSobject new]];
185+
NSInteger initialPhotoIndex = 0;
186+
187+
FRPFullSizePhotoViewModel *viewModel = \
188+
[[FRPFullSizePhotoViewModel alloc] initWithPhotoArray:model initialPhotoIndex:initialPhotoIndex];
189+
190+
id mockViewModel = [OCMockObject partialMockForObject:viewModel];
191+
[[[mockViewModel expect] andReturn:model[0]] photoModelAtIndex:initialPhotoIndex];
192+
193+
id returnedObject = [mockViewModel initialPhotoModel];
194+
195+
expect(returnedObject).to.equal(model[0]);
196+
197+
[mockViewModel verify];
198+
});
199+
```
200+
201+
这个测试是用来确认当`initialPhotoModel`被调用时,接下来它应该调用`photoModelAtIndex:`方法并将`initialPhotoIndex`作为参数传入。这个测试是否简单取决于我们测试`photoModelAtIndex:`是否充分。
202+
203+
接下来,就让我们一起来看看`FRPGalleryViewModel`,这看似非常简单:
204+
205+
```Objective-C
206+
- (instancetype)init {
207+
self = [super init];
208+
if(!self) return nil;
209+
210+
RAC(self, model) = [[[FRPPhotoImporter importPhotos] logError] catchTo:[RACSignal empty]];
211+
212+
return self;
213+
}
214+
215+
```
216+
217+
然而,它可测性不高,需要重构。
218+
219+
我们简单地重构下视图模型。新的实现如下:
220+
221+
```Objective-C
222+
@implementation FRPGalleryViewModel
223+
224+
- (instancetype)init {
225+
self = [super init];
226+
if(!self) return nil;
227+
228+
RAC(self, model) = [self importPhotosSignal];
229+
230+
return self;
231+
}
232+
233+
- (RACSignal *)importPhotosSignal {
234+
return [[[FRPPhotoImporter importPhotos] logError] catchTo:[RACSignal empty]];
235+
}
236+
237+
@end
238+
239+
```
240+
241+
我们把`importPhotos`的调用抽出来,以方便测试这个方法是否被调用。我们不会测试`FRPPhotoImporter`,关于它的测试(即单例测试)已经超出了本书的范畴。
242+
243+
这部分的测试代码如下:
244+
245+
```Objective-C
246+
#import "Specta.h"
247+
#import <OCMock/OCMock.h>
248+
249+
#import "FRPGalleryViewModel.h"
250+
251+
@interface FRPGalleryViewModel ()
252+
253+
- (RACSignal *)importPhotosSignal;
254+
255+
@end
256+
257+
SpecBegin(FRPGalleryViewModel)
258+
259+
describe(@"FRPGalleryViewModel",^{
260+
it(@"should be initialized and call importPhotos", ^{
261+
id mockObject = [OCMockObject mockForClass:[FRPGalleryViewModel class]];
262+
[[[mockObject expect] andReturn:[RACSignal empty]] importPhotosSignal];
263+
264+
mockObject = [mockObject init];
265+
266+
[mockObject verify];
267+
[mockObject stopMocking];
268+
});
269+
});
270+
271+
```
272+
273+
为了测试一个方法,测试代码也太多了吧! 我知道,我知道~ 这是OCMock没落的原因之一,它竟然需要这么多的模板。但你不能责怪它,因为它要工作在令它不寒而栗的Objective-C平台上!
274+
275+
276+
277+
278+

0 commit comments

Comments
 (0)