Skip to content

Commit a081743

Browse files
author
Mouad
committed
Add LessonDetailScreen tests
1 parent ce2bff9 commit a081743

File tree

8 files changed

+190
-13
lines changed

8 files changed

+190
-13
lines changed

iPhotoSchool.xcodeproj/project.pbxproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@
5353
EDB4DAC22976A20100F48EE7 /* RemoteDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDB4DAC02976A20100F48EE7 /* RemoteDataSource.swift */; };
5454
EDB4DAC52976A28300F48EE7 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDB4DAC42976A28300F48EE7 /* Constants.swift */; };
5555
EDB4DAC72976A34000F48EE7 /* Authentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDB4DAC62976A34000F48EE7 /* Authentication.swift */; };
56+
EDD14E2A297D7C34000831EF /* Fake.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDD14E29297D7C34000831EF /* Fake.swift */; };
57+
EDD14E30297D82AE000831EF /* LessonDetailScreenTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDD14E2F297D82AE000831EF /* LessonDetailScreenTest.swift */; };
5658
EDE9FEEB297B336B00E497A9 /* LessonDetailViewModelTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDE9FEEA297B336B00E497A9 /* LessonDetailViewModelTest.swift */; };
5759
EDE9FEED297B33DB00E497A9 /* MockVideoRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDE9FEEC297B33DB00E497A9 /* MockVideoRepository.swift */; };
5860
EDE9FEF8297C69AF00E497A9 /* LessonListScreenTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDE9FEF7297C69AF00E497A9 /* LessonListScreenTest.swift */; };
@@ -119,6 +121,8 @@
119121
EDB4DAC02976A20100F48EE7 /* RemoteDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteDataSource.swift; sourceTree = "<group>"; };
120122
EDB4DAC42976A28300F48EE7 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = "<group>"; };
121123
EDB4DAC62976A34000F48EE7 /* Authentication.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Authentication.swift; sourceTree = "<group>"; };
124+
EDD14E29297D7C34000831EF /* Fake.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Fake.swift; sourceTree = "<group>"; };
125+
EDD14E2F297D82AE000831EF /* LessonDetailScreenTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LessonDetailScreenTest.swift; sourceTree = "<group>"; };
122126
EDE9FEEA297B336B00E497A9 /* LessonDetailViewModelTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LessonDetailViewModelTest.swift; sourceTree = "<group>"; };
123127
EDE9FEEC297B33DB00E497A9 /* MockVideoRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockVideoRepository.swift; sourceTree = "<group>"; };
124128
EDE9FEF7297C69AF00E497A9 /* LessonListScreenTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LessonListScreenTest.swift; sourceTree = "<group>"; };
@@ -314,6 +318,7 @@
314318
children = (
315319
ED63BB292976D8EF008351E4 /* lessons.json */,
316320
ED63BB272976D8AB008351E4 /* Stub.swift */,
321+
EDD14E29297D7C34000831EF /* Fake.swift */,
317322
);
318323
path = Stub;
319324
sourceTree = "<group>";
@@ -446,6 +451,7 @@
446451
isa = PBXGroup;
447452
children = (
448453
EDE9FEEA297B336B00E497A9 /* LessonDetailViewModelTest.swift */,
454+
EDD14E2F297D82AE000831EF /* LessonDetailScreenTest.swift */,
449455
);
450456
path = LessonDetails;
451457
sourceTree = "<group>";
@@ -592,6 +598,7 @@
592598
ED77DD742979B19B00A3B242 /* RemoteVideoSource.swift in Sources */,
593599
ED5C80692976FD64007CBF22 /* Composer.swift in Sources */,
594600
ED5C808129781C22007CBF22 /* RemoteImageSource.swift in Sources */,
601+
EDD14E2A297D7C34000831EF /* Fake.swift in Sources */,
595602
EDB4DA8B29769E1C00F48EE7 /* iPhotoSchoolApp.swift in Sources */,
596603
EDB4DAC52976A28300F48EE7 /* Constants.swift in Sources */,
597604
ED77DD7C297AA55900A3B242 /* LessonDetailViewModel.swift in Sources */,
@@ -620,6 +627,7 @@
620627
EDE9FEEB297B336B00E497A9 /* LessonDetailViewModelTest.swift in Sources */,
621628
ED63BB252976D868008351E4 /* TestHelpers.swift in Sources */,
622629
ED14604D2978511B00A061C1 /* MockRemoteDataSource.swift in Sources */,
630+
EDD14E30297D82AE000831EF /* LessonDetailScreenTest.swift in Sources */,
623631
EDE9FEFC297C6B4400E497A9 /* MockLessonRepository.swift in Sources */,
624632
);
625633
runOnlyForDeploymentPostprocessing = 0;

iPhotoSchool/Model/Composer.swift

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,13 @@
55
// Created by MOUAD BENJRINIJA on 17/1/2023.
66
//
77

8-
import Foundation
8+
import Foundation
9+
10+
// For test purposes
11+
struct Env {
12+
static let isRunningTests = ProcessInfo.processInfo.environment["isRunningTests"] != nil
13+
static let useFake = ProcessInfo.processInfo.environment["useFake"] != nil
14+
}
915

1016
protocol Composing {
1117
@MainActor
@@ -15,8 +21,7 @@ protocol Composing {
1521
}
1622

1723
class Composer: Composing {
18-
static var shared: Composing = Composer()
19-
private init() {}
24+
static var shared: Composing = Env.useFake ? FakeComposer() : Composer()
2025

2126
func appModel() -> AppModel {
2227
let lessonsRemoteDataSource = LessonsRemoteDataSourceMain(session: .default)
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
//
2+
// Fake.swift
3+
// iPhotoSchool
4+
//
5+
// Created by MOUAD BENJRINIJA on 22/1/2023.
6+
//
7+
8+
import Foundation
9+
10+
class FakeLessonsRepository: LessonsRepository {
11+
func fetchLessons() async -> Loadable<[Lesson]> {
12+
return .loaded(value: Stub.lessons)
13+
}
14+
}
15+
16+
class FakeComposer: Composing {
17+
18+
let realComposer = Composer()
19+
20+
func appModel() -> AppModel {
21+
AppModel(lessonRepository: FakeLessonsRepository())
22+
}
23+
24+
func imageRepository() -> ImageRepository {
25+
realComposer.imageRepository()
26+
}
27+
28+
func videoRepository() -> VideoRepository {
29+
realComposer.videoRepository()
30+
}
31+
32+
}

iPhotoSchool/iPhotoSchoolApp.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@ import SwiftUI
99

1010
@main
1111
struct iPhotoSchoolApp: App {
12+
1213
@StateObject var model = Composer.shared.appModel()
13-
let isRunningTests = ProcessInfo.processInfo.environment["isRunningTests"] != nil
1414

1515
var body: some Scene {
1616
WindowGroup {
17-
if isRunningTests {
17+
if Env.isRunningTests {
1818
// to avoid inaccurate coverage
1919
Text("IS TESTING")
2020
} else {
@@ -23,3 +23,4 @@ struct iPhotoSchoolApp: App {
2323
}
2424
}
2525
}
26+

iPhotoSchoolTests/Mock/MockLessonRepository.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ class MockLessonsRepository: LessonsRepository {
1313
var response: Loadable<[Lesson]> = .notLoaded
1414

1515
func fetchLessons() async -> Loadable<[Lesson]> {
16-
print("loaded", response.value?.count)
1716
return response
1817
}
1918
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
//
2+
// LessonDetailScreenTest.swift
3+
// iPhotoSchoolTests
4+
//
5+
// Created by MOUAD BENJRINIJA on 22/1/2023.
6+
//
7+
8+
import Foundation
9+
10+
import XCTest
11+
import ViewInspector
12+
import SwiftUI
13+
import Combine
14+
import AVKit
15+
16+
@testable import iPhotoSchool
17+
18+
final class LessonDetailScreenTest: XCTestCase {
19+
let lessons = Stub.lessons
20+
var view: LessonDetailScreen!
21+
var sut: LessonDetailViewController!
22+
var viewModel: LessonDetailViewModel!
23+
24+
override func setUpWithError() throws {
25+
view = LessonDetailScreen(lesson: lessons[0], onNext: {})
26+
ViewHosting.host(view: view)
27+
sut = try view.inspect().view(LessonDetailScreen.self).actualView().viewController()
28+
viewModel = sut.viewModel
29+
}
30+
31+
func test_LessonDetails_DisplayedCorrectly() throws {
32+
// Given: a lesson
33+
let lesson = lessons[1]
34+
// When: we pass it to the viewModel
35+
viewModel.lesson = lesson
36+
// Then: views should be correctly updated
37+
XCTAssertEqual(sut.nameLabel.text, lesson.name!)
38+
XCTAssertEqual(sut.descriptionLabel.text, lesson.description!)
39+
let videoURL = (sut.playerViewController.player?.currentItem?.asset as? AVURLAsset)?.url
40+
XCTAssertEqual(videoURL, URL(string: lesson.videoURL!))
41+
}
42+
43+
func test_NextButton_HiddenState() throws {
44+
// Given: any lesson passed
45+
viewModel.lesson = lessons[0]
46+
// When: we pass a nil onNext closure
47+
viewModel.onNext = nil
48+
// Then: next button should be hidden
49+
XCTAssertTrue(sut.nextButton.isHidden)
50+
// When: we pass a valid onNext closure
51+
viewModel.onNext = { }
52+
// Then: next button should be displayed
53+
XCTAssertFalse(sut.nextButton.isHidden)
54+
}
55+
56+
func test_DownloadButton_State() throws {
57+
// Given: any lesson passed
58+
viewModel.lesson = lessons[0]
59+
60+
// When: the video is not downloaded
61+
viewModel.downloadState = .normal
62+
// Then: nav bar button should show a download button
63+
XCTAssertFalse(sut.downloadButton.downloadButton.isHidden)
64+
65+
// When: the video is downloading
66+
viewModel.downloadState = .downloading(progress: 25)
67+
// Then: nav bar button should show the download progress
68+
XCTAssertFalse(sut.downloadButton.downloadingButton.isHidden)
69+
XCTAssertEqual(sut.downloadButton.downloadingButton.currentProgress, 0.25)
70+
71+
// When: the video is not downloaded
72+
viewModel.downloadState = .downloaded
73+
// Then: nav bar button should show a download button
74+
XCTAssertFalse(sut.downloadButton.downloadedButton.isHidden)
75+
}
76+
77+
func test_Player_StartPlaying() throws {
78+
// Given: any lesson passed
79+
viewModel.lesson = lessons[0]
80+
81+
// When: we view is loaded
82+
// Then: start button should be displayed
83+
XCTAssertFalse(sut.startButton.isHidden)
84+
XCTAssertEqual(viewModel.playerState, .ready)
85+
86+
// When: we click on the start button
87+
sut.startButton.sendActions(for: .touchUpInside)
88+
// Then: next button should be hidden and player state updated
89+
XCTAssertTrue(sut.startButton.isHidden)
90+
XCTAssertEqual(viewModel.playerState, .playing)
91+
}
92+
93+
}
94+
95+
struct LessonDetailScreenInspector {
96+
let inspector: InspectableView<ViewType.ClassifiedView>
97+
98+
var loadable: InspectableView<ViewType.View<LoadableView<[Lesson], some View>>> {
99+
get throws {
100+
try inspector
101+
.navigationStack()
102+
.find(LoadableView<[Lesson], AnyView>.self)
103+
}
104+
}
105+
106+
var list: InspectableView<ViewType.List> {
107+
get throws {
108+
try loadable
109+
.list(0)
110+
}
111+
}
112+
113+
var listItems: [InspectableView<ViewType.View<NavigationLink<LessonItemView, Never>>>] {
114+
get throws {
115+
try list.findAll(NavigationLink<LessonItemView, Never>.self)
116+
}
117+
}
118+
119+
var retryButton: InspectableView<ViewType.Button> {
120+
get throws {
121+
try inspector.find(button: "Retry")
122+
}
123+
}
124+
}

iPhotoSchoolTests/UI/LessonDetails/LessonDetailViewModelTest.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ final class TestLessonDetailViewModel: XCTestCase {
2121
sut = LessonDetailViewModel(videoRepository: videoRepository)
2222
}
2323

24+
override func tearDownWithError() throws {
25+
videoRepository = nil
26+
}
27+
2428
func test_initialPreLoadingState() throws {
2529
// Given: No setup
2630
// Then
@@ -123,7 +127,7 @@ final class TestLessonDetailViewModel: XCTestCase {
123127
}
124128
}.store(in: &bag)
125129

126-
wait(for: [downloadingExpectation, downloadedExpectation], timeout: 1.0)
130+
wait(for: [downloadingExpectation, downloadedExpectation], timeout: 5)
127131
// Then: repository should receive download request
128132
videoRepository.verify()
129133
}

iPhotoSchoolTests/UI/LessonsList/LessonListScreenTest.swift

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,19 @@ import Combine
1313

1414
final class LessonListScreenTest: XCTestCase {
1515
let lessons = Stub.lessons
16-
let repository = MockLessonsRepository()
16+
var repository: MockLessonsRepository!
1717
var view: LessonListScreen!
1818
var sut: LessonListScreenInspector!
1919

2020
@MainActor
2121
override func setUpWithError() throws {
22+
repository = MockLessonsRepository()
2223
view = LessonListScreen(model: AppModel(lessonRepository: repository))
2324
sut = LessonListScreenInspector(inspector: try view.inspect())
25+
}
2426

27+
override func tearDownWithError() throws {
28+
repository = nil
2529
}
2630

2731
@MainActor
@@ -46,20 +50,20 @@ final class LessonListScreenTest: XCTestCase {
4650
// Given: a lesson loading failure response
4751
repository.response = .failed(error: APIError.unexpectedResponse)
4852
// When: view finish loading
49-
let loadingExpectation = view.inspection.inspect(after: 0.3) { view in
50-
let loadableView = try self.sut.loadable
53+
54+
let loadingExpectation = view.inspection.inspect(after: 5) { view in
5155
// Then: should display an error view with a retry button
5256
_ = try self.sut.retryButton
5357
}
5458
ViewHosting.host(view: view)
55-
wait(for: [loadingExpectation], timeout: 1)
59+
wait(for: [loadingExpectation], timeout: 6)
5660
}
5761

5862
func test_LessonLoading_retryAfterFailure() throws {
5963
// Given: a lesson loading failure response
6064
repository.response = .failed(error: APIError.unexpectedResponse)
6165
let retryingExpectation = expectation(description: "Is retrying after failure")
62-
let loadingExpectation = view.inspection.inspect(after: 0.3) { view in
66+
let loadingExpectation = view.inspection.inspect(after: 5) { view in
6367
// When: we tap on retry while simulating a successful response
6468
self.repository.response = .loaded(value: self.lessons)
6569
let retryButton = try self.sut.retryButton
@@ -71,7 +75,7 @@ final class LessonListScreenTest: XCTestCase {
7175
}
7276
}
7377
ViewHosting.host(view: view)
74-
wait(for: [loadingExpectation, retryingExpectation], timeout: 1)
78+
wait(for: [loadingExpectation, retryingExpectation], timeout: 6)
7579
}
7680

7781
}

0 commit comments

Comments
 (0)