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
9 changes: 6 additions & 3 deletions RIBs/Classes/LeakDetector/LeakDetector.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,12 @@ public protocol LeakDetectionHandle {
public class LeakDetector {

/// The singleton instance.
public static let instance = LeakDetector()
public static private(set) var instance = LeakDetector()

// 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.
static func setInstance(_ newInstance: LeakDetector) {
instance = newInstance
}

/// The status of leak detection.
///
Expand Down Expand Up @@ -170,8 +175,6 @@ public class LeakDetector {
}
return LeakDetector.disableLeakDetectorOverride
}()

private init() {}
}

fileprivate class LeakDetectionHandleImpl: LeakDetectionHandle {
Expand Down
172 changes: 172 additions & 0 deletions RIBsTests/Interactor/InteractorTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
//
// InteractorTests.swift
// RIBs
//
// Created by Alex Bush on 6/22/25.
//

@testable import RIBs
import XCTest
import RxSwift

final class InteractorTests: XCTestCase {

private var interactor: InteractorMock!

override func setUp() {
super.setUp()

interactor = InteractorMock() // NOTE: we're using InteractorMock here to test the underlying parent class, Interactor, behavior so this is appropriate here.
}

func test_interactorIsInactiveByDefault() {
XCTAssertFalse(interactor.isActive)
let _ = interactor.isActiveStream.subscribe { isActive in
XCTAssertFalse(isActive)
}
}

func test_isActive_whenStarted_isTrue() {
// give
// when
interactor.activate()
// then
XCTAssertTrue(interactor.isActive)
let _ = interactor.isActiveStream.subscribe { isActive in
XCTAssertTrue(isActive)
}
}

func test_isActive_whenDeactivated_isFalse() {
// given
interactor.activate()
// when
interactor.deactivate()
// then
XCTAssertFalse(interactor.isActive)
let _ = interactor.isActiveStream.subscribe { isActive in
XCTAssertFalse(isActive)
}
}

func test_didBecomeActive_isCalledWhenStarted() {
// given
// when
interactor.activate()
// then
XCTAssertEqual(interactor.didBecomeActiveCallCount, 1)
}

func test_didBecomeActive_isNotCalledWhenAlreadyActive() {
// given
interactor.activate()
XCTAssertEqual(interactor.didBecomeActiveCallCount, 1)
// when
interactor.activate()
// then
XCTAssertEqual(interactor.didBecomeActiveCallCount, 1)
}

func test_willResignActive_isCalledWhenDeactivated() {
// given
interactor.activate()
// when
interactor.deactivate()
// then
XCTAssertEqual(interactor.willResignActiveCallCount, 1)
}

func test_willResignActive_isNotCalledWhenAlreadyInactive() {
// given
interactor.activate()
interactor.deactivate()
XCTAssertEqual(interactor.willResignActiveCallCount, 1)
// when
interactor.deactivate()
// then
XCTAssertEqual(interactor.willResignActiveCallCount, 1)
}

func test_isActiveStream_completedOnInteractorDeinit() {
// given
var isActiveStreamCompleted = false
interactor.activate()
let _ = interactor.isActiveStream.subscribe { _ in } onCompleted: {
isActiveStreamCompleted = true
}

// when
interactor = nil
// then
XCTAssertTrue(isActiveStreamCompleted)

}

// MARK: - BEGIN Observables Attached/Detached to/from Interactor
func test_observableAttachedToInactiveInteactorIsDisposedImmediately() {
// given
var onDisposeCalled = false
let subjectEmiitingValues: PublishSubject<Int> = .init()
let observable = subjectEmiitingValues.asObservable().do { _ in } onDispose: {
onDisposeCalled = true
}
// when
observable.subscribe().disposeOnDeactivate(interactor: interactor)
// then
XCTAssertTrue(onDisposeCalled)
}

func test_observableIsDisposedOnInteractorDeactivation() {
// given
var onDisposeCalled = false
let subjectEmiitingValues: PublishSubject<Int> = .init()
let observable = subjectEmiitingValues.asObservable().do { _ in } onDispose: {
onDisposeCalled = true
}
interactor.activate()
observable.subscribe().disposeOnDeactivate(interactor: interactor)
// when
interactor.deactivate()
// then
XCTAssertTrue(onDisposeCalled)
}

func test_observableIsDisposedOnInteractorDeinit() {
// given
var onDisposeCalled = false
let subjectEmiitingValues: PublishSubject<Int> = .init()
let observable = subjectEmiitingValues.asObservable().do { _ in } onDispose: {
onDisposeCalled = true
}
interactor.activate()
observable.subscribe().disposeOnDeactivate(interactor: interactor)
XCTAssertFalse(onDisposeCalled)
// when
interactor = nil
// then
XCTAssertTrue(onDisposeCalled)
}
// MARK: Observables Attached/Detached to/from Interactor END -

// MARK: - BEGIN Observables Confined to Interactor
func test_observableConfinedToInteractorOnlyEmitsValueWhenInteractorIsActive() {
// given
var emittedValue: Int?
let subjectEmiitingValues: PublishSubject<Int> = .init()
let confinedObservable = subjectEmiitingValues.asObservable().confineTo(interactor)
let _ = confinedObservable.confineTo(interactor)
let _ = confinedObservable.subscribe { newValue in
emittedValue = newValue
}

subjectEmiitingValues.onNext(1)
XCTAssertNil(emittedValue)
// when
interactor.activate()
subjectEmiitingValues.onNext(2)
// then
XCTAssertNotNil(emittedValue)
XCTAssertEqual(emittedValue, 2)
}
// MARK: Observables Confined to Interactor -
}
40 changes: 40 additions & 0 deletions RIBsTests/Interactor/PresentableInteractorTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//
// PresentableInteractorTests.swift
// RIBs
//
// Created by Alex Bush on 6/23/25.
//

@testable import RIBs
import XCTest
import RxSwift

protocol TestPresenter {}

final class PresenterMock: TestPresenter {}

final class PresentableInteractorTests: XCTestCase {

private var interactor: PresentableInteractor<TestPresenter>!

override func setUp() {
super.setUp()

}

func test_deinit_doesNotLeakPresenter() {
// given
let presenterMock = PresenterMock()
let disposeBag = DisposeBag()
interactor = PresentableInteractor<TestPresenter>(presenter: presenterMock)
var status: LeakDetectionStatus = .DidComplete
LeakDetector.instance.status.subscribe { newStatus in
status = newStatus
}.disposed(by: disposeBag)

// when
interactor = nil
// then
XCTAssertEqual(status, .InProgress)
}
}
39 changes: 39 additions & 0 deletions RIBsTests/LeakDetector/LeakDetectorMock.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//
// LeakDetectorMock.swift
// RIBs
//
// Created by Alex Bush on 7/26/25.
//

@testable import RIBs
import Foundation
import RxSwift
import UIKit

final class LeakDetectionHandleMock: LeakDetectionHandle {
var cancelCallCount = 0
func cancel() {
cancelCallCount += 1
}
}

final class LeakDetectorMock: LeakDetector {

var expectDeallocateCallCount = 0
override func expectDeallocate(object: AnyObject, inTime time: TimeInterval) -> LeakDetectionHandle {
expectDeallocateCallCount += 1
return LeakDetectionHandleMock()
}

var expectViewControllerDisappearCallCount = 0
override func expectViewControllerDisappear(viewController: UIViewController, inTime time: TimeInterval) -> LeakDetectionHandle {
expectViewControllerDisappearCallCount += 1
return LeakDetectionHandleMock()
}

var statusCallCount = 0
override var status: Observable<LeakDetectionStatus> {
statusCallCount += 1
return super.status
}
}
41 changes: 21 additions & 20 deletions RIBsTests/Mocks.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,27 +44,28 @@ class ViewControllableMock: ViewControllable {
let uiviewController = UIViewController(nibName: nil, bundle: nil)
}

class InteractorMock: Interactable {
var isActive: Bool {
return active.value
}

var isActiveStream: Observable<Bool> {
return active.asObservable()
}

private let active = BehaviorRelay<Bool>(value: false)

init() {}

// MARK: - Lifecycle

func activate() {
active.accept(true)
class InteractorMock: Interactor {
var didBecomeActiveHandler: (() -> ())?
var didBecomeActiveCallCount: Int = 0
var willResignActiveHandler: (() -> ())?
var willResignActiveCallCount: Int = 0

override func didBecomeActive() {
didBecomeActiveCallCount += 1
super.didBecomeActive()

if let didBecomeActiveHandler = didBecomeActiveHandler {
didBecomeActiveHandler()
}
}

func deactivate() {
active.accept(false)

override func willResignActive() {
willResignActiveCallCount += 1
super.willResignActive()

if let willResignActiveHandler = willResignActiveHandler {
willResignActiveHandler()
}
}
}

Expand Down
Loading
Loading