Skip to content

Commit e305157

Browse files
authored
Merge pull request #18 from alexvbush/main
Add extensive unit-tests for Interactor and Router
2 parents 670d3aa + 2bf6df8 commit e305157

8 files changed

Lines changed: 514 additions & 90 deletions

File tree

RIBs/Classes/LeakDetector/LeakDetector.swift

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,12 @@ public protocol LeakDetectionHandle {
5454
public class LeakDetector {
5555

5656
/// The singleton instance.
57-
public static let instance = LeakDetector()
57+
public static private(set) var instance = LeakDetector()
58+
59+
// This is used internally to be able to set mock instance in unit-tests. The public API and behavior of the public static LeakDetector instance above does not change.
60+
static func setInstance(_ newInstance: LeakDetector) {
61+
instance = newInstance
62+
}
5863

5964
/// The status of leak detection.
6065
///
@@ -170,8 +175,6 @@ public class LeakDetector {
170175
}
171176
return LeakDetector.disableLeakDetectorOverride
172177
}()
173-
174-
private init() {}
175178
}
176179

177180
fileprivate class LeakDetectionHandleImpl: LeakDetectionHandle {
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
//
2+
// InteractorTests.swift
3+
// RIBs
4+
//
5+
// Created by Alex Bush on 6/22/25.
6+
//
7+
8+
@testable import RIBs
9+
import XCTest
10+
import RxSwift
11+
12+
final class InteractorTests: XCTestCase {
13+
14+
private var interactor: InteractorMock!
15+
16+
override func setUp() {
17+
super.setUp()
18+
19+
interactor = InteractorMock() // NOTE: we're using InteractorMock here to test the underlying parent class, Interactor, behavior so this is appropriate here.
20+
}
21+
22+
func test_interactorIsInactiveByDefault() {
23+
XCTAssertFalse(interactor.isActive)
24+
let _ = interactor.isActiveStream.subscribe { isActive in
25+
XCTAssertFalse(isActive)
26+
}
27+
}
28+
29+
func test_isActive_whenStarted_isTrue() {
30+
// give
31+
// when
32+
interactor.activate()
33+
// then
34+
XCTAssertTrue(interactor.isActive)
35+
let _ = interactor.isActiveStream.subscribe { isActive in
36+
XCTAssertTrue(isActive)
37+
}
38+
}
39+
40+
func test_isActive_whenDeactivated_isFalse() {
41+
// given
42+
interactor.activate()
43+
// when
44+
interactor.deactivate()
45+
// then
46+
XCTAssertFalse(interactor.isActive)
47+
let _ = interactor.isActiveStream.subscribe { isActive in
48+
XCTAssertFalse(isActive)
49+
}
50+
}
51+
52+
func test_didBecomeActive_isCalledWhenStarted() {
53+
// given
54+
// when
55+
interactor.activate()
56+
// then
57+
XCTAssertEqual(interactor.didBecomeActiveCallCount, 1)
58+
}
59+
60+
func test_didBecomeActive_isNotCalledWhenAlreadyActive() {
61+
// given
62+
interactor.activate()
63+
XCTAssertEqual(interactor.didBecomeActiveCallCount, 1)
64+
// when
65+
interactor.activate()
66+
// then
67+
XCTAssertEqual(interactor.didBecomeActiveCallCount, 1)
68+
}
69+
70+
func test_willResignActive_isCalledWhenDeactivated() {
71+
// given
72+
interactor.activate()
73+
// when
74+
interactor.deactivate()
75+
// then
76+
XCTAssertEqual(interactor.willResignActiveCallCount, 1)
77+
}
78+
79+
func test_willResignActive_isNotCalledWhenAlreadyInactive() {
80+
// given
81+
interactor.activate()
82+
interactor.deactivate()
83+
XCTAssertEqual(interactor.willResignActiveCallCount, 1)
84+
// when
85+
interactor.deactivate()
86+
// then
87+
XCTAssertEqual(interactor.willResignActiveCallCount, 1)
88+
}
89+
90+
func test_isActiveStream_completedOnInteractorDeinit() {
91+
// given
92+
var isActiveStreamCompleted = false
93+
interactor.activate()
94+
let _ = interactor.isActiveStream.subscribe { _ in } onCompleted: {
95+
isActiveStreamCompleted = true
96+
}
97+
98+
// when
99+
interactor = nil
100+
// then
101+
XCTAssertTrue(isActiveStreamCompleted)
102+
103+
}
104+
105+
// MARK: - BEGIN Observables Attached/Detached to/from Interactor
106+
func test_observableAttachedToInactiveInteactorIsDisposedImmediately() {
107+
// given
108+
var onDisposeCalled = false
109+
let subjectEmiitingValues: PublishSubject<Int> = .init()
110+
let observable = subjectEmiitingValues.asObservable().do { _ in } onDispose: {
111+
onDisposeCalled = true
112+
}
113+
// when
114+
observable.subscribe().disposeOnDeactivate(interactor: interactor)
115+
// then
116+
XCTAssertTrue(onDisposeCalled)
117+
}
118+
119+
func test_observableIsDisposedOnInteractorDeactivation() {
120+
// given
121+
var onDisposeCalled = false
122+
let subjectEmiitingValues: PublishSubject<Int> = .init()
123+
let observable = subjectEmiitingValues.asObservable().do { _ in } onDispose: {
124+
onDisposeCalled = true
125+
}
126+
interactor.activate()
127+
observable.subscribe().disposeOnDeactivate(interactor: interactor)
128+
// when
129+
interactor.deactivate()
130+
// then
131+
XCTAssertTrue(onDisposeCalled)
132+
}
133+
134+
func test_observableIsDisposedOnInteractorDeinit() {
135+
// given
136+
var onDisposeCalled = false
137+
let subjectEmiitingValues: PublishSubject<Int> = .init()
138+
let observable = subjectEmiitingValues.asObservable().do { _ in } onDispose: {
139+
onDisposeCalled = true
140+
}
141+
interactor.activate()
142+
observable.subscribe().disposeOnDeactivate(interactor: interactor)
143+
XCTAssertFalse(onDisposeCalled)
144+
// when
145+
interactor = nil
146+
// then
147+
XCTAssertTrue(onDisposeCalled)
148+
}
149+
// MARK: Observables Attached/Detached to/from Interactor END -
150+
151+
// MARK: - BEGIN Observables Confined to Interactor
152+
func test_observableConfinedToInteractorOnlyEmitsValueWhenInteractorIsActive() {
153+
// given
154+
var emittedValue: Int?
155+
let subjectEmiitingValues: PublishSubject<Int> = .init()
156+
let confinedObservable = subjectEmiitingValues.asObservable().confineTo(interactor)
157+
let _ = confinedObservable.confineTo(interactor)
158+
let _ = confinedObservable.subscribe { newValue in
159+
emittedValue = newValue
160+
}
161+
162+
subjectEmiitingValues.onNext(1)
163+
XCTAssertNil(emittedValue)
164+
// when
165+
interactor.activate()
166+
subjectEmiitingValues.onNext(2)
167+
// then
168+
XCTAssertNotNil(emittedValue)
169+
XCTAssertEqual(emittedValue, 2)
170+
}
171+
// MARK: Observables Confined to Interactor -
172+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
//
2+
// PresentableInteractorTests.swift
3+
// RIBs
4+
//
5+
// Created by Alex Bush on 6/23/25.
6+
//
7+
8+
@testable import RIBs
9+
import XCTest
10+
import RxSwift
11+
12+
protocol TestPresenter {}
13+
14+
final class PresenterMock: TestPresenter {}
15+
16+
final class PresentableInteractorTests: XCTestCase {
17+
18+
private var interactor: PresentableInteractor<TestPresenter>!
19+
20+
override func setUp() {
21+
super.setUp()
22+
23+
}
24+
25+
func test_deinit_doesNotLeakPresenter() {
26+
// given
27+
let presenterMock = PresenterMock()
28+
let disposeBag = DisposeBag()
29+
interactor = PresentableInteractor<TestPresenter>(presenter: presenterMock)
30+
var status: LeakDetectionStatus = .DidComplete
31+
LeakDetector.instance.status.subscribe { newStatus in
32+
status = newStatus
33+
}.disposed(by: disposeBag)
34+
35+
// when
36+
interactor = nil
37+
// then
38+
XCTAssertEqual(status, .InProgress)
39+
}
40+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
//
2+
// LeakDetectorMock.swift
3+
// RIBs
4+
//
5+
// Created by Alex Bush on 7/26/25.
6+
//
7+
8+
@testable import RIBs
9+
import Foundation
10+
import RxSwift
11+
import UIKit
12+
13+
final class LeakDetectionHandleMock: LeakDetectionHandle {
14+
var cancelCallCount = 0
15+
func cancel() {
16+
cancelCallCount += 1
17+
}
18+
}
19+
20+
final class LeakDetectorMock: LeakDetector {
21+
22+
var expectDeallocateCallCount = 0
23+
override func expectDeallocate(object: AnyObject, inTime time: TimeInterval) -> LeakDetectionHandle {
24+
expectDeallocateCallCount += 1
25+
return LeakDetectionHandleMock()
26+
}
27+
28+
var expectViewControllerDisappearCallCount = 0
29+
override func expectViewControllerDisappear(viewController: UIViewController, inTime time: TimeInterval) -> LeakDetectionHandle {
30+
expectViewControllerDisappearCallCount += 1
31+
return LeakDetectionHandleMock()
32+
}
33+
34+
var statusCallCount = 0
35+
override var status: Observable<LeakDetectionStatus> {
36+
statusCallCount += 1
37+
return super.status
38+
}
39+
}

RIBsTests/Mocks.swift

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -44,27 +44,28 @@ class ViewControllableMock: ViewControllable {
4444
let uiviewController = UIViewController(nibName: nil, bundle: nil)
4545
}
4646

47-
class InteractorMock: Interactable {
48-
var isActive: Bool {
49-
return active.value
50-
}
51-
52-
var isActiveStream: Observable<Bool> {
53-
return active.asObservable()
54-
}
55-
56-
private let active = BehaviorRelay<Bool>(value: false)
57-
58-
init() {}
59-
60-
// MARK: - Lifecycle
61-
62-
func activate() {
63-
active.accept(true)
47+
class InteractorMock: Interactor {
48+
var didBecomeActiveHandler: (() -> ())?
49+
var didBecomeActiveCallCount: Int = 0
50+
var willResignActiveHandler: (() -> ())?
51+
var willResignActiveCallCount: Int = 0
52+
53+
override func didBecomeActive() {
54+
didBecomeActiveCallCount += 1
55+
super.didBecomeActive()
56+
57+
if let didBecomeActiveHandler = didBecomeActiveHandler {
58+
didBecomeActiveHandler()
59+
}
6460
}
65-
66-
func deactivate() {
67-
active.accept(false)
61+
62+
override func willResignActive() {
63+
willResignActiveCallCount += 1
64+
super.willResignActive()
65+
66+
if let willResignActiveHandler = willResignActiveHandler {
67+
willResignActiveHandler()
68+
}
6869
}
6970
}
7071

0 commit comments

Comments
 (0)