Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 0 additions & 23 deletions EssentialApp/EssentialApp/NullStore.swift

This file was deleted.

5 changes: 2 additions & 3 deletions EssentialApp/EssentialApp/SceneDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {

return DispatchQueue(
label: "com.essentialdeveloper.infra.queue",
qos: .userInitiated,
attributes: .concurrent
qos: .userInitiated
).eraseToAnyScheduler()
}()

Expand All @@ -39,7 +38,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
} catch {
assertionFailure("Failed to instantiate CoreData store with error: \(error.localizedDescription)")
logger.fault("Failed to instantiate CoreData store with error: \(error.localizedDescription)")
return NullStore()
return InMemoryFeedStore()
}
}()

Expand Down
54 changes: 0 additions & 54 deletions EssentialApp/EssentialAppTests/Helpers/InMemoryFeedStore.swift

This file was deleted.

38 changes: 37 additions & 1 deletion EssentialFeed/EssentialFeed.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,11 @@
5B74FD1A2D649D0E007478DC /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B74FD192D649D0D007478DC /* ErrorView.swift */; };
5B74FD1F2D64A821007478DC /* UIRefreshControl+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B74FD1E2D64A821007478DC /* UIRefreshControl+Helpers.swift */; };
5B74FD222D6A5F39007478DC /* FeedPresenterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B74FD212D6A5F39007478DC /* FeedPresenterTests.swift */; };
5B75A21B2D9CD20800C1849D /* XCTestCase+FeedImageDataStoreSpecs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B75A21A2D9CD20800C1849D /* XCTestCase+FeedImageDataStoreSpecs.swift */; };
5B75A21E2D9CD2B000C1849D /* FeedImageDataStoreSpecs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B75A21D2D9CD2B000C1849D /* FeedImageDataStoreSpecs.swift */; };
5B75A2232D9CD61600C1849D /* InMemoryFeedStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B75A2222D9CD61600C1849D /* InMemoryFeedStore.swift */; };
5B75A2252D9CD6E300C1849D /* InMemoryFeedStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B75A2242D9CD6E300C1849D /* InMemoryFeedStoreTests.swift */; };
5B75A2272D9CD76100C1849D /* InMemoryFeedImageDataStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B75A2262D9CD76100C1849D /* InMemoryFeedImageDataStoreTests.swift */; };
5B7AB8D72BF8BCE60034C68B /* FeedItemsMapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B7AB8D62BF8BCE60034C68B /* FeedItemsMapperTests.swift */; };
5B8829032D6A7401006E0BD7 /* FeedPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B8829022D6A7401006E0BD7 /* FeedPresenter.swift */; };
5B8829042D6A7527006E0BD7 /* Feed.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 5B8828FF2D6A7137006E0BD7 /* Feed.xcstrings */; };
Expand Down Expand Up @@ -265,6 +270,11 @@
5B74FD192D649D0D007478DC /* ErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = "<group>"; };
5B74FD1E2D64A821007478DC /* UIRefreshControl+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIRefreshControl+Helpers.swift"; sourceTree = "<group>"; };
5B74FD212D6A5F39007478DC /* FeedPresenterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedPresenterTests.swift; sourceTree = "<group>"; };
5B75A21A2D9CD20800C1849D /* XCTestCase+FeedImageDataStoreSpecs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCTestCase+FeedImageDataStoreSpecs.swift"; sourceTree = "<group>"; };
5B75A21D2D9CD2B000C1849D /* FeedImageDataStoreSpecs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedImageDataStoreSpecs.swift; sourceTree = "<group>"; };
5B75A2222D9CD61600C1849D /* InMemoryFeedStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InMemoryFeedStore.swift; sourceTree = "<group>"; };
5B75A2242D9CD6E300C1849D /* InMemoryFeedStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InMemoryFeedStoreTests.swift; sourceTree = "<group>"; };
5B75A2262D9CD76100C1849D /* InMemoryFeedImageDataStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InMemoryFeedImageDataStoreTests.swift; sourceTree = "<group>"; };
5B7AB8D62BF8BCE60034C68B /* FeedItemsMapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedItemsMapperTests.swift; sourceTree = "<group>"; };
5B8828FF2D6A7137006E0BD7 /* Feed.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Feed.xcstrings; sourceTree = "<group>"; };
5B8829022D6A7401006E0BD7 /* FeedPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedPresenter.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -794,6 +804,23 @@
path = "Feed Presentation";
sourceTree = "<group>";
};
5B75A21C2D9CD28800C1849D /* FeedImageDataStoreSpecs */ = {
isa = PBXGroup;
children = (
5B75A21D2D9CD2B000C1849D /* FeedImageDataStoreSpecs.swift */,
5B75A21A2D9CD20800C1849D /* XCTestCase+FeedImageDataStoreSpecs.swift */,
);
path = FeedImageDataStoreSpecs;
sourceTree = "<group>";
};
5B75A2212D9CD60000C1849D /* InMemory */ = {
isa = PBXGroup;
children = (
5B75A2222D9CD61600C1849D /* InMemoryFeedStore.swift */,
);
path = InMemory;
sourceTree = "<group>";
};
5B7BDE002BFA81830002E7F8 /* Feed API */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -847,13 +874,16 @@
children = (
5B034B3D2CA0BA7A00FB65F8 /* Helpers */,
5BF9F3062CD9A4DF00C8DB96 /* FeedStoreSpecs */,
5B75A21C2D9CD28800C1849D /* FeedImageDataStoreSpecs */,
5B8BD18E2C3798D400CCA870 /* CacheFeedUseCaseTests.swift */,
5B034B3A2CA0B09F00FB65F8 /* LoadFeedFromCacheUseCaseTests.swift */,
5B034B402CA371CB00FB65F8 /* ValidateFeedCacheUseCaseTests.swift */,
5BF9F3072CDAD1B600C8DB96 /* CoreDataFeedStoreTests.swift */,
5B88291F2D6BE22C006E0BD7 /* LoadFeedImageDataFromCacheUseCaseTests.swift */,
5B8829282D6BF226006E0BD7 /* CacheFeedImageDataUseCaseTests.swift */,
5B88292A2D6BF4F0006E0BD7 /* CoreDataFeedImageDataStoreTests.swift */,
5B75A2242D9CD6E300C1849D /* InMemoryFeedStoreTests.swift */,
5B75A2262D9CD76100C1849D /* InMemoryFeedImageDataStoreTests.swift */,
);
path = "Feed Cache";
sourceTree = "<group>";
Expand Down Expand Up @@ -881,6 +911,7 @@
isa = PBXGroup;
children = (
5BC4F6C92CDAEFD30002D4CF /* CoreData */,
5B75A2212D9CD60000C1849D /* InMemory */,
);
path = Infrastructure;
sourceTree = "<group>";
Expand All @@ -905,8 +936,8 @@
5BF9F2F82CD9961400C8DB96 /* FeedStoreSpecs.swift */,
5BF9F2FA2CD997F300C8DB96 /* XCTestCase+FeedStoreSpecs.swift */,
5BF9F2FE2CD99FF300C8DB96 /* XCTestCase+FailableRetrieveFeedStoreSpecs.swift */,
5BF9F3002CD9A10500C8DB96 /* XCTestCase+FailableInsertFeedStoreSpecs.swift */,
5BF9F3022CD9A1C600C8DB96 /* XCTestCase+FailableDeleteFeedStoreSpecs.swift */,
5BF9F3002CD9A10500C8DB96 /* XCTestCase+FailableInsertFeedStoreSpecs.swift */,
);
path = FeedStoreSpecs;
sourceTree = "<group>";
Expand Down Expand Up @@ -1202,6 +1233,7 @@
5B1926572D8BB459006C9C65 /* FeedEndpoint.swift in Sources */,
5B107E132BF5BB4200927709 /* FeedImage.swift in Sources */,
5B1926552D8BAFB1006C9C65 /* ImageCommentsEndpoint.swift in Sources */,
5B75A2232D9CD61600C1849D /* InMemoryFeedStore.swift in Sources */,
5BBDA01A2D6FF5F100D68DF0 /* FeedImageDataCache.swift in Sources */,
5B8829242D6BEB71006E0BD7 /* FeedImageDataStore.swift in Sources */,
5B8829032D6A7401006E0BD7 /* FeedPresenter.swift in Sources */,
Expand Down Expand Up @@ -1229,20 +1261,24 @@
5B88291E2D6BD7BE006E0BD7 /* URLProtocolStub.swift in Sources */,
5B034B412CA371CB00FB65F8 /* ValidateFeedCacheUseCaseTests.swift in Sources */,
5B74FD222D6A5F39007478DC /* FeedPresenterTests.swift in Sources */,
5B75A2272D9CD76100C1849D /* InMemoryFeedImageDataStoreTests.swift in Sources */,
5B8829132D6BAA6C006E0BD7 /* FeedImageDataMapperTests.swift in Sources */,
5B7AB8D72BF8BCE60034C68B /* FeedItemsMapperTests.swift in Sources */,
5B88290D2D6A82D3006E0BD7 /* FeedImagePresenterTests.swift in Sources */,
5BF9F3012CD9A10500C8DB96 /* XCTestCase+FailableInsertFeedStoreSpecs.swift in Sources */,
5B7349472D8532C9007F7D5D /* ImageCommentsPresenterTests.swift in Sources */,
5B8829292D6BF226006E0BD7 /* CacheFeedImageDataUseCaseTests.swift in Sources */,
5B88290B2D6A8133006E0BD7 /* FeedLocalizationTests.swift in Sources */,
5B75A21B2D9CD20800C1849D /* XCTestCase+FeedImageDataStoreSpecs.swift in Sources */,
5B75A2252D9CD6E300C1849D /* InMemoryFeedStoreTests.swift in Sources */,
5B304EEA2BFF582400AF431F /* URLSessionHTTPClientTests.swift in Sources */,
5B8829202D6BE22C006E0BD7 /* LoadFeedImageDataFromCacheUseCaseTests.swift in Sources */,
5B7349122D819FC7007F7D5D /* ImageCommentsMapperTests.swift in Sources */,
5B034B3F2CA0BAA500FB65F8 /* FeedStoreSpy.swift in Sources */,
5B19265B2D8BB575006C9C65 /* ImageCommentsEndpointTests.swift in Sources */,
5B034B452CA3A1A100FB65F8 /* SharedTestHelpers.swift in Sources */,
5B8BD18F2C3798D400CCA870 /* CacheFeedUseCaseTests.swift in Sources */,
5B75A21E2D9CD2B000C1849D /* FeedImageDataStoreSpecs.swift in Sources */,
5B7349352D844D6D007F7D5D /* SharedLocalizationTests.swift in Sources */,
5B8829262D6BF168006E0BD7 /* FeedImageDataStoreSpy.swift in Sources */,
5B73494E2D85356B007F7D5D /* ImageCommentsLocalizationTests.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
//
// Created by Rodrigo Porto.
// Copyright © 2025 PortoCode. All Rights Reserved.
//

import Foundation

public class InMemoryFeedStore {
private var feedCache: CachedFeed?
private var feedImageDataCache = NSCache<NSURL, NSData>()

public init() {}
}

extension InMemoryFeedStore: FeedStore {
public func deleteCachedFeed() throws {
feedCache = nil
}

public func insert(_ feed: [LocalFeedImage], timestamp: Date) throws {
feedCache = CachedFeed(feed: feed, timestamp: timestamp)
}

public func retrieve() throws -> CachedFeed? {
feedCache
}
}

extension InMemoryFeedStore: FeedImageDataStore {
public func insert(_ data: Data, for url: URL) throws {
feedImageDataCache.setObject(data as NSData, forKey: url as NSURL)
}

public func retrieve(dataForURL url: URL) throws -> Data? {
feedImageDataCache.object(forKey: url as NSURL) as Data?
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,96 +6,56 @@
import XCTest
import EssentialFeed

class CoreDataFeedImageDataStoreTests: XCTestCase {
class CoreDataFeedImageDataStoreTests: XCTestCase, FeedImageDataStoreSpecs {

func test_retrieveImageData_deliversNotFoundWhenEmpty() throws {
try makeSUT { sut in
expect(sut, toCompleteRetrievalWith: notFound(), for: anyURL())
try makeSUT { sut, imageDataURL in
self.assertThatRetrieveImageDataDeliversNotFoundOnEmptyCache(on: sut, imageDataURL: imageDataURL)
}
}

func test_retrieveImageData_deliversNotFoundWhenStoredDataURLDoesNotMatch() throws {
try makeSUT { sut in
let url = URL(string: "http://a-url.com")!
let nonMatchingURL = URL(string: "http://another-url.com")!

insert(anyData(), for: url, into: sut)

expect(sut, toCompleteRetrievalWith: notFound(), for: nonMatchingURL)
try makeSUT { sut, imageDataURL in
self.assertThatRetrieveImageDataDeliversNotFoundWhenStoredDataURLDoesNotMatch(on: sut, imageDataURL: imageDataURL)
}
}

func test_retrieveImageData_deliversFoundDataWhenThereIsAStoredImageDataMatchingURL() throws {
try makeSUT { sut in
let storedData = anyData()
let matchingURL = URL(string: "http://a-url.com")!

insert(storedData, for: matchingURL, into: sut)

expect(sut, toCompleteRetrievalWith: found(storedData), for: matchingURL)
try makeSUT { sut, imageDataURL in
self.assertThatRetrieveImageDataDeliversFoundDataWhenThereIsAStoredImageDataMatchingURL(on: sut, imageDataURL: imageDataURL)
}
}

func test_retrieveImageData_deliversLastInsertedValue() throws {
try makeSUT { sut in
let firstStoredData = Data("first".utf8)
let lastStoredData = Data("last".utf8)
let url = URL(string: "http://a-url.com")!

insert(firstStoredData, for: url, into: sut)
insert(lastStoredData, for: url, into: sut)

expect(sut, toCompleteRetrievalWith: found(lastStoredData), for: url)
try makeSUT { sut, imageDataURL in
self.assertThatRetrieveImageDataDeliversLastInsertedValueForURL(on: sut, imageDataURL: imageDataURL)
}
}

// - MARK: Helpers
// MARK: - Helpers

private func makeSUT(_ test: @escaping (CoreDataFeedStore) -> Void, file: StaticString = #filePath, line: UInt = #line) throws {
private func makeSUT(_ test: @escaping (CoreDataFeedStore, URL) -> Void, file: StaticString = #filePath, line: UInt = #line) throws {
let storeURL = URL(fileURLWithPath: "/dev/null")
let sut = try CoreDataFeedStore(storeURL: storeURL)
trackForMemoryLeaks(sut, file: file, line: line)

let exp = expectation(description: "wait for operation")
sut.perform {
test(sut)
let imageDataURL = URL(string: "http://a-url.com")!
insertFeedImage(with: imageDataURL, into: sut, file: file, line: line)
test(sut, imageDataURL)
exp.fulfill()
}
wait(for: [exp], timeout: 0.1)
}

}

private func notFound() -> Result<Data?, Error> {
return .success(.none)
}

private func found(_ data: Data) -> Result<Data?, Error> {
return .success(data)
}

private func localImage(url: URL) -> LocalFeedImage {
return LocalFeedImage(id: UUID(), description: "any", location: "any", url: url)
}

private func expect(_ sut: CoreDataFeedStore, toCompleteRetrievalWith expectedResult: Result<Data?, Error>, for url: URL, file: StaticString = #filePath, line: UInt = #line) {
let receivedResult = Result { try sut.retrieve(dataForURL: url) }

switch (receivedResult, expectedResult) {
case let (.success( receivedData), .success(expectedData)):
XCTAssertEqual(receivedData, expectedData, file: file, line: line)

default:
XCTFail("Expected \(expectedResult), got \(receivedResult) instead", file: file, line: line)
}
}

private func insert(_ data: Data, for url: URL, into sut: CoreDataFeedStore, file: StaticString = #filePath, line: UInt = #line) {
private func insertFeedImage(with url: URL, into sut: CoreDataFeedStore, file: StaticString = #filePath, line: UInt = #line) {
do {
let image = localImage(url: url)
let image = LocalFeedImage(id: UUID(), description: "any", location: "any", url: url)
try sut.insert([image], timestamp: Date())
try sut.insert(data, for: url)
} catch {
XCTFail("Failed to insert \(data) with error \(error)", file: file, line: line)
XCTFail("Failed to insert feed image with URL \(url) - error: \(error)", file: file, line: line)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//
// Created by Rodrigo Porto.
// Copyright © 2025 PortoCode. All Rights Reserved.
//

import Foundation

protocol FeedImageDataStoreSpecs {
func test_retrieveImageData_deliversNotFoundWhenEmpty() throws
func test_retrieveImageData_deliversNotFoundWhenStoredDataURLDoesNotMatch() throws
func test_retrieveImageData_deliversFoundDataWhenThereIsAStoredImageDataMatchingURL() throws
func test_retrieveImageData_deliversLastInsertedValue() throws
}
Loading