From 69e69873cc38727aade9942798741d5693118b63 Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Sun, 11 Jun 2017 11:41:30 -0500 Subject: [PATCH] Migrate to Latest OCMock, Demonstrate Improved Unit Testing (#347) * Update OCMock 2.2 -> 3.4 * Clean up and port ASMultiplexImageNodeTests * Clean up * Be stricter about order * Log change * Update the licenses #important * Update the license headers more --- AsyncDisplayKit.xcodeproj/project.pbxproj | 6 + CHANGELOG.md | 1 + Podfile | 2 +- Tests/ASCALayerTests.m | 14 +- Tests/ASMultiplexImageNodeTests.m | 247 +++++++++------------- Tests/ASUICollectionViewTests.m | 28 ++- Tests/ASViewControllerTests.m | 17 +- Tests/NSInvocation+ASTestHelpers.h | 36 ++++ Tests/NSInvocation+ASTestHelpers.m | 36 ++++ 9 files changed, 224 insertions(+), 163 deletions(-) create mode 100644 Tests/NSInvocation+ASTestHelpers.h create mode 100644 Tests/NSInvocation+ASTestHelpers.m diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index 85b121ace..79d34a727 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -369,6 +369,7 @@ CCA282CD1E9EB73E0037E8B7 /* ASTipNode.m in Sources */ = {isa = PBXBuildFile; fileRef = CCA282CB1E9EB73E0037E8B7 /* ASTipNode.m */; }; CCA282D01E9EBF6C0037E8B7 /* ASTipsWindow.h in Headers */ = {isa = PBXBuildFile; fileRef = CCA282CE1E9EBF6C0037E8B7 /* ASTipsWindow.h */; }; CCA282D11E9EBF6C0037E8B7 /* ASTipsWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = CCA282CF1E9EBF6C0037E8B7 /* ASTipsWindow.m */; }; + CCA5F62C1EEC9E9B0060C137 /* NSInvocation+ASTestHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = CCA5F62B1EEC9E9B0060C137 /* NSInvocation+ASTestHelpers.m */; }; CCB2F34D1D63CCC6004E6DE9 /* ASDisplayNodeSnapshotTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CCB2F34C1D63CCC6004E6DE9 /* ASDisplayNodeSnapshotTests.m */; }; CCBBBF5D1EB161760069AA91 /* ASRangeManagingNode.h in Headers */ = {isa = PBXBuildFile; fileRef = CCBBBF5C1EB161760069AA91 /* ASRangeManagingNode.h */; settings = {ATTRIBUTES = (Public, ); }; }; CCCCCCD51EC3EF060087FE10 /* ASTextDebugOption.h in Headers */ = {isa = PBXBuildFile; fileRef = CCCCCCC31EC3EF060087FE10 /* ASTextDebugOption.h */; }; @@ -825,6 +826,8 @@ CCA282CB1E9EB73E0037E8B7 /* ASTipNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASTipNode.m; sourceTree = ""; }; CCA282CE1E9EBF6C0037E8B7 /* ASTipsWindow.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASTipsWindow.h; sourceTree = ""; }; CCA282CF1E9EBF6C0037E8B7 /* ASTipsWindow.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASTipsWindow.m; sourceTree = ""; }; + CCA5F62A1EEC9E9B0060C137 /* NSInvocation+ASTestHelpers.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSInvocation+ASTestHelpers.h"; sourceTree = ""; }; + CCA5F62B1EEC9E9B0060C137 /* NSInvocation+ASTestHelpers.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSInvocation+ASTestHelpers.m"; sourceTree = ""; }; CCB2F34C1D63CCC6004E6DE9 /* ASDisplayNodeSnapshotTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASDisplayNodeSnapshotTests.m; sourceTree = ""; }; CCBBBF5C1EB161760069AA91 /* ASRangeManagingNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASRangeManagingNode.h; sourceTree = ""; }; CCBD05DE1E4147B000D18509 /* ASIGListAdapterBasedDataSource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASIGListAdapterBasedDataSource.m; sourceTree = ""; }; @@ -1104,6 +1107,8 @@ 058D09C5195D04C000B7D73C /* Tests */ = { isa = PBXGroup; children = ( + CCA5F62A1EEC9E9B0060C137 /* NSInvocation+ASTestHelpers.h */, + CCA5F62B1EEC9E9B0060C137 /* NSInvocation+ASTestHelpers.m */, CC034A0F1E60C9BF00626263 /* ASRectTableTests.m */, CC11F9791DB181180024D77B /* ASNetworkImageNodeTests.m */, CC051F1E1D7A286A006434CB /* ASCALayerTests.m */, @@ -2027,6 +2032,7 @@ ACF6ED5C1B178DC700DA7C62 /* ASCenterLayoutSpecSnapshotTests.mm in Sources */, 9F06E5CD1B4CAF4200F015D8 /* ASCollectionViewTests.mm in Sources */, 2911485C1A77147A005D0878 /* ASControlNodeTests.m in Sources */, + CCA5F62C1EEC9E9B0060C137 /* NSInvocation+ASTestHelpers.m in Sources */, CC3B208E1C3F7D0A00798563 /* ASWeakSetTests.m in Sources */, CC034A101E60C9BF00626263 /* ASRectTableTests.m in Sources */, F711994E1D20C21100568860 /* ASDisplayNodeExtrasTests.m in Sources */, diff --git a/CHANGELOG.md b/CHANGELOG.md index 8bef9a852..84f110325 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Fixed an issue where GIFs with placeholders never had their placeholders uncover the GIF. [Garrett Moon](https://github.com/garrettmoon) - [Yoga] Implement ASYogaLayoutSpec, a simplified integration strategy for Yoga-powered layout calculation. [Scott Goodson](https://github.com/appleguy) - Fixed an issue where calls to setNeedsDisplay and setNeedsLayout would stop working on loaded nodes. [Garrett Moon](https://github.com/garrettmoon) +- Migrated unit tests to OCMock 3.4 (from 2.2) and improved the multiplex image node tests. [Adlai Holler](https://github.com/Adlai-Holler) ##2.3.3 - [ASTextKitFontSizeAdjuster] Replace use of NSAttributedString's boundingRectWithSize:options:context: with NSLayoutManager's boundingRectForGlyphRange:inTextContainer: [Ricky Cancro](https://github.com/rcancro) diff --git a/Podfile b/Podfile index 4e3fc26b2..3f317b3a0 100644 --- a/Podfile +++ b/Podfile @@ -3,7 +3,7 @@ source 'https://github.com/CocoaPods/Specs.git' platform :ios, '8.0' target :'AsyncDisplayKitTests' do - pod 'OCMock', '~> 2.2' + pod 'OCMock', '~> 3.4' pod 'FBSnapshotTestCase/Core', '~> 2.1' pod 'JGMethodSwizzler', :git => 'https://github.com/JonasGessner/JGMethodSwizzler', :branch => 'master' diff --git a/Tests/ASCALayerTests.m b/Tests/ASCALayerTests.m index 1fa4975cd..1ff20d396 100644 --- a/Tests/ASCALayerTests.m +++ b/Tests/ASCALayerTests.m @@ -2,14 +2,22 @@ // ASCALayerTests.m // Texture // -// Created by Adlai Holler on 9/2/16. -// Copyright © 2016 Facebook. All rights reserved. +// Copyright (c) 2014-present, Facebook, Inc. All rights reserved. +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the /ASDK-Licenses directory of this source tree. An additional +// grant of patent rights can be found in the PATENTS file in the same directory. +// +// Modifications to this file made after 4/13/2017 are: Copyright (c) 2017-present, +// Pinterest, Inc. Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 // #import #import -#import /** * Tests that confirm what we know about Core Animation behavior. diff --git a/Tests/ASMultiplexImageNodeTests.m b/Tests/ASMultiplexImageNodeTests.m index f751f7244..9eb41874b 100644 --- a/Tests/ASMultiplexImageNodeTests.m +++ b/Tests/ASMultiplexImageNodeTests.m @@ -16,30 +16,29 @@ // #import -#import +#import "NSInvocation+ASTestHelpers.h" #import #import #import -#import - #import @interface ASMultiplexImageNodeTests : XCTestCase { @private - id _mockCache; - id _mockDownloader; + id mockCache; + id mockDownloader; + id mockDataSource; + id mockDelegate; + ASMultiplexImageNode *imageNode; } @end - @implementation ASMultiplexImageNodeTests -#pragma mark - -#pragma mark Helpers. +#pragma mark - Helpers. - (NSURL *)_testImageURL { @@ -53,8 +52,7 @@ - (UIImage *)_testImage return [UIImage imageWithContentsOfFile:[self _testImageURL].path]; } -#pragma mark - -#pragma mark Unit tests. +#pragma mark - Unit tests. // TODO: add tests for delegate display notifications @@ -62,83 +60,73 @@ - (void)setUp { [super setUp]; - _mockCache = [OCMockObject mockForProtocol:@protocol(ASImageCacheProtocol)]; - _mockDownloader = [OCMockObject mockForProtocol:@protocol(ASImageDownloaderProtocol)]; + mockCache = OCMStrictProtocolMock(@protocol(ASImageCacheProtocol)); + [mockCache setExpectationOrderMatters:YES]; + mockDownloader = OCMStrictProtocolMock(@protocol(ASImageDownloaderProtocol)); + [mockDownloader setExpectationOrderMatters:YES]; + imageNode = [[ASMultiplexImageNode alloc] initWithCache:mockCache downloader:mockDownloader]; + + mockDataSource = OCMStrictProtocolMock(@protocol(ASMultiplexImageNodeDataSource)); + [mockDataSource setExpectationOrderMatters:YES]; + imageNode.dataSource = mockDataSource; + + mockDelegate = OCMProtocolMock(@protocol(ASMultiplexImageNodeDelegate)); + [mockDelegate setExpectationOrderMatters:YES]; + imageNode.delegate = mockDelegate; } - (void)tearDown { - _mockCache = nil; - _mockDownloader = nil; + OCMVerifyAll(mockDelegate); + OCMVerifyAll(mockDataSource); + OCMVerifyAll(mockDownloader); + OCMVerifyAll(mockCache); [super tearDown]; } - (void)testDataSourceImageMethod { - ASMultiplexImageNode *imageNode = [[ASMultiplexImageNode alloc] initWithCache:_mockCache downloader:_mockDownloader]; - - // Mock the data source. - // Note that we're not using a niceMock because we want to assert if the URL data-source method gets hit, as the image - // method should be hit first and exclusively if it successfully returns an image. - id mockDataSource = [OCMockObject mockForProtocol:@protocol(ASMultiplexImageNodeDataSource)]; - imageNode.dataSource = mockDataSource; - NSNumber *imageIdentifier = @1; - // Expect the image method to be hit, and have it return our test image. - UIImage *testImage = [self _testImage]; - [[[mockDataSource expect] andReturn:testImage] multiplexImageNode:imageNode imageForImageIdentifier:imageIdentifier]; + OCMExpect([mockDataSource multiplexImageNode:imageNode imageForImageIdentifier:imageIdentifier]) + .andReturn([self _testImage]); imageNode.imageIdentifiers = @[imageIdentifier]; [imageNode reloadImageIdentifierSources]; - [mockDataSource verify]; - // Also expect it to be loaded immediately. XCTAssertEqualObjects(imageNode.loadedImageIdentifier, imageIdentifier, @"imageIdentifier was not loaded"); // And for the image to be equivalent to the image we provided. XCTAssertEqualObjects(UIImagePNGRepresentation(imageNode.image), - UIImagePNGRepresentation(testImage), + UIImagePNGRepresentation([self _testImage]), @"Loaded image isn't the one we provided"); - - imageNode.delegate = nil; - imageNode.dataSource = nil; } - (void)testDataSourceURLMethod { - ASMultiplexImageNode *imageNode = [[ASMultiplexImageNode alloc] initWithCache:_mockCache downloader:_mockDownloader]; - NSNumber *imageIdentifier = @1; - // Mock the data source such that we... - id mockDataSource = [OCMockObject niceMockForProtocol:@protocol(ASMultiplexImageNodeDataSource)]; - imageNode.dataSource = mockDataSource; - // (a) first expect to be hit for the image directly, and fail to return it. - [mockDataSource setExpectationOrderMatters:YES]; - [[[mockDataSource expect] andReturn:nil] multiplexImageNode:imageNode imageForImageIdentifier:imageIdentifier]; - // (b) and then expect to be hit for the URL, which we'll return. - [[[mockDataSource expect] andReturn:[self _testImageURL]] multiplexImageNode:imageNode URLForImageIdentifier:imageIdentifier]; + // First expect to be hit for the image directly, and fail to return it. + OCMExpect([mockDataSource multiplexImageNode:imageNode imageForImageIdentifier:imageIdentifier]) + .andReturn(nil); + // BUG: -imageForImageIdentifier is called twice in this case (where we return nil). + OCMExpect([mockDataSource multiplexImageNode:imageNode imageForImageIdentifier:imageIdentifier]) + .andReturn(nil); + // Then expect to be hit for the URL, which we'll return. + OCMExpect([mockDataSource multiplexImageNode:imageNode URLForImageIdentifier:imageIdentifier]) + .andReturn([self _testImageURL]); // Mock the cache to do a cache-hit for the test image URL. - [[[_mockCache stub] andDo:^(NSInvocation *inv) { - // Params are URL, callbackQueue, completion - NSArray *URL = [inv getArgumentAtIndexAsObject:2]; - - ASImageCacherCompletion completionBlock = [inv getArgumentAtIndexAsObject:4]; - - // Call the completion block with our test image and URL. - NSURL *testImageURL = [self _testImageURL]; - XCTAssertEqualObjects(URL, testImageURL, @"Fetching URL other than test image"); + OCMExpect([mockCache cachedImageWithURL:[self _testImageURL] callbackQueue:OCMOCK_ANY completion:[OCMArg isNotNil]]) + .andDo(^(NSInvocation *inv) { + ASImageCacherCompletion completionBlock = [inv as_argumentAtIndexAsObject:4]; completionBlock([self _testImage]); - }] cachedImageWithURL:[OCMArg any] callbackQueue:[OCMArg any] completion:[OCMArg any]]; + }); imageNode.imageIdentifiers = @[imageIdentifier]; // Kick off loading. [imageNode reloadImageIdentifierSources]; - // Verify the data source. - [mockDataSource verify]; // Also expect it to be loaded immediately. XCTAssertEqualObjects(imageNode.loadedImageIdentifier, imageIdentifier, @"imageIdentifier was not loaded"); // And for the image to be equivalent to the image we provided. @@ -150,152 +138,123 @@ - (void)testDataSourceURLMethod - (void)testAddLowerQualityImageIdentifier { // Adding a lower quality image identifier should not cause any loading. - ASMultiplexImageNode *imageNode = [[ASMultiplexImageNode alloc] initWithCache:_mockCache downloader:_mockDownloader]; - - NSNumber *highResIdentifier = @2; - - // Mock the data source such that we: (a) return the test image, and log whether we get hit for the lower-quality image. - id mockDataSource = [OCMockObject mockForProtocol:@protocol(ASMultiplexImageNodeDataSource)]; - imageNode.dataSource = mockDataSource; - __block int dataSourceHits = 0; - [[[mockDataSource stub] andDo:^(NSInvocation *inv) { - dataSourceHits++; - - // Return the test image. - [inv setReturnValue:(void *)[self _testImage]]; - }] multiplexImageNode:[OCMArg any] imageForImageIdentifier:[OCMArg any]]; + NSNumber *highResIdentifier = @2, *lowResIdentifier = @1; + OCMExpect([mockDataSource multiplexImageNode:imageNode imageForImageIdentifier:highResIdentifier]) + .andReturn([self _testImage]); imageNode.imageIdentifiers = @[highResIdentifier]; [imageNode reloadImageIdentifierSources]; // At this point, we should have the high-res identifier loaded and the DS should have been hit once. XCTAssertEqualObjects(imageNode.loadedImageIdentifier, highResIdentifier, @"High res identifier should be loaded."); - XCTAssertTrue(dataSourceHits == 1, @"Unexpected DS hit count"); - // Add the low res identifier. - NSNumber *lowResIdentifier = @1; + // BUG: We should not get another -imageForImageIdentifier:highResIdentifier. + OCMExpect([mockDataSource multiplexImageNode:imageNode imageForImageIdentifier:highResIdentifier]) + .andReturn([self _testImage]); + imageNode.imageIdentifiers = @[highResIdentifier, lowResIdentifier]; [imageNode reloadImageIdentifierSources]; - // At this point the high-res should still be loaded, and the data source should have been hit again + // At this point the high-res should still be loaded, and the data source should not have been hit again (see BUG above). XCTAssertEqualObjects(imageNode.loadedImageIdentifier, highResIdentifier, @"High res identifier should be loaded."); - XCTAssertTrue(dataSourceHits == 2, @"Unexpected DS hit count"); } - (void)testAddHigherQualityImageIdentifier { - // Adding a higher quality image identifier should cause loading. - ASMultiplexImageNode *imageNode = [[ASMultiplexImageNode alloc] initWithCache:_mockCache downloader:_mockDownloader]; - - NSNumber *lowResIdentifier = @1; + NSNumber *lowResIdentifier = @1, *highResIdentifier = @2; - // Mock the data source such that we: (a) return the test image, and log how many times the DS gets hit. - id mockDataSource = [OCMockObject mockForProtocol:@protocol(ASMultiplexImageNodeDataSource)]; - imageNode.dataSource = mockDataSource; - __block int dataSourceHits = 0; - [[[mockDataSource stub] andDo:^(NSInvocation *inv) { - dataSourceHits++; - - // Return the test image. - [inv setReturnValue:(void *)[self _testImage]]; - }] multiplexImageNode:[OCMArg any] imageForImageIdentifier:[OCMArg any]]; + OCMExpect([mockDataSource multiplexImageNode:imageNode imageForImageIdentifier:lowResIdentifier]) + .andReturn([self _testImage]); imageNode.imageIdentifiers = @[lowResIdentifier]; [imageNode reloadImageIdentifierSources]; // At this point, we should have the low-res identifier loaded and the DS should have been hit once. XCTAssertEqualObjects(imageNode.loadedImageIdentifier, lowResIdentifier, @"Low res identifier should be loaded."); - XCTAssertTrue(dataSourceHits == 1, @"Unexpected DS hit count"); - // Add the low res identifier. - NSNumber *highResIdentifier = @2; + OCMExpect([mockDataSource multiplexImageNode:imageNode imageForImageIdentifier:highResIdentifier]) + .andReturn([self _testImage]); + imageNode.imageIdentifiers = @[highResIdentifier, lowResIdentifier]; [imageNode reloadImageIdentifierSources]; // At this point the high-res should be loaded, and the data source should been hit twice. XCTAssertEqualObjects(imageNode.loadedImageIdentifier, highResIdentifier, @"High res identifier should be loaded."); - XCTAssertTrue(dataSourceHits == 2, @"Unexpected DS hit count"); } -- (void)testProgressiveDownloading +- (void)testIntermediateImageDownloading { - ASMultiplexImageNode *imageNode = [[ASMultiplexImageNode alloc] initWithCache:_mockCache downloader:_mockDownloader]; imageNode.downloadsIntermediateImages = YES; + // Let them call URLForImageIdentifier all they want. + OCMStub([mockDataSource multiplexImageNode:imageNode URLForImageIdentifier:[OCMArg isNotNil]]); + // Set up a few identifiers to load. NSInteger identifierCount = 5; NSMutableArray *imageIdentifiers = [NSMutableArray array]; - for (NSInteger identifierIndex = 0; identifierIndex < identifierCount; identifierIndex++) - [imageIdentifiers insertObject:@(identifierIndex + 1) atIndex:0]; - - // Mock the data source to only make the images available progressively. - // This is necessary because ASMultiplexImageNode will try to grab the best image immediately, regardless of - // `downloadsIntermediateImages`. - id mockDataSource = [OCMockObject niceMockForProtocol:@protocol(ASMultiplexImageNodeDataSource)]; - imageNode.dataSource = mockDataSource; - __block NSUInteger loadedImageCount = 0; - [[[mockDataSource stub] andDo:^(NSInvocation *inv) { - id requestedIdentifier = [inv getArgumentAtIndexAsObject:3]; - - NSInteger requestedIdentifierValue = [requestedIdentifier intValue]; - - // If no images are loaded, bail on trying to load anything but the worst image. - if (!imageNode.loadedImageIdentifier && requestedIdentifierValue != [[imageIdentifiers lastObject] integerValue]) - return; - - // Bail if it's trying to load an identifier that's more than one step than what's loaded. - NSInteger nextImageIdentifier = [(NSNumber *)imageNode.loadedImageIdentifier integerValue] + 1; - if (requestedIdentifierValue != nextImageIdentifier) - return; - - // Return the test image. - loadedImageCount++; - [inv setReturnValue:(void *)[self _testImage]]; - }] multiplexImageNode:[OCMArg any] imageForImageIdentifier:[OCMArg any]]; + for (NSInteger identifier = identifierCount; identifier > 0; identifier--) { + [imageIdentifiers addObject:@(identifier)]; + } + + // Create the array of IDs in the order we expect them to get -imageForImageIdentifier: + // BUG: The second to last ID (the last one that returns nil) will get -imageForImageIdentifier: called + // again after the last ID (the one that returns non-nil). + id secondToLastID = imageIdentifiers[identifierCount - 2]; + NSArray *imageIdentifiersThatWillBeCalled = [imageIdentifiers arrayByAddingObject:secondToLastID]; + + for (id imageID in imageIdentifiersThatWillBeCalled) { + // Return nil for everything except the worst ID. + OCMExpect([mockDataSource multiplexImageNode:imageNode imageForImageIdentifier:imageID]) + .andDo(^(NSInvocation *inv){ + id imageID = [inv as_argumentAtIndexAsObject:3]; + if ([imageID isEqual:imageIdentifiers.lastObject]) { + [inv as_setReturnValueWithObject:[self _testImage]]; + } else { + [inv as_setReturnValueWithObject:nil]; + } + }); + } imageNode.imageIdentifiers = imageIdentifiers; [imageNode reloadImageIdentifierSources]; - - XCTAssertTrue(loadedImageCount == identifierCount, @"Expected to load the same number of identifiers we supplied"); } - (void)testUncachedDownload { // Mock a cache miss. - id mockCache = [OCMockObject mockForProtocol:@protocol(ASImageCacheProtocol)]; - [[[mockCache stub] andDo:^(NSInvocation *inv) { - ASImageCacherCompletion completion = [inv getArgumentAtIndexAsObject:4]; + OCMExpect([mockCache cachedImageWithURL:[self _testImageURL] callbackQueue:OCMOCK_ANY completion:[OCMArg isNotNil]]) + .andDo(^(NSInvocation *inv){ + ASImageCacherCompletion completion = [inv as_argumentAtIndexAsObject:4]; completion(nil); - }] cachedImageWithURL:[OCMArg any] callbackQueue:[OCMArg any] completion:[OCMArg any]]; + }); // Mock a 50%-progress URL download. - id mockDownloader = [OCMockObject mockForProtocol:@protocol(ASImageDownloaderProtocol)]; const CGFloat mockedProgress = 0.5; - [[[mockDownloader stub] andDo:^(NSInvocation *inv) { + OCMExpect([mockDownloader downloadImageWithURL:[self _testImageURL] callbackQueue:OCMOCK_ANY downloadProgress:[OCMArg isNotNil] completion:[OCMArg isNotNil]]) + .andDo(^(NSInvocation *inv){ // Simulate progress. - ASImageDownloaderProgress progressBlock = [inv getArgumentAtIndexAsObject:4]; + ASImageDownloaderProgress progressBlock = [inv as_argumentAtIndexAsObject:4]; progressBlock(mockedProgress); // Simulate completion. - ASImageDownloaderCompletion completionBlock = [inv getArgumentAtIndexAsObject:5]; + ASImageDownloaderCompletion completionBlock = [inv as_argumentAtIndexAsObject:5]; completionBlock([self _testImage], nil, nil); - }] downloadImageWithURL:[OCMArg any] callbackQueue:[OCMArg any] downloadProgress:[OCMArg any] completion:[OCMArg any]]; + }); - ASMultiplexImageNode *imageNode = [[ASMultiplexImageNode alloc] initWithCache:mockCache downloader:mockDownloader]; NSNumber *imageIdentifier = @1; - // Mock the data source to return our test URL. - id mockDataSource = [OCMockObject niceMockForProtocol:@protocol(ASMultiplexImageNodeDataSource)]; - [[[mockDataSource stub] andReturn:[self _testImageURL]] multiplexImageNode:imageNode URLForImageIdentifier:imageIdentifier]; - imageNode.dataSource = mockDataSource; + // Mock the data source to return nil image, and our test URL. + OCMExpect([mockDataSource multiplexImageNode:imageNode imageForImageIdentifier:imageIdentifier]); + // BUG: Multiplex image node will call imageForImageIdentifier twice if we return nil. + OCMExpect([mockDataSource multiplexImageNode:imageNode imageForImageIdentifier:imageIdentifier]); + OCMExpect([mockDataSource multiplexImageNode:imageNode URLForImageIdentifier:imageIdentifier]) + .andReturn([self _testImageURL]); // Mock the delegate to expect start, 50% progress, and completion invocations. - id mockDelegate = [OCMockObject mockForProtocol:@protocol(ASMultiplexImageNodeDelegate)]; - [[mockDelegate expect] multiplexImageNode:imageNode didStartDownloadOfImageWithIdentifier:imageIdentifier]; - [[mockDelegate expect] multiplexImageNode:imageNode didUpdateDownloadProgress:mockedProgress forImageWithIdentifier:imageIdentifier]; - [[mockDelegate expect] multiplexImageNode:imageNode didFinishDownloadingImageWithIdentifier:imageIdentifier error:nil]; - [[mockDelegate expect] multiplexImageNode:imageNode didUpdateImage:[OCMArg any] withIdentifier:imageIdentifier fromImage:nil withIdentifier:nil]; - imageNode.delegate = mockDelegate; + OCMExpect([mockDelegate multiplexImageNode:imageNode didStartDownloadOfImageWithIdentifier:imageIdentifier]); + OCMExpect([mockDelegate multiplexImageNode:imageNode didUpdateDownloadProgress:mockedProgress forImageWithIdentifier:imageIdentifier]); + OCMExpect([mockDelegate multiplexImageNode:imageNode didUpdateImage:[OCMArg isNotNil] withIdentifier:imageIdentifier fromImage:[OCMArg isNil] withIdentifier:[OCMArg isNil]]); + OCMExpect([mockDelegate multiplexImageNode:imageNode didFinishDownloadingImageWithIdentifier:imageIdentifier error:[OCMArg isNil]]); imageNode.imageIdentifiers = @[imageIdentifier]; // Kick off loading. @@ -304,15 +263,11 @@ - (void)testUncachedDownload // Wait until the image is loaded. [self expectationForPredicate:[NSPredicate predicateWithFormat:@"loadedImageIdentifier = %@", imageIdentifier] evaluatedWithObject:imageNode handler:nil]; [self waitForExpectationsWithTimeout:30 handler:nil]; - - // Verify the delegation. - [mockDelegate verify]; } - (void)testThatSettingAnImageExternallyWillThrow { - ASMultiplexImageNode *multiplexImageNode = [[ASMultiplexImageNode alloc] init]; - XCTAssertThrows(multiplexImageNode.image = [UIImage imageNamed:@""]); + XCTAssertThrows(imageNode.image = [UIImage imageNamed:@""]); } @end diff --git a/Tests/ASUICollectionViewTests.m b/Tests/ASUICollectionViewTests.m index d106bae2b..ec7a51fa2 100644 --- a/Tests/ASUICollectionViewTests.m +++ b/Tests/ASUICollectionViewTests.m @@ -2,13 +2,22 @@ // ASUICollectionViewTests.m // Texture // -// Created by Adlai Holler on 8/18/16. -// Copyright © 2016 Facebook. All rights reserved. +// Copyright (c) 2014-present, Facebook, Inc. All rights reserved. +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the /ASDK-Licenses directory of this source tree. An additional +// grant of patent rights can be found in the PATENTS file in the same directory. +// +// Modifications to this file made after 4/13/2017 are: Copyright (c) 2017-present, +// Pinterest, Inc. Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 // #import #import -#import +#import "NSInvocation+ASTestHelpers.h" @interface ASUICollectionViewTests : XCTestCase @@ -86,7 +95,7 @@ - (void)_testSupplementaryNodeAtIndexPath:(NSIndexPath *)indexPath sectionCount: id dataSource = [OCMockObject niceMockForProtocol:@protocol(UICollectionViewDataSource)]; __block id view = nil; [[[dataSource expect] andDo:^(NSInvocation *invocation) { - NSIndexPath *indexPath = [invocation getArgumentAtIndexAsObject:4]; + NSIndexPath *indexPath = [invocation as_argumentAtIndexAsObject:4]; view = [cv dequeueReusableSupplementaryViewOfKind:@"SuppKind" withReuseIdentifier:@"ReuseID" forIndexPath:indexPath]; [invocation setReturnValue:&view]; }] collectionView:cv viewForSupplementaryElementOfKind:@"SuppKind" atIndexPath:indexPath]; @@ -95,12 +104,13 @@ - (void)_testSupplementaryNodeAtIndexPath:(NSIndexPath *)indexPath sectionCount: cv.dataSource = dataSource; if (shouldFail) { XCTAssertThrowsSpecificNamed([cv layoutIfNeeded], NSException, NSInternalInconsistencyException); - } else { - [cv layoutIfNeeded]; - XCTAssertEqualObjects(attr, [cv layoutAttributesForSupplementaryElementOfKind:@"SuppKind" atIndexPath:indexPath]); - XCTAssertEqual(view, [cv supplementaryViewForElementKind:@"SuppKind" atIndexPath:indexPath]); + // Early return because behavior after exception is thrown is undefined. + return; } + [cv layoutIfNeeded]; + XCTAssertEqualObjects(attr, [cv layoutAttributesForSupplementaryElementOfKind:@"SuppKind" atIndexPath:indexPath]); + XCTAssertEqual(view, [cv supplementaryViewForElementKind:@"SuppKind" atIndexPath:indexPath]); [dataSource verify]; [layoutMock verify]; } @@ -113,7 +123,7 @@ - (void)testThatIssuingAnUpdateBeforeInitialReloadIsUnacceptable // Setup empty data source – 0 sections, 0 items [[[dataSource stub] andDo:^(NSInvocation *invocation) { - NSIndexPath *indexPath = [invocation getArgumentAtIndexAsObject:3]; + NSIndexPath *indexPath = [invocation as_argumentAtIndexAsObject:3]; __autoreleasing UICollectionViewCell *view = [cv dequeueReusableCellWithReuseIdentifier:@"CellID" forIndexPath:indexPath]; [invocation setReturnValue:&view]; }] collectionView:cv cellForItemAtIndexPath:OCMOCK_ANY]; diff --git a/Tests/ASViewControllerTests.m b/Tests/ASViewControllerTests.m index 700ba6e25..3fa91d87f 100644 --- a/Tests/ASViewControllerTests.m +++ b/Tests/ASViewControllerTests.m @@ -2,14 +2,23 @@ // ASViewControllerTests.m // Texture // -// Created by Adlai Holler on 8/25/16. -// Copyright © 2016 Facebook. All rights reserved. +// Copyright (c) 2014-present, Facebook, Inc. All rights reserved. +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the /ASDK-Licenses directory of this source tree. An additional +// grant of patent rights can be found in the PATENTS file in the same directory. +// +// Modifications to this file made after 4/13/2017 are: Copyright (c) 2017-present, +// Pinterest, Inc. Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 // #import #import #import -#import +#import "NSInvocation+ASTestHelpers.h" @interface ASViewControllerTests : XCTestCase @@ -51,7 +60,7 @@ - (void)testThatViewControllerFrameIsRightAfterCustomTransitionWithNonextendedEd [[[animator expect] andReturnValue:@0.3] transitionDuration:[OCMArg any]]; XCTestExpectation *e = [self expectationWithDescription:@"Transition completed"]; [[[animator expect] andDo:^(NSInvocation *invocation) { - id ctx = [invocation getArgumentAtIndexAsObject:2]; + id ctx = [invocation as_argumentAtIndexAsObject:2]; UIView *container = [ctx containerView]; [container addSubview:vc.view]; vc.view.alpha = 0; diff --git a/Tests/NSInvocation+ASTestHelpers.h b/Tests/NSInvocation+ASTestHelpers.h new file mode 100644 index 000000000..da64096e3 --- /dev/null +++ b/Tests/NSInvocation+ASTestHelpers.h @@ -0,0 +1,36 @@ +// +// NSInvocation+ASTestHelpers.h +// Texture +// +// Copyright (c) 2017-present, Pinterest, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface NSInvocation (ASTestHelpers) + +/** + * Formats the argument at the given index as an object and returns it. + * + * Currently only supports arguments that are themselves objects, but handles + * getting the argument into ARC safely. + */ +- (nullable id)as_argumentAtIndexAsObject:(NSInteger)index; + +/** + * Sets the return value, simulating ARC behavior. + * + * Currently only supports invocations whose return values are already object types. + */ +- (void)as_setReturnValueWithObject:(nullable id)object; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Tests/NSInvocation+ASTestHelpers.m b/Tests/NSInvocation+ASTestHelpers.m new file mode 100644 index 000000000..d442a2011 --- /dev/null +++ b/Tests/NSInvocation+ASTestHelpers.m @@ -0,0 +1,36 @@ +// +// NSInvocation+ASTestHelpers.m +// Texture +// +// Copyright (c) 2017-present, Pinterest, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "NSInvocation+ASTestHelpers.h" + +@implementation NSInvocation (ASTestHelpers) + +- (id)as_argumentAtIndexAsObject:(NSInteger)index +{ + void *buf; + [self getArgument:&buf atIndex:index]; + return (__bridge id)buf; +} + +- (void)as_setReturnValueWithObject:(id)object +{ + if (object == nil) { + const void *fixedBuf = NULL; + [self setReturnValue:&fixedBuf]; + } else { + // Retain, then autorelease. + const void *fixedBuf = CFAutorelease((__bridge_retained void *)object); + [self setReturnValue:&fixedBuf]; + } +} + +@end