// // 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: Constants private let coordination: FeedListCoordinationSpy = .init() // 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: coordination ) } 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() async { let expectation = XCTestExpectation() var isLoading: [Bool] = [] // GIVEN MockURLProtocol.response = .init( statusCode: 200, object: Feed(entries: .sample) ) // WHEN sut.$isLoading .collect(3) .sink { value in isLoading.append(contentsOf: value) expectation.fulfill() } .store(in: &cancellables) await sut.fetch() 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() async { let expectation = XCTestExpectation() var isLoading: [Bool] = [] // GIVEN MockURLProtocol.response = .init( statusCode: 200, object: Feed(entries: .none) ) // WHEN sut.$isLoading .collect(3) .sink { value in isLoading.append(contentsOf: value) expectation.fulfill() } .store(in: &cancellables) await sut.fetch() 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() async { let expectation = XCTestExpectation() var isLoading: [Bool] = [] // GIVEN MockURLProtocol.response = .init(statusCode: 404) // WHEN sut.$isLoading .collect(3) .sink { value in isLoading.append(contentsOf: value) expectation.fulfill() } .store(in: &cancellables) await sut.fetch() 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() async { let expectation = XCTestExpectation() var isLoading: [Bool] = [] // GIVEN MockURLProtocol.response = .init( statusCode: 200, object: Feed(entries: .sample) ) // WHEN sut.$isLoading .collect(3) .sink { value in isLoading.append(contentsOf: value) expectation.fulfill() } .store(in: &cancellables) sut.filter(by: .only1Star) await 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) 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() async { let expectation = XCTestExpectation() var isLoading: [Bool] = [] // GIVEN MockURLProtocol.response = .init( statusCode: 200, object: Feed(entries: .none) ) // WHEN sut.$isLoading .collect(3) .sink { value in isLoading.append(contentsOf: value) expectation.fulfill() } .store(in: &cancellables) sut.filter(by: .only1Star) await 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) XCTAssertEqual(sut.itemsCount, 0) XCTAssertEqual(sut.state, .empty) XCTAssertTrue(sut.words.isEmpty) } func testFetch_filteredItems_whenResponseNotOK() async { let expectation = XCTestExpectation() var isLoading: [Bool] = [] // GIVEN MockURLProtocol.response = .init(statusCode: 404) // WHEN sut.$isLoading .collect(3) .sink { value in isLoading.append(contentsOf: value) expectation.fulfill() } .store(in: &cancellables) sut.filter(by: .only1Star) await 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) XCTAssertEqual(sut.itemsCount, 0) XCTAssertEqual(sut.state, .error) XCTAssertTrue(sut.words.isEmpty) } func testFilter_forNewOption_withSomeItems() async { 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) await 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() async { 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) await 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() async { 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) await 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() async { 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) await 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 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 XCTAssertNotNil(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) } 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) } }