From 6c3c66f3ab2e448858bd43e6aa0489c86c712526 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Fri, 22 Mar 2024 12:06:30 +0100 Subject: [PATCH 1/8] Created the FeedTest target in the project. --- .../Feed/Test/Tests/ReviewsFeedTests.swift | 36 +++++ Reviews.xcodeproj/project.pbxproj | 136 +++++++++++++++++- 2 files changed, 171 insertions(+), 1 deletion(-) create mode 100644 Frameworks/Feed/Test/Tests/ReviewsFeedTests.swift diff --git a/Frameworks/Feed/Test/Tests/ReviewsFeedTests.swift b/Frameworks/Feed/Test/Tests/ReviewsFeedTests.swift new file mode 100644 index 0000000..a4cfd21 --- /dev/null +++ b/Frameworks/Feed/Test/Tests/ReviewsFeedTests.swift @@ -0,0 +1,36 @@ +// +// ReviewsFeedTests.swift +// ReviewsFeedTests +// +// Created by Javier Cicchelli on 22/03/2024. +// Copyright © 2024 Röck+Cöde. All rights reserved. +// + +import XCTest + +final class ReviewsFeedTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + // Any test you write for XCTest can be annotated as throws and async. + // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. + // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. + } + + func testPerformanceExample() throws { + // This is an example of a performance test case. + measure { + // Put the code you want to measure the time of here. + } + } + +} diff --git a/Reviews.xcodeproj/project.pbxproj b/Reviews.xcodeproj/project.pbxproj index 0a47e76..cfa396e 100644 --- a/Reviews.xcodeproj/project.pbxproj +++ b/Reviews.xcodeproj/project.pbxproj @@ -18,6 +18,8 @@ 02909E792BAB6B0200710E14 /* FilterOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02909E782BAB6B0200710E14 /* FilterOption.swift */; }; 02909E7B2BAB6D2E00710E14 /* Bundle+Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02909E7A2BAB6D2E00710E14 /* Bundle+Constants.swift */; }; 02909E7D2BAB7FFE00710E14 /* Review+DTOs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02909E7C2BAB7FFE00710E14 /* Review+DTOs.swift */; }; + 02B36F7C2BAD9D1A00F1A89D /* ReviewsFeedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02B36F7B2BAD9D1A00F1A89D /* ReviewsFeedTests.swift */; }; + 02B36F7D2BAD9D1A00F1A89D /* ReviewsFeed.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 02DC7F8F2BA51793000EEEBE /* ReviewsFeed.framework */; platformFilter = ios; }; 02C1B1972BAC9BFE001781DE /* FeedListCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02C1B1962BAC9BFE001781DE /* FeedListCoordinator.swift */; }; 02C1B1A92BACA722001781DE /* AppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02C1B1A82BACA722001781DE /* AppCoordinator.swift */; }; 02DA924E2BAAE3FD00C47985 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 02DA924D2BAAE3FD00C47985 /* Localizable.xcstrings */; }; @@ -40,6 +42,13 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ + 02B36F7E2BAD9D1A00F1A89D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 345AD11024C6EDD9004E2EE1 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 02DC7F8E2BA51793000EEEBE; + remoteInfo = Feed; + }; 02DC7FA02BA51793000EEEBE /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 345AD11024C6EDD9004E2EE1 /* Project object */; @@ -75,6 +84,8 @@ 02909E782BAB6B0200710E14 /* FilterOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterOption.swift; sourceTree = ""; }; 02909E7A2BAB6D2E00710E14 /* Bundle+Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+Constants.swift"; sourceTree = ""; }; 02909E7C2BAB7FFE00710E14 /* Review+DTOs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Review+DTOs.swift"; sourceTree = ""; }; + 02B36F792BAD9D1A00F1A89D /* FeedTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FeedTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 02B36F7B2BAD9D1A00F1A89D /* ReviewsFeedTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewsFeedTests.swift; sourceTree = ""; }; 02C1B1962BAC9BFE001781DE /* FeedListCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedListCoordinator.swift; sourceTree = ""; }; 02C1B1A82BACA722001781DE /* AppCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinator.swift; sourceTree = ""; }; 02DA924D2BAAE3FD00C47985 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; @@ -97,6 +108,14 @@ /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 02B36F762BAD9D1A00F1A89D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 02B36F7D2BAD9D1A00F1A89D /* ReviewsFeed.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 02DC7F8C2BA51793000EEEBE /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -224,10 +243,19 @@ 02A6DA302BA5929F00B943E2 /* Test */ = { isa = PBXGroup; children = ( + 02B36F742BAD9C4500F1A89D /* Tests */, ); path = Test; sourceTree = ""; }; + 02B36F742BAD9C4500F1A89D /* Tests */ = { + isa = PBXGroup; + children = ( + 02B36F7B2BAD9D1A00F1A89D /* ReviewsFeedTests.swift */, + ); + path = Tests; + sourceTree = ""; + }; 02C1B1952BAC9BE7001781DE /* Coordinators */ = { isa = PBXGroup; children = ( @@ -364,6 +392,7 @@ children = ( 345AD11824C6EDD9004E2EE1 /* Reviews.app */, 02DC7F8F2BA51793000EEEBE /* ReviewsFeed.framework */, + 02B36F792BAD9D1A00F1A89D /* FeedTests.xctest */, ); name = Products; sourceTree = ""; @@ -391,6 +420,24 @@ /* End PBXHeadersBuildPhase section */ /* Begin PBXNativeTarget section */ + 02B36F782BAD9D1A00F1A89D /* FeedTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 02B36F802BAD9D1A00F1A89D /* Build configuration list for PBXNativeTarget "FeedTests" */; + buildPhases = ( + 02B36F752BAD9D1A00F1A89D /* Sources */, + 02B36F762BAD9D1A00F1A89D /* Frameworks */, + 02B36F772BAD9D1A00F1A89D /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 02B36F7F2BAD9D1A00F1A89D /* PBXTargetDependency */, + ); + name = FeedTests; + productName = ReviewsFeedTests; + productReference = 02B36F792BAD9D1A00F1A89D /* FeedTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; 02DC7F8E2BA51793000EEEBE /* Feed */ = { isa = PBXNativeTarget; buildConfigurationList = 02DC7FA92BA51793000EEEBE /* Build configuration list for PBXNativeTarget "Feed" */; @@ -445,6 +492,9 @@ LastUpgradeCheck = 1530; ORGANIZATIONNAME = "Röck+Cöde"; TargetAttributes = { + 02B36F782BAD9D1A00F1A89D = { + CreatedOnToolsVersion = 15.3; + }; 02DC7F8E2BA51793000EEEBE = { CreatedOnToolsVersion = 15.3; }; @@ -466,13 +516,21 @@ projectDirPath = ""; projectRoot = ""; targets = ( - 02DC7F8E2BA51793000EEEBE /* Feed */, 345AD11724C6EDD9004E2EE1 /* App */, + 02DC7F8E2BA51793000EEEBE /* Feed */, + 02B36F782BAD9D1A00F1A89D /* FeedTests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + 02B36F772BAD9D1A00F1A89D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 02DC7F8D2BA51793000EEEBE /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -493,6 +551,14 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + 02B36F752BAD9D1A00F1A89D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 02B36F7C2BAD9D1A00F1A89D /* ReviewsFeedTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 02DC7F8B2BA51793000EEEBE /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -532,6 +598,12 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ + 02B36F7F2BAD9D1A00F1A89D /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + platformFilter = ios; + target = 02DC7F8E2BA51793000EEEBE /* Feed */; + targetProxy = 02B36F7E2BAD9D1A00F1A89D /* PBXContainerItemProxy */; + }; 02DC7FA12BA51793000EEEBE /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 02DC7F8E2BA51793000EEEBE /* Feed */; @@ -551,6 +623,59 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ + 02B36F812BAD9D1A00F1A89D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 7FMNM89WKG; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.rock-n-code.assignment.ing.framework.feed.test.unit"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Debug; + }; + 02B36F822BAD9D1A00F1A89D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 7FMNM89WKG; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.rock-n-code.assignment.ing.framework.feed.test.unit"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Release; + }; 02DC7FA52BA51793000EEEBE /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -825,6 +950,15 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 02B36F802BAD9D1A00F1A89D /* Build configuration list for PBXNativeTarget "FeedTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 02B36F812BAD9D1A00F1A89D /* Debug */, + 02B36F822BAD9D1A00F1A89D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 02DC7FA92BA51793000EEEBE /* Build configuration list for PBXNativeTarget "Feed" */ = { isa = XCConfigurationList; buildConfigurations = ( -- 2.47.1 From 4ea48bd67194cdf5d6d085e247d386ab37dd8f6f Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Fri, 22 Mar 2024 13:42:45 +0100 Subject: [PATCH 2/8] Implemented the "none" and "sample" constants for the Array+ReviewDTOs extension in the Feed framework. --- .../Extensions/Array+ReviewDTOs.swift | 65 +++++++++++++++++++ .../FeedListViewController.swift | 48 +------------- .../Feed/Test/Tests/ReviewsFeedTests.swift | 36 ---------- Reviews.xcodeproj/project.pbxproj | 22 +++++++ 4 files changed, 88 insertions(+), 83 deletions(-) create mode 100644 Frameworks/Feed/Bundle/Previews/Extensions/Array+ReviewDTOs.swift delete mode 100644 Frameworks/Feed/Test/Tests/ReviewsFeedTests.swift diff --git a/Frameworks/Feed/Bundle/Previews/Extensions/Array+ReviewDTOs.swift b/Frameworks/Feed/Bundle/Previews/Extensions/Array+ReviewDTOs.swift new file mode 100644 index 0000000..7a345f1 --- /dev/null +++ b/Frameworks/Feed/Bundle/Previews/Extensions/Array+ReviewDTOs.swift @@ -0,0 +1,65 @@ +// +// Array+ReviewDTOs.swift +// ReviewsFeed +// +// Created by Javier Cicchelli on 22/03/2024. +// Copyright © 2024 Röck+Cöde. All rights reserved. +// + +import Foundation +import ReviewsFeedKit + +extension Array where Element == ReviewsFeedKit.Review { + + // MARK: Constants + static let none: [ReviewsFeedKit.Review] = [] + + static let sample: [ReviewsFeedKit.Review] = [ + .init( + id: 1, + author: "Some author name #1 here", + title: "Some review title #1 goes here...", + content: "Some long, long, explanatory review comment #1 goes here...", + rating: 3, + version: "v1.0.0", + updated: .init() + ), + .init( + id: 2, + author: "Some author name #2 here", + title: "Some review title #2 goes here...", + content: "Some long, long, explanatory review comment #2 goes here...", + rating: 5, + version: "v1.0.0", + updated: .init() + ), + .init( + id: 3, + author: "Some author name #3 here", + title: "Some review title #3 goes here...", + content: "Some long, long, explanatory review comment #3 goes here...", + rating: 1, + version: "v1.0.0", + updated: .init() + ), + .init( + id: 4, + author: "Some author name #4 here", + title: "Some review title #4 goes here...", + content: "Some long, long, explanatory review comment #4 goes here...", + rating: 4, + version: "v1.0.0", + updated: .init() + ), + .init( + id: 5, + author: "Some author name #5 here", + title: "Some review title #5 goes here...", + content: "Some long, long, explanatory review comment #5 goes here...", + rating: 2, + version: "v1.0.0", + updated: .init() + ), + ] + +} diff --git a/Frameworks/Feed/Bundle/Sources/UI/View Controllers/FeedListViewController.swift b/Frameworks/Feed/Bundle/Sources/UI/View Controllers/FeedListViewController.swift index a9e662f..991ae02 100644 --- a/Frameworks/Feed/Bundle/Sources/UI/View Controllers/FeedListViewController.swift +++ b/Frameworks/Feed/Bundle/Sources/UI/View Controllers/FeedListViewController.swift @@ -455,53 +455,7 @@ import ReviewsiTunesKit #Preview("Feed List with few reviews") { MockURLProtocol.response = .init( statusCode: 200, - object: Feed(entries: [ - .init( - id: 1, - author: "Some author name #1 here", - title: "Some review title #1 goes here...", - content: "Some long, explanatory review comment #1 goes here...", - rating: 3, - version: "v1.0.0", - updated: .init() - ), - .init( - id: 2, - author: "Some author name #2 here", - title: "Some review title #2 goes here...", - content: "Some long, explanatory review comment #2 goes here...", - rating: 5, - version: "v1.0.0", - updated: .init() - ), - .init( - id: 3, - author: "Some author name #3 here", - title: "Some review title #3 goes here...", - content: "Some long, explanatory review comment #3 goes here...", - rating: 1, - version: "v1.0.0", - updated: .init() - ), - .init( - id: 4, - author: "Some author name #4 here", - title: "Some review title #4 goes here...", - content: "Some long, explanatory review comment #4 goes here...", - rating: 4, - version: "v1.0.0", - updated: .init() - ), - .init( - id: 5, - author: "Some author name #5 here", - title: "Some review title #5 goes here...", - content: "Some long, explanatory review comment #5 goes here...", - rating: 2, - version: "v1.0.0", - updated: .init() - ), - ]) + object: Feed(entries: .sample) ) return UINavigationController(rootViewController: FeedListViewController(.init( diff --git a/Frameworks/Feed/Test/Tests/ReviewsFeedTests.swift b/Frameworks/Feed/Test/Tests/ReviewsFeedTests.swift deleted file mode 100644 index a4cfd21..0000000 --- a/Frameworks/Feed/Test/Tests/ReviewsFeedTests.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// ReviewsFeedTests.swift -// ReviewsFeedTests -// -// Created by Javier Cicchelli on 22/03/2024. -// Copyright © 2024 Röck+Cöde. All rights reserved. -// - -import XCTest - -final class ReviewsFeedTests: XCTestCase { - - override func setUpWithError() throws { - // Put setup code here. This method is called before the invocation of each test method in the class. - } - - override func tearDownWithError() throws { - // Put teardown code here. This method is called after the invocation of each test method in the class. - } - - func testExample() throws { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct results. - // Any test you write for XCTest can be annotated as throws and async. - // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. - // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. - } - - func testPerformanceExample() throws { - // This is an example of a performance test case. - measure { - // Put the code you want to measure the time of here. - } - } - -} diff --git a/Reviews.xcodeproj/project.pbxproj b/Reviews.xcodeproj/project.pbxproj index cfa396e..a5f8d0f 100644 --- a/Reviews.xcodeproj/project.pbxproj +++ b/Reviews.xcodeproj/project.pbxproj @@ -20,6 +20,7 @@ 02909E7D2BAB7FFE00710E14 /* Review+DTOs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02909E7C2BAB7FFE00710E14 /* Review+DTOs.swift */; }; 02B36F7C2BAD9D1A00F1A89D /* ReviewsFeedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02B36F7B2BAD9D1A00F1A89D /* ReviewsFeedTests.swift */; }; 02B36F7D2BAD9D1A00F1A89D /* ReviewsFeed.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 02DC7F8F2BA51793000EEEBE /* ReviewsFeed.framework */; platformFilter = ios; }; + 02B36F892BADB26C00F1A89D /* Array+ReviewDTOs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02B36F882BADB26C00F1A89D /* Array+ReviewDTOs.swift */; }; 02C1B1972BAC9BFE001781DE /* FeedListCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02C1B1962BAC9BFE001781DE /* FeedListCoordinator.swift */; }; 02C1B1A92BACA722001781DE /* AppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02C1B1A82BACA722001781DE /* AppCoordinator.swift */; }; 02DA924E2BAAE3FD00C47985 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 02DA924D2BAAE3FD00C47985 /* Localizable.xcstrings */; }; @@ -86,6 +87,7 @@ 02909E7C2BAB7FFE00710E14 /* Review+DTOs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Review+DTOs.swift"; sourceTree = ""; }; 02B36F792BAD9D1A00F1A89D /* FeedTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FeedTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 02B36F7B2BAD9D1A00F1A89D /* ReviewsFeedTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewsFeedTests.swift; sourceTree = ""; }; + 02B36F882BADB26C00F1A89D /* Array+ReviewDTOs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+ReviewDTOs.swift"; sourceTree = ""; }; 02C1B1962BAC9BFE001781DE /* FeedListCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedListCoordinator.swift; sourceTree = ""; }; 02C1B1A82BACA722001781DE /* AppCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinator.swift; sourceTree = ""; }; 02DA924D2BAAE3FD00C47985 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; @@ -236,6 +238,7 @@ 02DC7F912BA51793000EEEBE /* ReviewsFeed.h */, 02DA924B2BAAE3E500C47985 /* Resources */, 02DC7FB02BA51B4F000EEEBE /* Sources */, + 02B36F862BADB1FD00F1A89D /* Previews */, ); path = Bundle; sourceTree = ""; @@ -256,6 +259,22 @@ path = Tests; sourceTree = ""; }; + 02B36F862BADB1FD00F1A89D /* Previews */ = { + isa = PBXGroup; + children = ( + 02B36F872BADB23200F1A89D /* Extensions */, + ); + path = Previews; + sourceTree = ""; + }; + 02B36F872BADB23200F1A89D /* Extensions */ = { + isa = PBXGroup; + children = ( + 02B36F882BADB26C00F1A89D /* Array+ReviewDTOs.swift */, + ); + path = Extensions; + sourceTree = ""; + }; 02C1B1952BAC9BE7001781DE /* Coordinators */ = { isa = PBXGroup; children = ( @@ -564,6 +583,7 @@ buildActionMask = 2147483647; files = ( 02EACF302BABA50D00FF8ECD /* TopWordsCell.swift in Sources */, + 02B36F892BADB26C00F1A89D /* Array+ReviewDTOs.swift in Sources */, 02620B8C2BA89C9A00DE7137 /* FeedListViewModel.swift in Sources */, 02EACF342BABB28900FF8ECD /* TopWord.swift in Sources */, 023AC7FC2BAA3EC10027D064 /* Int+Constants.swift in Sources */, @@ -687,6 +707,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; + DEVELOPMENT_ASSET_PATHS = Frameworks/Feed/Bundle/Previews; DEVELOPMENT_TEAM = 7FMNM89WKG; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; @@ -736,6 +757,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; + DEVELOPMENT_ASSET_PATHS = Frameworks/Feed/Bundle/Previews; DEVELOPMENT_TEAM = 7FMNM89WKG; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; -- 2.47.1 From 5de35984dc7effb82f531a7af0f1419c0fe59adc Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Fri, 22 Mar 2024 14:09:58 +0100 Subject: [PATCH 3/8] Implemented some test cases for the "init(configuration: coordination)" and the "fetch()" functions of the FeedListViewModel view model in the Feed framework. --- .../Logic/View Models/FeedListViewModel.swift | 5 +- .../View Models/FeedListViewModelTests.swift | 264 ++++++++++++++++++ Reviews.xcodeproj/project.pbxproj | 16 +- 3 files changed, 280 insertions(+), 5 deletions(-) create mode 100644 Frameworks/Feed/Test/Tests/View Models/FeedListViewModelTests.swift diff --git a/Frameworks/Feed/Bundle/Sources/Logic/View Models/FeedListViewModel.swift b/Frameworks/Feed/Bundle/Sources/Logic/View Models/FeedListViewModel.swift index c561eac..0403702 100644 --- a/Frameworks/Feed/Bundle/Sources/Logic/View Models/FeedListViewModel.swift +++ b/Frameworks/Feed/Bundle/Sources/Logic/View Models/FeedListViewModel.swift @@ -57,7 +57,7 @@ extension FeedListViewController { var isWordsShowing: Bool { filter != .all - && !words.isEmpty + && !words.isEmpty } // MARK: Functions @@ -89,6 +89,9 @@ extension FeedListViewController { items = filter == .all ? reviewsAll : reviewsFiltered[filter] ?? [] + words = filter == .all + ? [] + : reviewsTopWords[filter] ?? [] isFilterEnabled = !items.isEmpty state = items.isEmpty diff --git a/Frameworks/Feed/Test/Tests/View Models/FeedListViewModelTests.swift b/Frameworks/Feed/Test/Tests/View Models/FeedListViewModelTests.swift new file mode 100644 index 0000000..cdf8634 --- /dev/null +++ b/Frameworks/Feed/Test/Tests/View Models/FeedListViewModelTests.swift @@ -0,0 +1,264 @@ +// +// FeedListViewModelTests.swift +// ReviewsFeedTests +// +// Created by Javier Cicchelli on 22/03/2024. +// Copyright © 2024 Röck+Cöde. All rights reserved. +// + +import Combine +import ReviewsFoundationKit +import ReviewsiTunesKit +import XCTest + +@testable import ReviewsFeed + +final class FeedListViewModelTests: XCTestCase { + + // MARK: Properties + private var sut: FeedListViewController.ViewModel! + + private var cancellables: Set = [] + + // MARK: Setup + override func setUp() async throws { + sut = .init( + configuration: .init(session: .mock), + coordination: nil + ) + } + + override func tearDown() async throws { + cancellables.removeAll() + } + + // MARK: Initialisers tests + func testInit() { + // GIVEN + // WHEN + // THEN + XCTAssertEqual(sut.filter, .all) + XCTAssertFalse(sut.isFilterEnabled) + XCTAssertFalse(sut.isFiltering) + XCTAssertFalse(sut.isLoading) + XCTAssertFalse(sut.isWordsShowing) + XCTAssertTrue(sut.items.isEmpty) + XCTAssertEqual(sut.itemsCount, 0) + XCTAssertEqual(sut.state, .initial) + XCTAssertTrue(sut.words.isEmpty) + } + + // MARK: Functions tests + func testFetch_allItems_whenResponseOK_withSomeItems() { + let expectation = XCTestExpectation() + + var isLoading: [Bool] = [] + + // GIVEN + MockURLProtocol.response = .init( + statusCode: 200, + object: Feed(entries: .sample) + ) + + // WHEN + sut.fetch() + + sut.$isLoading + .collect(3) + .sink { value in + isLoading.append(contentsOf: value) + + expectation.fulfill() + } + .store(in: &cancellables) + + wait(for: [expectation], timeout: 2) + + // THEN + XCTAssertEqual(isLoading, [false, true, false]) + XCTAssertTrue(sut.isFilterEnabled) + XCTAssertFalse(sut.isWordsShowing) + XCTAssertFalse(sut.items.isEmpty) + XCTAssertEqual(sut.items.count, 5) + XCTAssertEqual(sut.itemsCount, 5) + XCTAssertEqual(sut.state, .populated) + } + + func testFetch_allItems_whenResponseOK_withNoItems() { + let expectation = XCTestExpectation() + + var isLoading: [Bool] = [] + + // GIVEN + MockURLProtocol.response = .init( + statusCode: 200, + object: Feed(entries: .none) + ) + + // WHEN + sut.fetch() + + sut.$isLoading + .collect(3) + .sink { value in + isLoading.append(contentsOf: value) + + expectation.fulfill() + } + .store(in: &cancellables) + + wait(for: [expectation], timeout: 2) + + // THEN + XCTAssertEqual(isLoading, [false, true, false]) + XCTAssertFalse(sut.isFilterEnabled) + XCTAssertFalse(sut.isWordsShowing) + XCTAssertTrue(sut.items.isEmpty) + XCTAssertEqual(sut.itemsCount, 0) + XCTAssertEqual(sut.state, .empty) + } + + func testFetch_allItems_whenResponseNotOK() { + let expectation = XCTestExpectation() + + var isLoading: [Bool] = [] + + // GIVEN + MockURLProtocol.response = .init(statusCode: 404) + + // WHEN + sut.fetch() + + sut.$isLoading + .collect(3) + .sink { value in + isLoading.append(contentsOf: value) + + expectation.fulfill() + } + .store(in: &cancellables) + + wait(for: [expectation], timeout: 2) + + // THEN + XCTAssertEqual(isLoading, [false, true, false]) + XCTAssertFalse(sut.isFilterEnabled) + XCTAssertFalse(sut.isWordsShowing) + XCTAssertTrue(sut.items.isEmpty) + XCTAssertEqual(sut.itemsCount, 0) + XCTAssertEqual(sut.state, .error) + } + + func testFetch_filteredItems_whenResponseOK_withSomeItems() { + let expectation = XCTestExpectation() + + var isLoading: [Bool] = [] + + // GIVEN + sut.filter = .only1Star + + MockURLProtocol.response = .init( + statusCode: 200, + object: Feed(entries: .sample) + ) + + // WHEN + sut.fetch() + + sut.$isLoading + .collect(3) + .sink { value in + isLoading.append(contentsOf: value) + + expectation.fulfill() + } + .store(in: &cancellables) + + wait(for: [expectation], timeout: 2) + + // THEN + XCTAssertEqual(isLoading, [false, true, false]) + XCTAssertTrue(sut.isFilterEnabled) + XCTAssertTrue(sut.isWordsShowing) + XCTAssertFalse(sut.items.isEmpty) + XCTAssertEqual(sut.items.count, 1) + XCTAssertEqual(sut.itemsCount, 2) + XCTAssertEqual(sut.state, .populated) + XCTAssertFalse(sut.words.isEmpty) + XCTAssertEqual(sut.words.count, 3) + } + + func testFetch_filteredItems_whenResponseOK_withNoItems() { + let expectation = XCTestExpectation() + + var isLoading: [Bool] = [] + + // GIVEN + sut.filter = .only1Star + + MockURLProtocol.response = .init( + statusCode: 200, + object: Feed(entries: .none) + ) + + // WHEN + sut.fetch() + + sut.$isLoading + .collect(3) + .sink { value in + isLoading.append(contentsOf: value) + + expectation.fulfill() + } + .store(in: &cancellables) + + wait(for: [expectation], timeout: 2) + + // THEN + XCTAssertEqual(isLoading, [false, true, false]) + XCTAssertFalse(sut.isFilterEnabled) + XCTAssertFalse(sut.isWordsShowing) + XCTAssertTrue(sut.items.isEmpty) + XCTAssertEqual(sut.itemsCount, 0) + XCTAssertEqual(sut.state, .empty) + XCTAssertTrue(sut.words.isEmpty) + } + + func testFetch_filteredItems_whenResponseNotOK() { + let expectation = XCTestExpectation() + + var isLoading: [Bool] = [] + + // GIVEN + sut.filter = .only1Star + + MockURLProtocol.response = .init(statusCode: 404) + + // WHEN + sut.fetch() + + sut.$isLoading + .collect(3) + .sink { value in + isLoading.append(contentsOf: value) + + expectation.fulfill() + } + .store(in: &cancellables) + + wait(for: [expectation], timeout: 2) + + // THEN + XCTAssertEqual(isLoading, [false, true, false]) + XCTAssertFalse(sut.isFilterEnabled) + XCTAssertFalse(sut.isWordsShowing) + XCTAssertTrue(sut.items.isEmpty) + XCTAssertEqual(sut.itemsCount, 0) + XCTAssertEqual(sut.state, .error) + XCTAssertTrue(sut.words.isEmpty) + } + + func test + +} diff --git a/Reviews.xcodeproj/project.pbxproj b/Reviews.xcodeproj/project.pbxproj index a5f8d0f..4f92294 100644 --- a/Reviews.xcodeproj/project.pbxproj +++ b/Reviews.xcodeproj/project.pbxproj @@ -18,8 +18,8 @@ 02909E792BAB6B0200710E14 /* FilterOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02909E782BAB6B0200710E14 /* FilterOption.swift */; }; 02909E7B2BAB6D2E00710E14 /* Bundle+Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02909E7A2BAB6D2E00710E14 /* Bundle+Constants.swift */; }; 02909E7D2BAB7FFE00710E14 /* Review+DTOs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02909E7C2BAB7FFE00710E14 /* Review+DTOs.swift */; }; - 02B36F7C2BAD9D1A00F1A89D /* ReviewsFeedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02B36F7B2BAD9D1A00F1A89D /* ReviewsFeedTests.swift */; }; 02B36F7D2BAD9D1A00F1A89D /* ReviewsFeed.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 02DC7F8F2BA51793000EEEBE /* ReviewsFeed.framework */; platformFilter = ios; }; + 02B36F852BAD9DDB00F1A89D /* FeedListViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02B36F842BAD9DDB00F1A89D /* FeedListViewModelTests.swift */; }; 02B36F892BADB26C00F1A89D /* Array+ReviewDTOs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02B36F882BADB26C00F1A89D /* Array+ReviewDTOs.swift */; }; 02C1B1972BAC9BFE001781DE /* FeedListCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02C1B1962BAC9BFE001781DE /* FeedListCoordinator.swift */; }; 02C1B1A92BACA722001781DE /* AppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02C1B1A82BACA722001781DE /* AppCoordinator.swift */; }; @@ -86,7 +86,7 @@ 02909E7A2BAB6D2E00710E14 /* Bundle+Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+Constants.swift"; sourceTree = ""; }; 02909E7C2BAB7FFE00710E14 /* Review+DTOs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Review+DTOs.swift"; sourceTree = ""; }; 02B36F792BAD9D1A00F1A89D /* FeedTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FeedTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 02B36F7B2BAD9D1A00F1A89D /* ReviewsFeedTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewsFeedTests.swift; sourceTree = ""; }; + 02B36F842BAD9DDB00F1A89D /* FeedListViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedListViewModelTests.swift; sourceTree = ""; }; 02B36F882BADB26C00F1A89D /* Array+ReviewDTOs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+ReviewDTOs.swift"; sourceTree = ""; }; 02C1B1962BAC9BFE001781DE /* FeedListCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedListCoordinator.swift; sourceTree = ""; }; 02C1B1A82BACA722001781DE /* AppCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinator.swift; sourceTree = ""; }; @@ -254,11 +254,19 @@ 02B36F742BAD9C4500F1A89D /* Tests */ = { isa = PBXGroup; children = ( - 02B36F7B2BAD9D1A00F1A89D /* ReviewsFeedTests.swift */, + 02B36F832BAD9DC700F1A89D /* View Models */, ); path = Tests; sourceTree = ""; }; + 02B36F832BAD9DC700F1A89D /* View Models */ = { + isa = PBXGroup; + children = ( + 02B36F842BAD9DDB00F1A89D /* FeedListViewModelTests.swift */, + ); + path = "View Models"; + sourceTree = ""; + }; 02B36F862BADB1FD00F1A89D /* Previews */ = { isa = PBXGroup; children = ( @@ -574,7 +582,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 02B36F7C2BAD9D1A00F1A89D /* ReviewsFeedTests.swift in Sources */, + 02B36F852BAD9DDB00F1A89D /* FeedListViewModelTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; -- 2.47.1 From a1abe1f4ae14cc040c24a5f341f272e7774aac77 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Fri, 22 Mar 2024 14:58:13 +0100 Subject: [PATCH 4/8] Implemented some test cases for the "filter(by:)" function of the FeedListViewModel view model in the Feed framework. --- .../Logic/View Models/FeedListViewModel.swift | 14 +- .../View Models/FeedListViewModelTests.swift | 185 +++++++++++++++--- 2 files changed, 169 insertions(+), 30 deletions(-) diff --git a/Frameworks/Feed/Bundle/Sources/Logic/View Models/FeedListViewModel.swift b/Frameworks/Feed/Bundle/Sources/Logic/View Models/FeedListViewModel.swift index 0403702..992e180 100644 --- a/Frameworks/Feed/Bundle/Sources/Logic/View Models/FeedListViewModel.swift +++ b/Frameworks/Feed/Bundle/Sources/Logic/View Models/FeedListViewModel.swift @@ -20,14 +20,14 @@ extension FeedListViewController { private let topWords: TopWordsUseCase = .init() // MARK: Properties - @Published var filter: FilterOption = .all - @Published var isFilterEnabled: Bool = false - @Published var isFiltering: Bool = false - @Published var isLoading: Bool = false - @Published var state: FeedListState = .initial + @Published private(set) var filter: FilterOption = .all + @Published private(set) var isFilterEnabled: Bool = false + @Published private(set) var isFiltering: Bool = false + @Published private(set) var isLoading: Bool = false + @Published private(set) var state: FeedListState = .initial - var items: [Review] = [] - var words: [TopWord] = [] + private(set) var items: [Review] = [] + private(set) var words: [TopWord] = [] private var reviewsAll: [Review] = [] private var reviewsFiltered: FilteredReviews = [:] diff --git a/Frameworks/Feed/Test/Tests/View Models/FeedListViewModelTests.swift b/Frameworks/Feed/Test/Tests/View Models/FeedListViewModelTests.swift index cdf8634..0f9281c 100644 --- a/Frameworks/Feed/Test/Tests/View Models/FeedListViewModelTests.swift +++ b/Frameworks/Feed/Test/Tests/View Models/FeedListViewModelTests.swift @@ -61,8 +61,6 @@ final class FeedListViewModelTests: XCTestCase { ) // WHEN - sut.fetch() - sut.$isLoading .collect(3) .sink { value in @@ -71,7 +69,9 @@ final class FeedListViewModelTests: XCTestCase { expectation.fulfill() } .store(in: &cancellables) - + + sut.fetch() + wait(for: [expectation], timeout: 2) // THEN @@ -96,8 +96,6 @@ final class FeedListViewModelTests: XCTestCase { ) // WHEN - sut.fetch() - sut.$isLoading .collect(3) .sink { value in @@ -106,6 +104,8 @@ final class FeedListViewModelTests: XCTestCase { expectation.fulfill() } .store(in: &cancellables) + + sut.fetch() wait(for: [expectation], timeout: 2) @@ -127,17 +127,17 @@ final class FeedListViewModelTests: XCTestCase { MockURLProtocol.response = .init(statusCode: 404) // WHEN - sut.fetch() - sut.$isLoading .collect(3) .sink { value in isLoading.append(contentsOf: value) - + expectation.fulfill() } .store(in: &cancellables) + sut.fetch() + wait(for: [expectation], timeout: 2) // THEN @@ -155,16 +155,12 @@ final class FeedListViewModelTests: XCTestCase { var isLoading: [Bool] = [] // GIVEN - sut.filter = .only1Star - MockURLProtocol.response = .init( statusCode: 200, object: Feed(entries: .sample) ) // WHEN - sut.fetch() - sut.$isLoading .collect(3) .sink { value in @@ -173,11 +169,15 @@ final class FeedListViewModelTests: XCTestCase { expectation.fulfill() } .store(in: &cancellables) - + + sut.filter(by: .only1Star) + sut.fetch() + wait(for: [expectation], timeout: 2) // THEN XCTAssertEqual(isLoading, [false, true, false]) + XCTAssertEqual(sut.filter, .only1Star) XCTAssertTrue(sut.isFilterEnabled) XCTAssertTrue(sut.isWordsShowing) XCTAssertFalse(sut.items.isEmpty) @@ -194,16 +194,12 @@ final class FeedListViewModelTests: XCTestCase { var isLoading: [Bool] = [] // GIVEN - sut.filter = .only1Star - MockURLProtocol.response = .init( statusCode: 200, object: Feed(entries: .none) ) // WHEN - sut.fetch() - sut.$isLoading .collect(3) .sink { value in @@ -213,10 +209,14 @@ final class FeedListViewModelTests: XCTestCase { } .store(in: &cancellables) + sut.filter(by: .only1Star) + sut.fetch() + wait(for: [expectation], timeout: 2) // THEN XCTAssertEqual(isLoading, [false, true, false]) + XCTAssertEqual(sut.filter, .only1Star) XCTAssertFalse(sut.isFilterEnabled) XCTAssertFalse(sut.isWordsShowing) XCTAssertTrue(sut.items.isEmpty) @@ -231,13 +231,9 @@ final class FeedListViewModelTests: XCTestCase { var isLoading: [Bool] = [] // GIVEN - sut.filter = .only1Star - MockURLProtocol.response = .init(statusCode: 404) // WHEN - sut.fetch() - sut.$isLoading .collect(3) .sink { value in @@ -246,11 +242,15 @@ final class FeedListViewModelTests: XCTestCase { expectation.fulfill() } .store(in: &cancellables) - + + sut.filter(by: .only1Star) + sut.fetch() + wait(for: [expectation], timeout: 2) // THEN XCTAssertEqual(isLoading, [false, true, false]) + XCTAssertEqual(sut.filter, .only1Star) XCTAssertFalse(sut.isFilterEnabled) XCTAssertFalse(sut.isWordsShowing) XCTAssertTrue(sut.items.isEmpty) @@ -259,6 +259,145 @@ final class FeedListViewModelTests: XCTestCase { XCTAssertTrue(sut.words.isEmpty) } - func test + func testFilter_forNewOption_withSomeItems() { + let expectation = XCTestExpectation() + + var filter: [FilterOption] = [] + + // GIVEN + MockURLProtocol.response = .init( + statusCode: 200, + object: Feed(entries: .sample) + ) + + // WHEN + sut.$filter + .dropFirst() + .sink { value in + filter.append(value) + + expectation.fulfill() + } + .store(in: &cancellables) + + sut.fetch() + sut.filter(by: .only1Star) + + wait(for: [expectation], timeout: 2) + + // THEN + XCTAssertEqual(filter, [.only1Star]) + XCTAssertFalse(sut.items.isEmpty) + XCTAssertEqual(sut.items.count, 1) + XCTAssertEqual(sut.itemsCount, 2) + XCTAssertFalse(sut.words.isEmpty) + XCTAssertEqual(sut.words.count, 3) + } + + func testFilter_forNewOption_withNoItems() { + let expectation = XCTestExpectation() + + var filter: [FilterOption] = [] + + // GIVEN + MockURLProtocol.response = .init( + statusCode: 200, + object: Feed(entries: .none) + ) + + // WHEN + sut.$filter + .dropFirst() + .sink { value in + filter.append(value) + + expectation.fulfill() + } + .store(in: &cancellables) + + sut.fetch() + sut.filter(by: .only1Star) + + wait(for: [expectation], timeout: 2) + + // THEN + XCTAssertEqual(filter, [.only1Star]) + XCTAssertTrue(sut.items.isEmpty) + XCTAssertEqual(sut.items.count, 0) + XCTAssertEqual(sut.itemsCount, 0) + XCTAssertTrue(sut.words.isEmpty) + XCTAssertEqual(sut.words.count, 0) + } + + func testFilter_forSameOption_withSomeItems() { + let expectation = XCTestExpectation() + + var filter: [FilterOption] = [] + + // GIVEN + MockURLProtocol.response = .init( + statusCode: 200, + object: Feed(entries: .sample) + ) + + // WHEN + sut.$filter + .dropFirst() + .sink { value in + filter.append(value) + + expectation.fulfill() + } + .store(in: &cancellables) + + sut.fetch() + sut.filter(by: .only1Star) + sut.filter(by: .only1Star) + + wait(for: [expectation], timeout: 2) + + // THEN + XCTAssertEqual(filter, [.only1Star]) + XCTAssertFalse(sut.items.isEmpty) + XCTAssertEqual(sut.items.count, 1) + XCTAssertEqual(sut.itemsCount, 2) + XCTAssertFalse(sut.words.isEmpty) + XCTAssertEqual(sut.words.count, 3) + } + + func testFilter_forSameOption_withNoItems() { + let expectation = XCTestExpectation() + + var filter: [FilterOption] = [] + + // GIVEN + MockURLProtocol.response = .init( + statusCode: 200, + object: Feed(entries: .none) + ) + + // WHEN + sut.$filter + .dropFirst() + .sink { value in + filter.append(value) + + expectation.fulfill() + } + .store(in: &cancellables) + + sut.fetch() + sut.filter(by: .only1Star) + + wait(for: [expectation], timeout: 2) + + // THEN + XCTAssertEqual(filter, [.only1Star]) + XCTAssertTrue(sut.items.isEmpty) + XCTAssertEqual(sut.items.count, 0) + XCTAssertEqual(sut.itemsCount, 0) + XCTAssertTrue(sut.words.isEmpty) + XCTAssertEqual(sut.words.count, 0) + } } -- 2.47.1 From 8eec6f56660c3e5f212ea388b69480c24b20d128 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Fri, 22 Mar 2024 15:15:16 +0100 Subject: [PATCH 5/8] Made the "fetch()" function for the FeedListViewController view controller in the Feed framework to use Swift concurrency. --- .../Logic/View Models/FeedListViewModel.swift | 76 +++++++++---------- .../FeedListViewController.swift | 6 +- 2 files changed, 40 insertions(+), 42 deletions(-) diff --git a/Frameworks/Feed/Bundle/Sources/Logic/View Models/FeedListViewModel.swift b/Frameworks/Feed/Bundle/Sources/Logic/View Models/FeedListViewModel.swift index 992e180..5ee2de6 100644 --- a/Frameworks/Feed/Bundle/Sources/Logic/View Models/FeedListViewModel.swift +++ b/Frameworks/Feed/Bundle/Sources/Logic/View Models/FeedListViewModel.swift @@ -61,49 +61,47 @@ extension FeedListViewController { } // MARK: Functions - func fetch() { - Task { - isFilterEnabled = false - isLoading = items.isEmpty + func fetch() async { + isFilterEnabled = false + isLoading = items.isEmpty - do { - let output = try await iTunesService.getReviews(.init( - appID: configuration.appID, - countryCode: configuration.countryCode - )) - - reviewsAll = output.reviews.map(Review.init) - reviewsFiltered = FilterOption.allCases - .reduce(into: FilteredReviews()) { partialResult, option in - partialResult[option] = reviewsAll.filter { $0.rating.stars == option.rawValue } - } - reviewsTopWords = reviewsFiltered - .mapValues { reviews in - reviews.map(\.comment) - .compactMap { try? filterWords($0) } - } - .mapValues { - topWords($0).map(TopWord.init) - } + do { + let output = try await iTunesService.getReviews(.init( + appID: configuration.appID, + countryCode: configuration.countryCode + )) + + reviewsAll = output.reviews.map(Review.init) + reviewsFiltered = FilterOption.allCases + .reduce(into: FilteredReviews()) { partialResult, option in + partialResult[option] = reviewsAll.filter { $0.rating.stars == option.rawValue } + } + reviewsTopWords = reviewsFiltered + .mapValues { reviews in + reviews.map(\.comment) + .compactMap { try? filterWords($0) } + } + .mapValues { + topWords($0).map(TopWord.init) + } - items = filter == .all - ? reviewsAll - : reviewsFiltered[filter] ?? [] - words = filter == .all - ? [] - : reviewsTopWords[filter] ?? [] + items = filter == .all + ? reviewsAll + : reviewsFiltered[filter] ?? [] + words = filter == .all + ? [] + : reviewsTopWords[filter] ?? [] - isFilterEnabled = !items.isEmpty - state = items.isEmpty - ? .empty - : .populated - } catch { - items = [] - state = .error - } - - isLoading = false + isFilterEnabled = !items.isEmpty + state = items.isEmpty + ? .empty + : .populated + } catch { + items = [] + state = .error } + + isLoading = false } func filter(by option: FilterOption) { diff --git a/Frameworks/Feed/Bundle/Sources/UI/View Controllers/FeedListViewController.swift b/Frameworks/Feed/Bundle/Sources/UI/View Controllers/FeedListViewController.swift index 991ae02..152a5ed 100644 --- a/Frameworks/Feed/Bundle/Sources/UI/View Controllers/FeedListViewController.swift +++ b/Frameworks/Feed/Bundle/Sources/UI/View Controllers/FeedListViewController.swift @@ -118,7 +118,7 @@ final class FeedListViewController: UIViewController { registerTableCells() bindViewModel() - viewModel.fetch() + Task { await viewModel.fetch() } } } @@ -170,7 +170,7 @@ private extension FeedListViewController { // MARK: Actions @objc func refresh(_ sender: AnyObject) { - self.viewModel.fetch() + Task { await self.viewModel.fetch() } } // MARK: Functions @@ -264,7 +264,7 @@ private extension FeedListViewController { ) : nil, action: isErrorState - ? { self.viewModel.fetch() } + ? { Task { await self.viewModel.fetch() } } : nil ) -- 2.47.1 From a5ec80015a34b1c8cffb010abd3bd1d35eabc325 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Fri, 22 Mar 2024 15:15:47 +0100 Subject: [PATCH 6/8] Implemented some test cases for the "item(for:)" function of the FeedListViewModel view model in the Feed framework. --- .../View Models/FeedListViewModelTests.swift | 131 +++++++++++++++--- 1 file changed, 111 insertions(+), 20 deletions(-) diff --git a/Frameworks/Feed/Test/Tests/View Models/FeedListViewModelTests.swift b/Frameworks/Feed/Test/Tests/View Models/FeedListViewModelTests.swift index 0f9281c..9f5a79e 100644 --- a/Frameworks/Feed/Test/Tests/View Models/FeedListViewModelTests.swift +++ b/Frameworks/Feed/Test/Tests/View Models/FeedListViewModelTests.swift @@ -49,7 +49,7 @@ final class FeedListViewModelTests: XCTestCase { } // MARK: Functions tests - func testFetch_allItems_whenResponseOK_withSomeItems() { + func testFetch_allItems_whenResponseOK_withSomeItems() async { let expectation = XCTestExpectation() var isLoading: [Bool] = [] @@ -70,7 +70,7 @@ final class FeedListViewModelTests: XCTestCase { } .store(in: &cancellables) - sut.fetch() + await sut.fetch() wait(for: [expectation], timeout: 2) @@ -84,7 +84,7 @@ final class FeedListViewModelTests: XCTestCase { XCTAssertEqual(sut.state, .populated) } - func testFetch_allItems_whenResponseOK_withNoItems() { + func testFetch_allItems_whenResponseOK_withNoItems() async { let expectation = XCTestExpectation() var isLoading: [Bool] = [] @@ -105,7 +105,7 @@ final class FeedListViewModelTests: XCTestCase { } .store(in: &cancellables) - sut.fetch() + await sut.fetch() wait(for: [expectation], timeout: 2) @@ -118,7 +118,7 @@ final class FeedListViewModelTests: XCTestCase { XCTAssertEqual(sut.state, .empty) } - func testFetch_allItems_whenResponseNotOK() { + func testFetch_allItems_whenResponseNotOK() async { let expectation = XCTestExpectation() var isLoading: [Bool] = [] @@ -136,7 +136,7 @@ final class FeedListViewModelTests: XCTestCase { } .store(in: &cancellables) - sut.fetch() + await sut.fetch() wait(for: [expectation], timeout: 2) @@ -149,7 +149,7 @@ final class FeedListViewModelTests: XCTestCase { XCTAssertEqual(sut.state, .error) } - func testFetch_filteredItems_whenResponseOK_withSomeItems() { + func testFetch_filteredItems_whenResponseOK_withSomeItems() async { let expectation = XCTestExpectation() var isLoading: [Bool] = [] @@ -171,7 +171,8 @@ final class FeedListViewModelTests: XCTestCase { .store(in: &cancellables) sut.filter(by: .only1Star) - sut.fetch() + + await sut.fetch() wait(for: [expectation], timeout: 2) @@ -188,7 +189,7 @@ final class FeedListViewModelTests: XCTestCase { XCTAssertEqual(sut.words.count, 3) } - func testFetch_filteredItems_whenResponseOK_withNoItems() { + func testFetch_filteredItems_whenResponseOK_withNoItems() async { let expectation = XCTestExpectation() var isLoading: [Bool] = [] @@ -210,7 +211,8 @@ final class FeedListViewModelTests: XCTestCase { .store(in: &cancellables) sut.filter(by: .only1Star) - sut.fetch() + + await sut.fetch() wait(for: [expectation], timeout: 2) @@ -225,7 +227,7 @@ final class FeedListViewModelTests: XCTestCase { XCTAssertTrue(sut.words.isEmpty) } - func testFetch_filteredItems_whenResponseNotOK() { + func testFetch_filteredItems_whenResponseNotOK() async { let expectation = XCTestExpectation() var isLoading: [Bool] = [] @@ -244,7 +246,8 @@ final class FeedListViewModelTests: XCTestCase { .store(in: &cancellables) sut.filter(by: .only1Star) - sut.fetch() + + await sut.fetch() wait(for: [expectation], timeout: 2) @@ -259,7 +262,7 @@ final class FeedListViewModelTests: XCTestCase { XCTAssertTrue(sut.words.isEmpty) } - func testFilter_forNewOption_withSomeItems() { + func testFilter_forNewOption_withSomeItems() async { let expectation = XCTestExpectation() var filter: [FilterOption] = [] @@ -280,7 +283,8 @@ final class FeedListViewModelTests: XCTestCase { } .store(in: &cancellables) - sut.fetch() + await sut.fetch() + sut.filter(by: .only1Star) wait(for: [expectation], timeout: 2) @@ -294,7 +298,7 @@ final class FeedListViewModelTests: XCTestCase { XCTAssertEqual(sut.words.count, 3) } - func testFilter_forNewOption_withNoItems() { + func testFilter_forNewOption_withNoItems() async { let expectation = XCTestExpectation() var filter: [FilterOption] = [] @@ -315,7 +319,8 @@ final class FeedListViewModelTests: XCTestCase { } .store(in: &cancellables) - sut.fetch() + await sut.fetch() + sut.filter(by: .only1Star) wait(for: [expectation], timeout: 2) @@ -329,7 +334,7 @@ final class FeedListViewModelTests: XCTestCase { XCTAssertEqual(sut.words.count, 0) } - func testFilter_forSameOption_withSomeItems() { + func testFilter_forSameOption_withSomeItems() async { let expectation = XCTestExpectation() var filter: [FilterOption] = [] @@ -350,7 +355,8 @@ final class FeedListViewModelTests: XCTestCase { } .store(in: &cancellables) - sut.fetch() + await sut.fetch() + sut.filter(by: .only1Star) sut.filter(by: .only1Star) @@ -365,7 +371,7 @@ final class FeedListViewModelTests: XCTestCase { XCTAssertEqual(sut.words.count, 3) } - func testFilter_forSameOption_withNoItems() { + func testFilter_forSameOption_withNoItems() async { let expectation = XCTestExpectation() var filter: [FilterOption] = [] @@ -386,7 +392,8 @@ final class FeedListViewModelTests: XCTestCase { } .store(in: &cancellables) - sut.fetch() + await sut.fetch() + sut.filter(by: .only1Star) wait(for: [expectation], timeout: 2) @@ -400,4 +407,88 @@ final class FeedListViewModelTests: XCTestCase { XCTAssertEqual(sut.words.count, 0) } + func testItemFor_index_withSomeItems() async { + // GIVEN + MockURLProtocol.response = .init( + statusCode: 200, + object: Feed(entries: .sample) + ) + + // WHEN + await sut.fetch() + + let item = sut.item(for: 0) + + // THEN + XCTAssertNotNil(item) + } + + func testItemFor_indexOutOfBounds_withSomeItems() async { + // GIVEN + MockURLProtocol.response = .init( + statusCode: 200, + object: Feed(entries: .sample) + ) + + // WHEN + await sut.fetch() + + let item = sut.item(for: 10) + + // THEN + XCTAssertNil(item) + } + + func testItemFor_index_withFilteredItems() async { + // GIVEN + MockURLProtocol.response = .init( + statusCode: 200, + object: Feed(entries: .sample) + ) + + // WHEN + await sut.fetch() + + sut.filter(by: .only1Star) + + let item = sut.item(for: 1) + + // THEN + XCTAssertNil(item) + } + + func testItemFor_indexOutOfBounds_withFilteredItems() async { + // GIVEN + MockURLProtocol.response = .init( + statusCode: 200, + object: Feed(entries: .sample) + ) + + // WHEN + await sut.fetch() + + sut.filter(by: .only1Star) + + let item = sut.item(for: 2) + + // THEN + XCTAssertNil(item) + } + + func testItemFor_index_withNoItems() async { + // GIVEN + MockURLProtocol.response = .init( + statusCode: 200, + object: Feed(entries: .none) + ) + + // WHEN + await sut.fetch() + + let item = sut.item(for: 0) + + // THEN + XCTAssertNil(item) + } + } -- 2.47.1 From de9f53e497acb69f0a32d7def5e12e8f42ddffca Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Fri, 22 Mar 2024 15:40:11 +0100 Subject: [PATCH 7/8] Implemented the FeedListCoordinationSpy spy in the FeedTest framework. --- .../FeedListCoordinationSpy.swift | 21 ++++++++++++++ Reviews.xcodeproj/project.pbxproj | 28 +++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 Frameworks/Feed/Test/Helpers/Spies/Coordination/FeedListCoordinationSpy.swift diff --git a/Frameworks/Feed/Test/Helpers/Spies/Coordination/FeedListCoordinationSpy.swift b/Frameworks/Feed/Test/Helpers/Spies/Coordination/FeedListCoordinationSpy.swift new file mode 100644 index 0000000..faae101 --- /dev/null +++ b/Frameworks/Feed/Test/Helpers/Spies/Coordination/FeedListCoordinationSpy.swift @@ -0,0 +1,21 @@ +// +// FeedListCoordinationSpy.swift +// ReviewsFeedTest +// +// Created by Javier Cicchelli on 22/03/2024. +// Copyright © 2024 Röck+Cöde. All rights reserved. +// + +@testable import ReviewsFeed + +final class FeedListCoordinationSpy: FeedListCoordination { + + // MARK: Properties + var itemOpened: Bool = false + + // MARK: Functions + func open(_ item: ReviewsFeed.Review) { + itemOpened = true + } + +} diff --git a/Reviews.xcodeproj/project.pbxproj b/Reviews.xcodeproj/project.pbxproj index 4f92294..654ef4f 100644 --- a/Reviews.xcodeproj/project.pbxproj +++ b/Reviews.xcodeproj/project.pbxproj @@ -21,6 +21,7 @@ 02B36F7D2BAD9D1A00F1A89D /* ReviewsFeed.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 02DC7F8F2BA51793000EEEBE /* ReviewsFeed.framework */; platformFilter = ios; }; 02B36F852BAD9DDB00F1A89D /* FeedListViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02B36F842BAD9DDB00F1A89D /* FeedListViewModelTests.swift */; }; 02B36F892BADB26C00F1A89D /* Array+ReviewDTOs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02B36F882BADB26C00F1A89D /* Array+ReviewDTOs.swift */; }; + 02B36F8F2BADCABC00F1A89D /* FeedListCoordinationSpy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02B36F8D2BADCA8000F1A89D /* FeedListCoordinationSpy.swift */; }; 02C1B1972BAC9BFE001781DE /* FeedListCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02C1B1962BAC9BFE001781DE /* FeedListCoordinator.swift */; }; 02C1B1A92BACA722001781DE /* AppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02C1B1A82BACA722001781DE /* AppCoordinator.swift */; }; 02DA924E2BAAE3FD00C47985 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 02DA924D2BAAE3FD00C47985 /* Localizable.xcstrings */; }; @@ -88,6 +89,7 @@ 02B36F792BAD9D1A00F1A89D /* FeedTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FeedTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 02B36F842BAD9DDB00F1A89D /* FeedListViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedListViewModelTests.swift; sourceTree = ""; }; 02B36F882BADB26C00F1A89D /* Array+ReviewDTOs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+ReviewDTOs.swift"; sourceTree = ""; }; + 02B36F8D2BADCA8000F1A89D /* FeedListCoordinationSpy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedListCoordinationSpy.swift; sourceTree = ""; }; 02C1B1962BAC9BFE001781DE /* FeedListCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedListCoordinator.swift; sourceTree = ""; }; 02C1B1A82BACA722001781DE /* AppCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinator.swift; sourceTree = ""; }; 02DA924D2BAAE3FD00C47985 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; @@ -246,6 +248,7 @@ 02A6DA302BA5929F00B943E2 /* Test */ = { isa = PBXGroup; children = ( + 02B36F8A2BADCA5B00F1A89D /* Helpers */, 02B36F742BAD9C4500F1A89D /* Tests */, ); path = Test; @@ -283,6 +286,30 @@ path = Extensions; sourceTree = ""; }; + 02B36F8A2BADCA5B00F1A89D /* Helpers */ = { + isa = PBXGroup; + children = ( + 02B36F8B2BADCA6200F1A89D /* Spies */, + ); + path = Helpers; + sourceTree = ""; + }; + 02B36F8B2BADCA6200F1A89D /* Spies */ = { + isa = PBXGroup; + children = ( + 02B36F8C2BADCA6A00F1A89D /* Coordination */, + ); + path = Spies; + sourceTree = ""; + }; + 02B36F8C2BADCA6A00F1A89D /* Coordination */ = { + isa = PBXGroup; + children = ( + 02B36F8D2BADCA8000F1A89D /* FeedListCoordinationSpy.swift */, + ); + path = Coordination; + sourceTree = ""; + }; 02C1B1952BAC9BE7001781DE /* Coordinators */ = { isa = PBXGroup; children = ( @@ -583,6 +610,7 @@ buildActionMask = 2147483647; files = ( 02B36F852BAD9DDB00F1A89D /* FeedListViewModelTests.swift in Sources */, + 02B36F8F2BADCABC00F1A89D /* FeedListCoordinationSpy.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; -- 2.47.1 From 9436244b34fddd51fdb1c1099d308b58f929438b Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Fri, 22 Mar 2024 15:40:47 +0100 Subject: [PATCH 8/8] Implemented some test cases for the "openItemAt(:)" function of the FeedListViewModel view model in the Feed framework. --- .../Logic/View Models/FeedListViewModel.swift | 17 ++-- .../View Models/FeedListViewModelTests.swift | 91 ++++++++++++++++++- 2 files changed, 99 insertions(+), 9 deletions(-) diff --git a/Frameworks/Feed/Bundle/Sources/Logic/View Models/FeedListViewModel.swift b/Frameworks/Feed/Bundle/Sources/Logic/View Models/FeedListViewModel.swift index 5ee2de6..f853281 100644 --- a/Frameworks/Feed/Bundle/Sources/Logic/View Models/FeedListViewModel.swift +++ b/Frameworks/Feed/Bundle/Sources/Logic/View Models/FeedListViewModel.swift @@ -116,16 +116,19 @@ extension FeedListViewController { } func item(for index: Int) -> Review? { - guard - !items.isEmpty, - index < items.count - else { + guard !items.isEmpty else { return nil } - return isWordsShowing - ? items[index - 1] - : items[index] + let indexToUse = isWordsShowing + ? index - 1 + : index + + guard indexToUse < items.count else { + return nil + } + + return items[indexToUse] } func openItem(at index: Int) { diff --git a/Frameworks/Feed/Test/Tests/View Models/FeedListViewModelTests.swift b/Frameworks/Feed/Test/Tests/View Models/FeedListViewModelTests.swift index 9f5a79e..6c13227 100644 --- a/Frameworks/Feed/Test/Tests/View Models/FeedListViewModelTests.swift +++ b/Frameworks/Feed/Test/Tests/View Models/FeedListViewModelTests.swift @@ -15,6 +15,9 @@ import XCTest final class FeedListViewModelTests: XCTestCase { + // MARK: Constants + private let coordination: FeedListCoordinationSpy = .init() + // MARK: Properties private var sut: FeedListViewController.ViewModel! @@ -24,7 +27,7 @@ final class FeedListViewModelTests: XCTestCase { override func setUp() async throws { sut = .init( configuration: .init(session: .mock), - coordination: nil + coordination: coordination ) } @@ -454,7 +457,7 @@ final class FeedListViewModelTests: XCTestCase { let item = sut.item(for: 1) // THEN - XCTAssertNil(item) + XCTAssertNotNil(item) } func testItemFor_indexOutOfBounds_withFilteredItems() async { @@ -491,4 +494,88 @@ final class FeedListViewModelTests: XCTestCase { XCTAssertNil(item) } + func testOpenItemAt_index_withSomeItems() async { + // GIVEN + MockURLProtocol.response = .init( + statusCode: 200, + object: Feed(entries: .sample) + ) + + // WHEN + await sut.fetch() + + sut.openItem(at: 0) + + // THEN + XCTAssertTrue(coordination.itemOpened) + } + + func testOpenItemAt_indexOutOfBounds_withSomeItems() async { + // GIVEN + MockURLProtocol.response = .init( + statusCode: 200, + object: Feed(entries: .sample) + ) + + // WHEN + await sut.fetch() + + sut.openItem(at: 10) + + // THEN + XCTAssertFalse(coordination.itemOpened) + } + + func testOpenItemAt_index_withFilteredItems() async { + // GIVEN + MockURLProtocol.response = .init( + statusCode: 200, + object: Feed(entries: .sample) + ) + + // WHEN + sut.filter(by: .only1Star) + + await sut.fetch() + + sut.openItem(at: 1) + + // THEN + XCTAssertTrue(coordination.itemOpened) + } + + func testOpenItemAt_indexOutOfBounds_withFilteredItems() async { + // GIVEN + MockURLProtocol.response = .init( + statusCode: 200, + object: Feed(entries: .sample) + ) + + // WHEN + sut.filter(by: .only1Star) + + await sut.fetch() + + sut.openItem(at: 2) + + // THEN + XCTAssertFalse(coordination.itemOpened) + } + + func testOpenItemAt_index_withNoItems() async { + // GIVEN + MockURLProtocol.response = .init( + statusCode: 200, + object: Feed(entries: .none) + ) + + // WHEN + await sut.fetch() + + sut.openItem(at: 0) + + // THEN + XCTAssertFalse(coordination.itemOpened) + } + } -- 2.47.1