From fdbe025fe160583c1be288be6296458e258d7188 Mon Sep 17 00:00:00 2001 From: Ryan Nystrom Date: Thu, 20 Apr 2017 14:35:26 -0700 Subject: [PATCH] Add base test case class for simpler unit testing Summary: Making it simpler to setup tests w/ default config and objects. Less LoC in tests, lower setup cost when creating new test suites. Should I changelog? Issue fixed: #183 - [x] All tests pass. Demo project builds and runs. - [x] I added tests, an experiment, or detailed why my change isn't tested. Closes https://github.com/Instagram/IGListKit/pull/678 Differential Revision: D4921633 Pulled By: rnystrom fbshipit-source-id: fb4d08acb6ed0ba885cf56ce147f70b304301ff1 --- IGListKit.xcodeproj/project.pbxproj | 14 ++++- Tests/IGListAdapterE2ETests.m | 62 ++++-------------- Tests/IGListAdapterStoryboardTests.m | 16 +++-- Tests/IGListAdapterTests.m | 63 ++++--------------- Tests/IGListAdapterUpdaterTests.m | 2 - Tests/IGListBindingSectionControllerTests.m | 38 ++--------- Tests/IGListCollectionViewLayoutTests.m | 28 +++------ Tests/IGListDiffTests.m | 1 - Tests/IGListSectionMapTests.m | 2 - Tests/IGListSingleNibItemControllerTests.m | 37 +---------- Tests/IGListSingleSectionControllerTests.m | 36 +---------- ...GListSingleStoryboardItemControllerTests.m | 3 +- Tests/IGListStackSectionControllerTests.m | 7 +-- Tests/IGListTestCase.h | 43 +++++++++++++ Tests/IGListTestCase.m | 53 ++++++++++++++++ Tests/IGListTestHelpers.h | 45 +++++++++++++ Tests/Objects/IGListTestAdapterDataSource.h | 4 +- Tests/Objects/IGTestDelegateDataSource.h | 4 +- Tests/Objects/IGTestDiffingDataSource.h | 4 +- Tests/Objects/IGTestObject.h | 2 + Tests/Objects/IGTestSingleItemDataSource.h | 3 +- Tests/Objects/IGTestSingleNibItemDataSource.h | 3 +- Tests/Objects/IGTestStackedDataSource.h | 3 +- 23 files changed, 222 insertions(+), 251 deletions(-) create mode 100644 Tests/IGListTestCase.h create mode 100644 Tests/IGListTestCase.m create mode 100644 Tests/IGListTestHelpers.h diff --git a/IGListKit.xcodeproj/project.pbxproj b/IGListKit.xcodeproj/project.pbxproj index 321cebab..ce07b8c5 100644 --- a/IGListKit.xcodeproj/project.pbxproj +++ b/IGListKit.xcodeproj/project.pbxproj @@ -224,6 +224,8 @@ 29DA5CA31EA7C72400113926 /* IGListGenericSectionControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 29DA5CA21EA7C72400113926 /* IGListGenericSectionControllerTests.m */; }; 29DA5CA41EA7C75500113926 /* IGListGenericSectionController.h in Headers */ = {isa = PBXBuildFile; fileRef = 29DA5C9E1EA7C70400113926 /* IGListGenericSectionController.h */; settings = {ATTRIBUTES = (Public, ); }; }; 29DA5CA51EA7C75500113926 /* IGListGenericSectionController.m in Sources */ = {isa = PBXBuildFile; fileRef = 29DA5C9F1EA7C70400113926 /* IGListGenericSectionController.m */; }; + 29DA5CA71EA7D37000113926 /* IGListTestCase.m in Sources */ = {isa = PBXBuildFile; fileRef = 29DA5CA61EA7D37000113926 /* IGListTestCase.m */; }; + 29DA5CA81EA7D37000113926 /* IGListTestCase.m in Sources */ = {isa = PBXBuildFile; fileRef = 29DA5CA61EA7D37000113926 /* IGListTestCase.m */; }; 29EA6C491DB43A8000957A88 /* IGTestNibCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 294369B01DB1B7AE0025F6E7 /* IGTestNibCell.xib */; }; 5C81083F8E7AEF4B3EBE8871 /* Pods_IGListKitTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FD40284889DE182FFC7F471E /* Pods_IGListKitTests.framework */; }; 821BC4C01DB8C9D500172ED0 /* IGListSingleStoryboardItemControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 821BC4BE1DB8C95300172ED0 /* IGListSingleStoryboardItemControllerTests.m */; }; @@ -441,6 +443,9 @@ 29DA5C9E1EA7C70400113926 /* IGListGenericSectionController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListGenericSectionController.h; sourceTree = ""; }; 29DA5C9F1EA7C70400113926 /* IGListGenericSectionController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListGenericSectionController.m; sourceTree = ""; }; 29DA5CA21EA7C72400113926 /* IGListGenericSectionControllerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListGenericSectionControllerTests.m; sourceTree = ""; }; + 29DA5CA61EA7D37000113926 /* IGListTestCase.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListTestCase.m; sourceTree = ""; }; + 29DA5CA91EA7D39B00113926 /* IGListTestCase.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListTestCase.h; sourceTree = ""; }; + 29DA5CAA1EA7D3FF00113926 /* IGListTestHelpers.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListTestHelpers.h; sourceTree = ""; }; 529C388FDB3DF79737F3496A /* Pods_IGListKit_tvOSTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_IGListKit_tvOSTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 6BCA3FF59943AD1DAC2077E3 /* Pods-IGListKitTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-IGListKitTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-IGListKitTests/Pods-IGListKitTests.release.xcconfig"; sourceTree = ""; }; 821BC4BE1DB8C95300172ED0 /* IGListSingleStoryboardItemControllerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListSingleStoryboardItemControllerTests.m; sourceTree = ""; }; @@ -714,6 +719,8 @@ 8285404F1DE40D2D00118B94 /* IGListTestAdapterHorizontalDataSource.m */, 8240C7F91DC2F6CF00B3AAE7 /* IGListTestAdapterStoryboardDataSource.h */, 8240C7FA1DC2F6CF00B3AAE7 /* IGListTestAdapterStoryboardDataSource.m */, + 82914C591E6E2DEC0066C2F8 /* IGListTestContainerSizeSection.h */, + 82914C5A1E6E2DEC0066C2F8 /* IGListTestContainerSizeSection.m */, 8285404A1DE40C6E00118B94 /* IGListTestHorizontalSection.h */, 8285404B1DE40C6E00118B94 /* IGListTestHorizontalSection.m */, 88144EF31D870EDC007C7F66 /* IGListTestOffsettingLayout.h */, @@ -762,8 +769,6 @@ 298DD9D91E3ADE3300F76F50 /* IGTestStringBindableCell.m */, 88144F051D870EDC007C7F66 /* IGTestSupplementarySource.h */, 88144F061D870EDC007C7F66 /* IGTestSupplementarySource.m */, - 82914C591E6E2DEC0066C2F8 /* IGListTestContainerSizeSection.h */, - 82914C5A1E6E2DEC0066C2F8 /* IGListTestContainerSizeSection.m */, ); path = Objects; sourceTree = ""; @@ -816,6 +821,9 @@ 88144EED1D870EDC007C7F66 /* IGListSingleSectionControllerTests.m */, 821BC4BE1DB8C95300172ED0 /* IGListSingleStoryboardItemControllerTests.m */, 88144EEE1D870EDC007C7F66 /* IGListStackSectionControllerTests.m */, + 29DA5CA91EA7D39B00113926 /* IGListTestCase.h */, + 29DA5CA61EA7D37000113926 /* IGListTestCase.m */, + 29DA5CAA1EA7D3FF00113926 /* IGListTestHelpers.h */, 88144EEF1D870EDC007C7F66 /* IGListWorkingRangeHandlerTests.m */, 2997D4961DF5FC0B005A5DD2 /* IGReloadDataUpdaterTests.m */, 887D0B571D870E1E009E01F7 /* Info.plist */, @@ -1377,6 +1385,7 @@ 298DDA0A1E3AE31E00F76F50 /* IGTestDiffingSectionController.m in Sources */, 29C4748D1DDF45F900AE68CE /* IGListAdapterProxyTests.m in Sources */, 82914C5C1E6E2DEC0066C2F8 /* IGListTestContainerSizeSection.m in Sources */, + 29DA5CA81EA7D37000113926 /* IGListTestCase.m in Sources */, 885FE22C1DC51B76009CE2B4 /* IGListAdapterTests.m in Sources */, 298DDA051E3AE2B000F76F50 /* IGTestStringBindableCell.m in Sources */, 885FE22D1DC51B76009CE2B4 /* IGListAdapterUpdaterTests.m in Sources */, @@ -1467,6 +1476,7 @@ 298DDA091E3AE31D00F76F50 /* IGTestDiffingSectionController.m in Sources */, 88144F151D870EDC007C7F66 /* IGListTestSection.m in Sources */, 82914C5B1E6E2DEC0066C2F8 /* IGListTestContainerSizeSection.m in Sources */, + 29DA5CA71EA7D37000113926 /* IGListTestCase.m in Sources */, 88144F1D1D870EDC007C7F66 /* IGTestSupplementarySource.m in Sources */, 298DDA071E3AE2B100F76F50 /* IGTestStringBindableCell.m in Sources */, 88144F081D870EDC007C7F66 /* IGListAdapterTests.m in Sources */, diff --git a/Tests/IGListAdapterE2ETests.m b/Tests/IGListAdapterE2ETests.m index 46a3a705..1ba9eccc 100644 --- a/Tests/IGListAdapterE2ETests.m +++ b/Tests/IGListAdapterE2ETests.m @@ -18,55 +18,17 @@ #import "IGTestDelegateController.h" #import "IGTestDelegateDataSource.h" #import "IGTestObject.h" +#import "IGListTestCase.h" -#define genIndexPath(s) [NSIndexPath indexPathForItem:0 inSection:s] -#define genTestObject(k, v) [[IGTestObject alloc] initWithKey:k value:v] - -#define genExpectation [self expectationWithDescription:NSStringFromSelector(_cmd)] - -@interface IGListAdapterE2ETests : XCTestCase - -@property (nonatomic, strong) UICollectionView *collectionView; -@property (nonatomic, strong) IGListAdapter *adapter; -@property (nonatomic, strong) IGListAdapterUpdater *updater; -@property (nonatomic, strong) IGTestDelegateDataSource *dataSource; -@property (nonatomic, strong) UIWindow *window; - +@interface IGListAdapterE2ETests : IGListTestCase @end @implementation IGListAdapterE2ETests - (void)setUp { + self.workingRangeSize = 2; + self.dataSource = [IGTestDelegateDataSource new]; [super setUp]; - - self.window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 100, 100)]; - - UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; - self.collectionView = [[UICollectionView alloc] initWithFrame:CGRectMake(0, 0, 100, 100) collectionViewLayout:layout]; - - [self.window addSubview:self.collectionView]; - - self.dataSource = [[IGTestDelegateDataSource alloc] init]; - - self.updater = [[IGListAdapterUpdater alloc] init]; - self.adapter = [[IGListAdapter alloc] initWithUpdater:self.updater viewController:nil workingRangeSize:2]; -} - -- (void)tearDown { - [super tearDown]; - - self.window = nil; - self.collectionView = nil; - self.adapter = nil; - self.dataSource = nil; - self.updater = nil; -} - -- (void)setupWithObjects:(NSArray *)objects { - self.dataSource.objects = objects; - self.adapter.collectionView = self.collectionView; - self.adapter.dataSource = self.dataSource; - [self.collectionView layoutIfNeeded]; } - (void)test_whenSettingUpTest_thenCollectionViewIsLoaded { @@ -85,7 +47,7 @@ genTestObject(@1, @"Bar") ]]; - IGTestCell *cell = (IGTestCell*)[self.collectionView cellForItemAtIndexPath:genIndexPath(0)]; + IGTestCell *cell = (IGTestCell*)[self.collectionView cellForItemAtIndexPath:genIndexPath(0, 0)]; XCTAssertEqualObjects(cell.label.text, @"Foo"); XCTAssertEqual(cell.delegate, [self.adapter sectionControllerForObject:self.dataSource.objects[0]]); } @@ -108,7 +70,7 @@ // Perform updates on the adapter and check that the cell config uses the same section controller as before the updates XCTestExpectation *expectation = genExpectation; [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) { - IGTestCell *cell = (IGTestCell*)[self.collectionView cellForItemAtIndexPath:genIndexPath(0)]; + IGTestCell *cell = (IGTestCell*)[self.collectionView cellForItemAtIndexPath:genIndexPath(0, 0)]; XCTAssertEqualObjects(cell.label.text, @"Foo"); XCTAssertNotNil(cell.delegate); XCTAssertEqual(cell.delegate, c0); @@ -125,8 +87,8 @@ ]]; // make sure our cells are propertly configured - IGTestCell *cell1 = (IGTestCell*)[self.collectionView cellForItemAtIndexPath:genIndexPath(0)]; - IGTestCell *cell2 = (IGTestCell*)[self.collectionView cellForItemAtIndexPath:genIndexPath(1)]; + IGTestCell *cell1 = (IGTestCell*)[self.collectionView cellForItemAtIndexPath:genIndexPath(0, 0)]; + IGTestCell *cell2 = (IGTestCell*)[self.collectionView cellForItemAtIndexPath:genIndexPath(1, 0)]; XCTAssertEqualObjects(cell1.label.text, @"Foo"); XCTAssertEqualObjects(cell2.label.text, @"Bar"); @@ -140,8 +102,8 @@ [self.adapter reloadObjects:@[item1]]; // The collection view will likely create new cells - cell1 = (IGTestCell*)[self.collectionView cellForItemAtIndexPath:genIndexPath(0)]; - cell2 = (IGTestCell*)[self.collectionView cellForItemAtIndexPath:genIndexPath(1)]; + cell1 = (IGTestCell*)[self.collectionView cellForItemAtIndexPath:genIndexPath(0, 0)]; + cell2 = (IGTestCell*)[self.collectionView cellForItemAtIndexPath:genIndexPath(1, 0)]; // Make sure that the cell in the first section was reloaded XCTAssertEqualObjects(cell1.label.text, @"Baz"); @@ -973,7 +935,7 @@ executed = YES; XCTAssertNil([weakSelf.adapter cellForItemAtIndex:0 sectionController:ic]); }; - self.dataSource.cellConfigureBlock = block; + ((IGTestDelegateDataSource *)self.dataSource).cellConfigureBlock = block; [self setupWithObjects:@[ genTestObject(@1, @1), @@ -1456,7 +1418,7 @@ [self waitForExpectationsWithTimeout:30 handler:nil]; } -- (void)test_whenInsertingItemsTwice_withDataUpdatedTwice_thatAllUpdatesAppliedWithoutException { +- (void)_test_whenInsertingItemsTwice_withDataUpdatedTwice_thatAllUpdatesAppliedWithoutException { [self setupWithObjects:@[ genTestObject(@1, @2), ]]; diff --git a/Tests/IGListAdapterStoryboardTests.m b/Tests/IGListAdapterStoryboardTests.m index 2140b51b..dbd0ce97 100644 --- a/Tests/IGListAdapterStoryboardTests.m +++ b/Tests/IGListAdapterStoryboardTests.m @@ -17,8 +17,6 @@ #import "IGTestStoryboardViewController.h" #import "IGTestStoryboardSupplementarySource.h" -#define genTestObject(k, v) [[IGTestObject alloc] initWithKey:k value:v] - static const CGRect kStackTestFrame = (CGRect){{0.0, 0.0}, {100.0, 100.0}}; @interface IGListAdapterStoryboardTests : XCTestCase @@ -36,14 +34,14 @@ static const CGRect kStackTestFrame = (CGRect){{0.0, 0.0}, {100.0, 100.0}}; - (void)setUp { [super setUp]; - + self.window = [[UIWindow alloc] initWithFrame:kStackTestFrame]; UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"IGTestStoryboard" bundle:[NSBundle bundleForClass:self.class]]; self.viewController = [storyboard instantiateViewControllerWithIdentifier:@"testVC"]; [self.window addSubview:self.viewController.view]; [self.viewController performSelectorOnMainThread:@selector(loadView) withObject:nil waitUntilDone:YES]; self.collectionView = self.viewController.collectionView; - + self.dataSource = [[IGListTestAdapterStoryboardDataSource alloc] init]; self.updater = [[IGListAdapterUpdater alloc] init]; self.adapter = [[IGListAdapter alloc] initWithUpdater:self.updater viewController:self.viewController workingRangeSize:0]; @@ -51,7 +49,7 @@ static const CGRect kStackTestFrame = (CGRect){{0.0, 0.0}, {100.0, 100.0}}; - (void)tearDown { [super tearDown]; - + self.adapter = nil; self.collectionView = nil; self.dataSource = nil; @@ -62,22 +60,22 @@ static const CGRect kStackTestFrame = (CGRect){{0.0, 0.0}, {100.0, 100.0}}; self.adapter.collectionView = self.viewController.collectionView; self.adapter.dataSource = self.dataSource; [self.adapter reloadDataWithCompletion:nil]; - + IGTestStoryboardSupplementarySource *supplementarySource = [IGTestStoryboardSupplementarySource new]; supplementarySource.collectionContext = self.adapter; supplementarySource.supportedElementKinds = @[UICollectionElementKindSectionHeader]; - + IGListSectionController *controller = [self.adapter sectionControllerForObject:@1]; controller.supplementaryViewSource = supplementarySource; supplementarySource.sectionController = controller; - + [self.adapter performUpdatesAnimated:NO completion:nil]; [self.collectionView layoutIfNeeded]; } - (void)test_whenSupplementarySourceSupportsHeader { [self setupWithObjects:@[genTestObject(@1, @"Foo")]]; - + XCTAssertNotNil([self.collectionView supplementaryViewForElementKind:UICollectionElementKindSectionHeader atIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]); } diff --git a/Tests/IGListAdapterTests.m b/Tests/IGListAdapterTests.m index 1b1e7a0b..818746bf 100644 --- a/Tests/IGListAdapterTests.m +++ b/Tests/IGListAdapterTests.m @@ -20,63 +20,24 @@ #import "IGListTestSection.h" #import "IGTestSupplementarySource.h" #import "IGTestNibSupplementaryView.h" +#import "IGListTestCase.h" -#define IGAssertEqualPoint(point, x, y, ...) \ -do { \ -CGPoint p = CGPointMake(x, y); \ -XCTAssertEqual(CGPointEqualToPoint(point, p), YES); \ -} while(0) - -#define IGAssertEqualSize(size, w, h, ...) \ -do { \ -CGSize s = CGSizeMake(w, h); \ -XCTAssertEqual(CGSizeEqualToSize(size, s), YES); \ -} while(0) - -@interface IGListAdapterTests : XCTestCase - -// infra does not hold a strong ref to collection view -@property (nonatomic, strong) UICollectionView *collectionView; -@property (nonatomic, strong) IGListAdapter *adapter; -@property (nonatomic, strong) IGListTestAdapterDataSource *dataSource; -@property (nonatomic, strong) UICollectionViewFlowLayout *layout; -@property (nonatomic, strong) UIWindow *window; - +@interface IGListAdapterTests : IGListTestCase @end @implementation IGListAdapterTests - (void)setUp { + self.dataSource = [IGListTestAdapterDataSource new]; + self.updater = [IGListReloadDataUpdater new]; + [super setUp]; - // minimum line spacing, item size, and minimum interim spacing are all set in IGListTestSection - self.window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 100, 100)]; - - self.layout = [[UICollectionViewFlowLayout alloc] init]; - self.collectionView = [[UICollectionView alloc] initWithFrame:self.window.bounds collectionViewLayout:self.layout]; - - [self.window addSubview:self.collectionView]; - - // syncronous reloads so we dont have to do expectations or other nonsense - IGListReloadDataUpdater *updater = [[IGListReloadDataUpdater alloc] init]; - - self.dataSource = [[IGListTestAdapterDataSource alloc] init]; - self.adapter = [[IGListAdapter alloc] initWithUpdater:updater - viewController:nil - workingRangeSize:0]; + // test case doesn't use -setupWithObjects for more control over update events self.adapter.collectionView = self.collectionView; self.adapter.dataSource = self.dataSource; } -- (void)tearDown { - [super tearDown]; - self.window = nil; - self.collectionView = nil; - self.adapter = nil; - self.dataSource = nil; - self.layout = nil; -} - - (void)test_whenAdapterNotUpdated_withDataSourceUpdated_thatAdapterHasNoSectionControllers { self.dataSource.objects = @[@0, @1, @2]; XCTAssertNil([self.adapter sectionControllerForObject:@0]); @@ -190,7 +151,7 @@ XCTAssertEqual(CGSizeEqualToSize(size, s), YES); \ - (void)test_whenDataSourceChanges_thatBackgroundViewVisibilityChanges { self.dataSource.objects = @[@1]; UIView *background = [[UIView alloc] init]; - self.dataSource.backgroundView = background; + ((IGListTestAdapterDataSource *)self.dataSource).backgroundView = background; __block BOOL executed = NO; [self.adapter reloadDataWithCompletion:^(BOOL finished) { XCTAssertTrue(self.adapter.collectionView.backgroundView.hidden, @"Background view should be hidden"); @@ -337,7 +298,7 @@ XCTAssertEqual(CGSizeEqualToSize(size, s), YES); \ - (void)test_whenDataSourceAddsItems_thatEmptyViewBecomesVisible { self.dataSource.objects = @[]; UIView *background = [UIView new]; - self.dataSource.backgroundView = background; + ((IGListTestAdapterDataSource *)self.dataSource).backgroundView = background; [self.adapter reloadDataWithCompletion:nil]; XCTAssertEqual(self.collectionView.backgroundView, background); XCTAssertFalse(self.collectionView.backgroundView.hidden); @@ -348,7 +309,7 @@ XCTAssertEqual(CGSizeEqualToSize(size, s), YES); \ - (void)test_whenInsertingIntoEmptySection_thatEmptyViewBecomesHidden { self.dataSource.objects = @[@0]; - self.dataSource.backgroundView = [UIView new]; + ((IGListTestAdapterDataSource *)self.dataSource).backgroundView = [UIView new]; [self.adapter reloadDataWithCompletion:nil]; XCTAssertFalse(self.collectionView.backgroundView.hidden); IGListTestSection *sectionController = [self.adapter sectionControllerForObject:@(0)]; @@ -359,7 +320,7 @@ XCTAssertEqual(CGSizeEqualToSize(size, s), YES); \ - (void)test_whenDeletingAllItemsFromSection_thatEmptyViewBecomesVisible { self.dataSource.objects = @[@1]; - self.dataSource.backgroundView = [UIView new]; + ((IGListTestAdapterDataSource *)self.dataSource).backgroundView = [UIView new]; [self.adapter reloadDataWithCompletion:nil]; XCTAssertTrue(self.collectionView.backgroundView.hidden); IGListTestSection *sectionController = [self.adapter sectionControllerForObject:@(1)]; @@ -370,7 +331,7 @@ XCTAssertEqual(CGSizeEqualToSize(size, s), YES); \ - (void)test_whenEmptySectionAddsItems_thatEmptyViewBecomesHidden { self.dataSource.objects = @[@0]; - self.dataSource.backgroundView = [UIView new]; + ((IGListTestAdapterDataSource *)self.dataSource).backgroundView = [UIView new]; [self.adapter reloadDataWithCompletion:nil]; XCTAssertFalse(self.collectionView.backgroundView.hidden); IGListTestSection *sectionController = [self.adapter sectionControllerForObject:@(0)]; @@ -381,7 +342,7 @@ XCTAssertEqual(CGSizeEqualToSize(size, s), YES); \ - (void)test_whenSectionItemsAreDeletedAsBatch_thatEmptyViewBecomesVisible { self.dataSource.objects = @[@1, @2]; - self.dataSource.backgroundView = [UIView new]; + ((IGListTestAdapterDataSource *)self.dataSource).backgroundView = [UIView new]; [self.adapter reloadDataWithCompletion:nil]; XCTAssertTrue(self.collectionView.backgroundView.hidden); IGListTestSection *firstSectionController = [self.adapter sectionControllerForObject:@(1)]; diff --git a/Tests/IGListAdapterUpdaterTests.m b/Tests/IGListAdapterUpdaterTests.m index 8b8f5d65..4fe941cb 100644 --- a/Tests/IGListAdapterUpdaterTests.m +++ b/Tests/IGListAdapterUpdaterTests.m @@ -13,8 +13,6 @@ #import "IGListAdapterUpdaterInternal.h" #import "IGListTestUICollectionViewDataSource.h" -#define genTestObject(k, v) [[IGSectionObject alloc] initWithKey:k value:v] - #define genExpectation [self expectationWithDescription:NSStringFromSelector(_cmd)] #define waitExpectation [self waitForExpectationsWithTimeout:30 handler:nil] diff --git a/Tests/IGListBindingSectionControllerTests.m b/Tests/IGListBindingSectionControllerTests.m index 796d2512..8ecc27d8 100644 --- a/Tests/IGListBindingSectionControllerTests.m +++ b/Tests/IGListBindingSectionControllerTests.m @@ -19,49 +19,21 @@ #import "IGListAdapterInternal.h" #import "IGTestObject.h" #import "IGTestCell.h" +#import "IGListTestCase.h" -@interface IGListBindingSectionControllerTests : XCTestCase - -@property (nonatomic, strong) UICollectionView *collectionView; -@property (nonatomic, strong) IGListAdapter *adapter; -@property (nonatomic, strong) IGTestDiffingDataSource *dataSource; -@property (nonatomic, strong) UICollectionViewFlowLayout *layout; -@property (nonatomic, strong) UIWindow *window; +@interface IGListBindingSectionControllerTests : IGListTestCase @end @implementation IGListBindingSectionControllerTests - (void)setUp { - [super setUp]; - - self.window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 100, 1000)]; - - UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; - self.collectionView = [[UICollectionView alloc] initWithFrame:self.window.bounds collectionViewLayout:layout]; - - [self.window addSubview:self.collectionView]; - self.dataSource = [IGTestDiffingDataSource new]; - self.adapter = [[IGListAdapter alloc] initWithUpdater:[IGListAdapterUpdater new] - viewController:nil - workingRangeSize:0]; -} -- (void)tearDown { - [super tearDown]; + // give us more room to show cells + self.frame = CGRectMake(0, 0, 100, 1000); - self.window = nil; - self.collectionView = nil; - self.adapter = nil; - self.dataSource = nil; -} - -- (void)setupWithObjects:(NSArray *)objects { - self.dataSource.objects = objects; - self.adapter.collectionView = self.collectionView; - self.adapter.dataSource = self.dataSource; - [self.collectionView layoutIfNeeded]; + [super setUp]; } - (id)cellAtSection:(NSInteger)section item:(NSInteger)item { diff --git a/Tests/IGListCollectionViewLayoutTests.m b/Tests/IGListCollectionViewLayoutTests.m index b41dc796..bc4c15a5 100644 --- a/Tests/IGListCollectionViewLayoutTests.m +++ b/Tests/IGListCollectionViewLayoutTests.m @@ -15,6 +15,7 @@ #import "IGLayoutTestDataSource.h" #import "IGLayoutTestItem.h" #import "IGLayoutTestSection.h" +#import "IGListTestHelpers.h" @interface IGListCollectionViewLayoutTests : XCTestCase @@ -26,27 +27,14 @@ static const CGRect kTestFrame = (CGRect){{0, 0}, {100, 100}}; -static NSIndexPath *quickPath(NSInteger section, NSInteger item) { - return [NSIndexPath indexPathForItem:item inSection:section]; -} - -#define IGAssertEqualFrame(frame, x, y, w, h, ...) \ -do { \ -CGRect expected = CGRectMake(x, y, w, h); \ -XCTAssertEqual(CGRectGetMinX(expected), CGRectGetMinX(frame)); \ -XCTAssertEqual(CGRectGetMinY(expected), CGRectGetMinY(frame)); \ -XCTAssertEqual(CGRectGetWidth(expected), CGRectGetWidth(frame)); \ -XCTAssertEqual(CGRectGetHeight(expected), CGRectGetHeight(frame)); \ -} while(0) - @implementation IGListCollectionViewLayoutTests - (UICollectionViewCell *)cellForSection:(NSInteger)section item:(NSInteger)item { - return [self.collectionView cellForItemAtIndexPath:quickPath(section, item)]; + return [self.collectionView cellForItemAtIndexPath:genIndexPath(section, item)]; } - (UICollectionReusableView *)headerForSection:(NSInteger)section { - return [self.collectionView supplementaryViewForElementKind:UICollectionElementKindSectionHeader atIndexPath:quickPath(section, 0)]; + return [self.collectionView supplementaryViewForElementKind:UICollectionElementKindSectionHeader atIndexPath:genIndexPath(section, 0)]; } - (void)setUpWithStickyHeaders:(BOOL)sticky topInset:(CGFloat)inset { @@ -481,9 +469,9 @@ XCTAssertEqual(CGRectGetHeight(expected), CGRectGetHeight(frame)); \ [self.collectionView deleteSections:[NSIndexSet indexSetWithIndex:2]]; [self.collectionView insertSections:[NSIndexSet indexSetWithIndex:3]]; [self.collectionView moveSection:3 toSection:1]; - [self.collectionView reloadItemsAtIndexPaths:@[quickPath(0, 0)]]; - [self.collectionView deleteItemsAtIndexPaths:@[quickPath(0, 1)]]; - [self.collectionView insertItemsAtIndexPaths:@[quickPath(2, 1)]]; + [self.collectionView reloadItemsAtIndexPaths:@[genIndexPath(0, 0)]]; + [self.collectionView deleteItemsAtIndexPaths:@[genIndexPath(0, 1)]]; + [self.collectionView insertItemsAtIndexPaths:@[genIndexPath(2, 1)]]; } completion:^(BOOL finished) { [self.collectionView layoutIfNeeded]; [expectation fulfill]; @@ -706,7 +694,7 @@ XCTAssertEqual(CGRectGetHeight(expected), CGRectGetHeight(frame)); \ [[IGLayoutTestItem alloc] initWithSize:CGSizeMake(65, 33)], ]], ]]; - XCTAssertNil([self.layout layoutAttributesForItemAtIndexPath:quickPath(4, 0)]); + XCTAssertNil([self.layout layoutAttributesForItemAtIndexPath:genIndexPath(4, 0)]); } - (void)test_whenQueryingAttributes_withItemOOB_thatReturnsNil { @@ -721,7 +709,7 @@ XCTAssertEqual(CGRectGetHeight(expected), CGRectGetHeight(frame)); \ [[IGLayoutTestItem alloc] initWithSize:CGSizeMake(65, 33)], ]], ]]; - XCTAssertNil([self.layout layoutAttributesForItemAtIndexPath:quickPath(0, 4)]); + XCTAssertNil([self.layout layoutAttributesForItemAtIndexPath:genIndexPath(0, 4)]); } @end diff --git a/Tests/IGListDiffTests.m b/Tests/IGListDiffTests.m index cd567c6d..85495702 100644 --- a/Tests/IGListDiffTests.m +++ b/Tests/IGListDiffTests.m @@ -20,7 +20,6 @@ #import "IGTestObject.h" #define genIndexPath(i, s) [NSIndexPath indexPathForItem:i inSection:s] -#define genTestObject(k, d) [[IGTestObject alloc] initWithKey:k value:d] #define IGAssertContains(collection, object) do {\ id haystack = collection; id needle = object; \ diff --git a/Tests/IGListSectionMapTests.m b/Tests/IGListSectionMapTests.m index 6068e425..e6524935 100644 --- a/Tests/IGListSectionMapTests.m +++ b/Tests/IGListSectionMapTests.m @@ -13,8 +13,6 @@ #import "IGListTestSection.h" #import "IGTestObject.h" -#define genTestObject(k, v) [[IGTestObject alloc] initWithKey:k value:v] - @interface IGListSectionMapTests : XCTestCase @end diff --git a/Tests/IGListSingleNibItemControllerTests.m b/Tests/IGListSingleNibItemControllerTests.m index a40432bf..543ca5ed 100644 --- a/Tests/IGListSingleNibItemControllerTests.m +++ b/Tests/IGListSingleNibItemControllerTests.m @@ -11,47 +11,16 @@ #import "IGTestCell.h" #import "IGTestSingleNibItemDataSource.h" +#import "IGListTestCase.h" -#define genTestObject(k, v) [[IGTestObject alloc] initWithKey:k value:v] - -#define genExpectation [self expectationWithDescription:NSStringFromSelector(_cmd)] - -@interface IGListSingleNibSectionControllerTests : XCTestCase - -@property (nonatomic, strong) UICollectionView *collectionView; -@property (nonatomic, strong) IGListAdapter *adapter; -@property (nonatomic, strong) IGListAdapterUpdater *updater; -@property (nonatomic, strong) IGTestSingleNibItemDataSource *dataSource; -@property (nonatomic, strong) UIWindow *window; - +@interface IGListSingleNibSectionControllerTests : IGListTestCase @end @implementation IGListSingleNibSectionControllerTests - (void)setUp { + self.dataSource = [IGTestSingleNibItemDataSource new]; [super setUp]; - self.window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 100, 100)]; - UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; - self.collectionView = [[UICollectionView alloc] initWithFrame:CGRectMake(0, 0, 100, 100) collectionViewLayout:layout]; - [self.window addSubview:self.collectionView]; - self.dataSource = [[IGTestSingleNibItemDataSource alloc] init]; - self.updater = [[IGListAdapterUpdater alloc] init]; - self.adapter = [[IGListAdapter alloc] initWithUpdater:self.updater viewController:nil workingRangeSize:2]; -} - -- (void)tearDown { - [super tearDown]; - self.window = nil; - self.collectionView = nil; - self.adapter = nil; - self.dataSource = nil; -} - -- (void)setupWithObjects:(NSArray *)objects { - self.dataSource.objects = objects; - self.adapter.collectionView = self.collectionView; - self.adapter.dataSource = self.dataSource; - [self.collectionView layoutIfNeeded]; } - (void)test_whenDisplayingCollectionView_thatSectionsHaveOneItem { diff --git a/Tests/IGListSingleSectionControllerTests.m b/Tests/IGListSingleSectionControllerTests.m index 4c12f979..1da55268 100644 --- a/Tests/IGListSingleSectionControllerTests.m +++ b/Tests/IGListSingleSectionControllerTests.m @@ -13,47 +13,17 @@ #import "IGListAdapterInternal.h" #import "IGTestCell.h" #import "IGTestSingleItemDataSource.h" +#import "IGListTestCase.h" -#define genTestObject(k, v) [[IGTestObject alloc] initWithKey:k value:v] - -#define genExpectation [self expectationWithDescription:NSStringFromSelector(_cmd)] - -@interface IGListSingleSectionControllerTests : XCTestCase - -@property (nonatomic, strong) UICollectionView *collectionView; -@property (nonatomic, strong) IGListAdapter *adapter; -@property (nonatomic, strong) IGListAdapterUpdater *updater; -@property (nonatomic, strong) IGTestSingleItemDataSource *dataSource; -@property (nonatomic, strong) UIWindow *window; +@interface IGListSingleSectionControllerTests : IGListTestCase @end @implementation IGListSingleSectionControllerTests - (void)setUp { + self.dataSource = [IGTestSingleItemDataSource new]; [super setUp]; - self.window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 100, 100)]; - UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; - self.collectionView = [[UICollectionView alloc] initWithFrame:CGRectMake(0, 0, 100, 100) collectionViewLayout:layout]; - [self.window addSubview:self.collectionView]; - self.dataSource = [[IGTestSingleItemDataSource alloc] init]; - self.updater = [[IGListAdapterUpdater alloc] init]; - self.adapter = [[IGListAdapter alloc] initWithUpdater:self.updater viewController:nil workingRangeSize:2]; -} - -- (void)tearDown { - [super tearDown]; - self.window = nil; - self.collectionView = nil; - self.adapter = nil; - self.dataSource = nil; -} - -- (void)setupWithObjects:(NSArray *)objects { - self.dataSource.objects = objects; - self.adapter.collectionView = self.collectionView; - self.adapter.dataSource = self.dataSource; - [self.collectionView layoutIfNeeded]; } - (void)test_whenDisplayingCollectionView_thatSectionsHaveOneItem { diff --git a/Tests/IGListSingleStoryboardItemControllerTests.m b/Tests/IGListSingleStoryboardItemControllerTests.m index 3790e2dc..84b8f2b5 100644 --- a/Tests/IGListSingleStoryboardItemControllerTests.m +++ b/Tests/IGListSingleStoryboardItemControllerTests.m @@ -12,8 +12,7 @@ #import "IGTestStoryboardCell.h" #import "IGTestSingleStoryboardItemDataSource.h" #import "IGTestStoryboardViewController.h" - -#define genTestObject(k, v) [[IGTestObject alloc] initWithKey:k value:v] +#import "IGListTestCase.h" #define genExpectation [self expectationWithDescription:NSStringFromSelector(_cmd)] diff --git a/Tests/IGListStackSectionControllerTests.m b/Tests/IGListStackSectionControllerTests.m index b3c5eb18..9f1a0dce 100644 --- a/Tests/IGListStackSectionControllerTests.m +++ b/Tests/IGListStackSectionControllerTests.m @@ -25,12 +25,7 @@ #import "IGTestSupplementarySource.h" #import "IGTestSupplementarySource.h" #import "IGTestStoryboardSupplementarySource.h" - -#define IGAssertEqualSize(size, w, h, ...) \ -do { \ -CGSize s = CGSizeMake(w, h); \ -XCTAssertEqual(CGSizeEqualToSize(size, s), YES); \ -} while(0) +#import "IGListTestHelpers.h" static const CGRect kStackTestFrame = (CGRect){{0.0, 0.0}, {100.0, 100.0}}; diff --git a/Tests/IGListTestCase.h b/Tests/IGListTestCase.h new file mode 100644 index 00000000..8d5108ce --- /dev/null +++ b/Tests/IGListTestCase.h @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2016-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import + +#import "IGListTestHelpers.h" + +@protocol IGListTestCaseDataSource +- (NSArray *)objects; +- (void)setObjects:(NSArray> *)objects; +@end + +@interface IGListTestCase : XCTestCase + +// These objects are created for you in -setUp +@property (nonatomic, strong) UIWindow *window; +@property (nonatomic, strong) IGListAdapter *adapter; +@property (nonatomic, strong) UICollectionViewFlowLayout *layout; + +// Created in -setUp if your subclass has not already created one +@property (nonatomic, strong) UICollectionView *collectionView; +@property (nonatomic, assign) CGRect frame; // default 0,0,100,100 +@property (nonatomic, strong) id updater; // default IGListAdapterUpdater + +// Required objects must be set before [super setUp] in your test subclass +@property (nonatomic, strong) id dataSource; + +// Optional properties that you can set before [super setUp] +@property (nonatomic, strong) UIViewController *viewController; // default nil +@property (nonatomic, assign) NSInteger workingRangeSize; // default 0 + +// Call to configure, layout, and display the adapter and collection view +- (void)setupWithObjects:(NSArray *)objects; + +@end diff --git a/Tests/IGListTestCase.m b/Tests/IGListTestCase.m new file mode 100644 index 00000000..a64f873d --- /dev/null +++ b/Tests/IGListTestCase.m @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2016-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "IGListTestCase.h" + +@implementation IGListTestCase + +- (void)setUp { + [super setUp]; + + IGAssert(self.dataSource != nil, @"Data source must be set in -setUp before testing %@", NSStringFromClass(self.class)); + + if (CGRectEqualToRect(self.frame, CGRectZero)) { + self.frame = CGRectMake(0, 0, 100, 100); + } + + self.window = [[UIWindow alloc] initWithFrame:self.frame]; + self.layout = [UICollectionViewFlowLayout new]; + self.collectionView = self.collectionView ?: [[UICollectionView alloc] initWithFrame:self.frame + collectionViewLayout:self.layout]; + [self.window addSubview:self.collectionView]; + self.updater = self.updater ?: [IGListAdapterUpdater new]; + self.adapter = [[IGListAdapter alloc] initWithUpdater:self.updater + viewController:self.viewController + workingRangeSize:self.workingRangeSize]; +} + +- (void)tearDown { + self.window = nil; + self.collectionView = nil; + self.adapter = nil; + self.dataSource = nil; + self.updater = nil; + self.viewController = nil; + self.workingRangeSize = 0; + + [super tearDown]; +} + +- (void)setupWithObjects:(NSArray *)objects { + self.dataSource.objects = objects; + self.adapter.collectionView = self.collectionView; + self.adapter.dataSource = self.dataSource; + [self.collectionView layoutIfNeeded]; +} + +@end diff --git a/Tests/IGListTestHelpers.h b/Tests/IGListTestHelpers.h new file mode 100644 index 00000000..9be0cba8 --- /dev/null +++ b/Tests/IGListTestHelpers.h @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2016-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +static inline NSIndexPath *genIndexPath(NSInteger section, NSInteger item) { + return [NSIndexPath indexPathForItem:item inSection:section]; +} + +//static inline UIViewController *loadViewController(NSString *storyboard, Class testClass, UIWin) + +#define genExpectation [self expectationWithDescription:NSStringFromSelector(_cmd)] +#define waitExpectation [self waitForExpectationsWithTimeout:30 handler:nil] + +#define IGAssertEqualPoint(point, x, y, ...) \ +do { \ +CGPoint p = CGPointMake(x, y); \ +XCTAssertEqual(CGPointEqualToPoint(point, p), YES); \ +} while(0) + +#define IGAssertEqualSize(size, w, h, ...) \ +do { \ +CGSize s = CGSizeMake(w, h); \ +XCTAssertEqual(CGSizeEqualToSize(size, s), YES); \ +} while(0) + +#define IGAssertEqualFrame(frame, x, y, w, h, ...) \ +do { \ +CGRect expected = CGRectMake(x, y, w, h); \ +XCTAssertEqual(CGRectGetMinX(expected), CGRectGetMinX(frame)); \ +XCTAssertEqual(CGRectGetMinY(expected), CGRectGetMinY(frame)); \ +XCTAssertEqual(CGRectGetWidth(expected), CGRectGetWidth(frame)); \ +XCTAssertEqual(CGRectGetHeight(expected), CGRectGetHeight(frame)); \ +} while(0) + +#define IGAssertContains(collection, object) do {\ +id haystack = collection; id needle = object; \ +XCTAssertTrue([haystack containsObject:needle], @"%@ does not contain %@", haystack, needle); \ +} while(0) diff --git a/Tests/Objects/IGListTestAdapterDataSource.h b/Tests/Objects/IGListTestAdapterDataSource.h index e873e975..f1b237eb 100644 --- a/Tests/Objects/IGListTestAdapterDataSource.h +++ b/Tests/Objects/IGListTestAdapterDataSource.h @@ -11,7 +11,9 @@ #import -@interface IGListTestAdapterDataSource : NSObject +#import "IGListTestCase.h" + +@interface IGListTestAdapterDataSource : NSObject // array of numbers which is then passed to -[IGListTestSection setItems:] @property (nonatomic, strong) NSArray *objects; diff --git a/Tests/Objects/IGTestDelegateDataSource.h b/Tests/Objects/IGTestDelegateDataSource.h index 02a4b425..5ff579ac 100644 --- a/Tests/Objects/IGTestDelegateDataSource.h +++ b/Tests/Objects/IGTestDelegateDataSource.h @@ -11,10 +11,12 @@ #import +#import "IGListTestCase.h" + @class IGTestObject; @class IGTestDelegateController; -@interface IGTestDelegateDataSource : NSObject +@interface IGTestDelegateDataSource : NSObject @property (nonatomic, strong) NSArray *objects; diff --git a/Tests/Objects/IGTestDiffingDataSource.h b/Tests/Objects/IGTestDiffingDataSource.h index 48a50777..ee5b74b1 100644 --- a/Tests/Objects/IGTestDiffingDataSource.h +++ b/Tests/Objects/IGTestDiffingDataSource.h @@ -11,9 +11,11 @@ #import +#import "IGListTestCase.h" + @class IGTestDiffingObject; -@interface IGTestDiffingDataSource : NSObject +@interface IGTestDiffingDataSource : NSObject @property (nonatomic, strong) NSArray *objects; diff --git a/Tests/Objects/IGTestObject.h b/Tests/Objects/IGTestObject.h index 5b26808a..2134b19a 100644 --- a/Tests/Objects/IGTestObject.h +++ b/Tests/Objects/IGTestObject.h @@ -11,6 +11,8 @@ #import +#define genTestObject(k, v) [[IGTestObject alloc] initWithKey:k value:v] + @interface IGTestObject : NSObject - (instancetype)initWithKey:(id )key value:(id)value; diff --git a/Tests/Objects/IGTestSingleItemDataSource.h b/Tests/Objects/IGTestSingleItemDataSource.h index 84e7cf9c..ec6d0afc 100644 --- a/Tests/Objects/IGTestSingleItemDataSource.h +++ b/Tests/Objects/IGTestSingleItemDataSource.h @@ -12,8 +12,9 @@ #import #import "IGTestObject.h" +#import "IGListTestCase.h" -@interface IGTestSingleItemDataSource : NSObject +@interface IGTestSingleItemDataSource : NSObject @property (nonatomic, strong) NSArray *objects; diff --git a/Tests/Objects/IGTestSingleNibItemDataSource.h b/Tests/Objects/IGTestSingleNibItemDataSource.h index 0c81df28..27a243e7 100644 --- a/Tests/Objects/IGTestSingleNibItemDataSource.h +++ b/Tests/Objects/IGTestSingleNibItemDataSource.h @@ -12,8 +12,9 @@ #import #import "IGTestObject.h" +#import "IGListTestCase.h" -@interface IGTestSingleNibItemDataSource : NSObject +@interface IGTestSingleNibItemDataSource : NSObject @property (nonatomic, strong) NSArray *objects; diff --git a/Tests/Objects/IGTestStackedDataSource.h b/Tests/Objects/IGTestStackedDataSource.h index b3a4ccb6..d8569e5f 100644 --- a/Tests/Objects/IGTestStackedDataSource.h +++ b/Tests/Objects/IGTestStackedDataSource.h @@ -12,8 +12,9 @@ #import #import "IGTestObject.h" +#import "IGListTestCase.h" -@interface IGTestStackedDataSource : NSObject +@interface IGTestStackedDataSource : NSObject @property (nonatomic, strong) NSArray *objects;