Вопрос

I've got UITableView which has delegate and separated datasource. DataSource is subclass of NSObject - ArrayDataSource class.

#import <Foundation/Foundation.h>

typedef void (^ConfigureCellBlock)(id cell, id object);

@interface ArrayDataSource : NSObject <UITableViewDataSource>

@property (nonatomic, readonly) NSArray *items;
@property (nonatomic, readonly) NSString *cellIdentifier;

- initWithItems:(NSArray *)items cellIdentifier:(NSString *)cellIdentifier configureCellBlock:(ConfigureCellBlock)block;

- (void)setItems:(NSArray *)items;
- (void)setCellIdentifier:(NSString *)cellIdentifier;
- (void)setConfigureCellBlock:(ConfigureCellBlock)block;

@end



#import "ArrayDataSource.h"

@interface ArrayDataSource ()
@property (nonatomic, copy) ConfigureCellBlock configureCellBlock;
@end

@implementation ArrayDataSource {
    NSArray *_items;
    NSString *_cellIdentifier;
}

#pragma mark - External
- (id)initWithItems:(NSArray *)items cellIdentifier:(NSString *)cellIdentifier configureCellBlock:(ConfigureCellBlock)block {
    self = [super init];
    if (self) {
        _items = items;
        _cellIdentifier = cellIdentifier;
        _configureCellBlock = [block copy];
    }
    return self;
}

- (void)setItems:(NSArray *)items {
    _items = items;
}

- (void)setCellIdentifier:(NSString *)cellIdentifier {
    _cellIdentifier = cellIdentifier;
}

- (void)setConfigureCellBlock:(ConfigureCellBlock)block {
    _configureCellBlock = block;
}


#pragma mark - Private
- (id)_itemAtIndexPath:(NSIndexPath *)indexPath {
    return _items[indexPath.row];
}

#pragma mark - UITableViewDataSource
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return _items.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    id cell = [tableView dequeueReusableCellWithIdentifier:_cellIdentifier forIndexPath:indexPath];
    id item = [self _itemAtIndexPath:indexPath];
    _configureCellBlock(cell, item);
    return cell;
}


#pragma mark - Accessors
- (NSArray *)items {
    return _items;
}

- (NSString *)cellIdentifier {
    return _cellIdentifier;
}

@end

Now I want to test if ConfigureCellBlock has been called with correct arguments in tableView:cellForRowAtIndexPath:.

My test now looks like following but it's not working correctly because it pass and block isn't called after all. My test class is delegate for _tableView.

#pragma mark - UITableView tests + Delegate + DataSource
- (void)testDataSource {
    UITableView *_tableView = [[UITableView alloc] init];
    _tableView.delegate = self;

    [dataSource setItems:@[@"A"]];

    [dataSource setConfigureCellBlock:^(UITableViewCell *cell, NSString *object) {
        XCTAssertEqualObjects(object, @"B", @"");
    }];

    id mockDataSource = [OCMockObject partialMockForObject:dataSource];
    SEL selector = NSSelectorFromString(@"_itemAtIndexPath:");
    [[[mockDataSource stub] andReturn:dataSource.items[0]] methodForSelector:selector];

    _tableView.dataSource = mockDataSource;

    UITableViewCell *cell = [[UITableViewCell alloc] initWithFrame:CGRectMake(0, 0, 320, 44)];
    NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];

    id mockTableView = [OCMockObject partialMockForObject:_tableView];
    [[[mockTableView stub] andReturn:cell] dequeueReusableCellWithIdentifier:OCMOCK_ANY forIndexPath:indexPath];

    [mockTableView reloadData];
}

How should I test this correctly?

Thank you in advance.

Это было полезно?

Решение 2

I did it. Test tests if block has been called in -tableView:cellForRowAtIndexPath::

- (void)testDataSource {
    /// Data Source
    [dataSource setItems:@[@"A"]];
    [dataSource setConfigureCellBlock:^(UITableViewCell *cell, NSString *object) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-retain-cycles"
        XCTAssertNotNil(cell, @"");
        XCTAssertEqualObjects(object, @"A", @"");
#pragma clang diagnostic pop
    }];

    /// Table View
    id mockTableView = [OCMockObject niceMockForClass:[UITableView class]];
    [[[mockTableView stub] andReturn:dataSource] dataSource];

    UITableViewCell *cell = [[UITableViewCell alloc] initWithFrame:CGRectMake(0, 0, 320, 44)];
    NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];

    [[[mockTableView stub] andReturn:cell] dequeueReusableCellWithIdentifier:OCMOCK_ANY forIndexPath:OCMOCK_ANY];

    [[mockTableView dataSource] tableView:mockTableView cellForRowAtIndexPath:indexPath];
}

I used #pragma and ignored warning about retain-cycle in the block. I don't know how should i put assert in the block without retain-cycles.

Thank you @sammyd and @Sebastian.

Другие советы

I test this kind of thing by putting a flag in the block:

__block BOOL invoked = NO;
[dataSource setConfigureCellBlock:^(UITableViewCell *cell, NSString *object) {
    invoked = YES;
}];

// do something to invoke block...
XCTAssertTrue(invoked, @"configure cell block was not invoked");

It seems to me you're doing a lot of unnecessary mocking. I try to focus test setup specifically on what's being verified, for example sending nil parameters to minimize side effects and stubbing. Here's the whole test as I would write it:

- (void)testDataSource {
    [dataSource setItems:@[@"A"]];

    __block BOOL invoked = NO;
    [dataSource setConfigureCellBlock:^(UITableViewCell *cell, NSString *object) {
        XCTAssertEqualObjects(object, @"A", @"got the wrong item");
        invoked = YES;
    }];

    [dataSource tableView:nil cellForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]];
    XCTAssertTrue(invoked, @"configure cell block was not invoked");
}
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top