Skip to content

gearmamn06/UnitTestKit

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

27 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

UnitTestKit

Test helper kit specialized for asynchronous operation (especially Combine)

Usage

import XCTest
import Combine

@testable import UnitTestKit


class TestSpecifiableTests_usage: BaseTestCase, SpecifiableTest {

    private var sut: ResourceManager!
    private var mockFileHandler: MockFileManager!
    
    override func setUp() {
        super.setUp()
        self.mockFileHandler = MockFileManager()
        self.sut = ResourceManager(fileHandler: self.mockFileHandler)
    }
    
    override func tearDown() {
        self.mockFileHandler = nil
        self.sut = nil
        super.tearDown()
    }
    
    // action -> side effect, and assert
    func testResourceManager_whenDownloadStarted_changeStatus() {
        given {
            let progresses: [Double] = [0, 0.1, 0.2]
            self.mockFileHandler.register("download", value: progresses)
        }
        .when {
            self.sut.startDownloading(path: "dummy_path")
        }
        .then {
            (self.sut.isDownloading == true).assert()
        }
    }
    
    func testResourceManager_whenDownloadFail_emitError() {
        
        given(wait: self.sut.downloadingError) {
            struct DummyError: Error {}
            self.mockFileHandler.register("download", value: DummyError())
        }
        .when {
            self.sut.startDownloading(path: "dummy_path")
        }
        .then(assert: { _ in
            (true).assert()
        })
    }
    
    func testResourceManager_whenDownloading_emitPercent() {
        
        given(wait: self.sut.downloadingPercent) {
            let progresses: [Double] = [0, 0.1, 0.2]
            self.mockFileHandler.register("download", value: progresses)
        }
        .when {
            self.sut.startDownloading(path: "dummy_path")
        }
        .then(take: 3) {
            ($0 == [0, 0.1, 0.2]).assert()
        }
    }
    
    func testResourceManager_loadFile() {
        
        given {
            self.mockFileHandler
                .register("read", value: Result<String, Error>.success("dummy_data").toFuture)
        }
        .whenWait { () -> Future<String, Error> in
            return self.sut.loadFile(path: "dummy_path")
        }
        .then { value in
            (value == "dummy_data").assert()
        }
    }
    
    func testResourceManager_loadFileUsingClosure() {
        
        let handler = ClosureEventHandler<String?>()
        given(wait: handler.eraseToAnyPublisher()) {
            self.mockFileHandler.register("read:closure", value: "dummy_data")
        }
        .when {
            self.sut.loadFile(path: "dummy_path", completed: handler.receiver.send)
        }
        .then { value in
            (value == "dummy_data").assert()
        }
    }
}

// MARK: Test Handler to publisher

extension TestSpecifiableTests_usage {
    
    func testHandler_valuePassingUsingEscapingClosure() {

        given {
        }
        .whenWait { () -> AnyPublisher<Int, Never> in
            let handler = ClosureEventHandler<Int>()
            self.sut.pass(value: 100, withEscapingClosure: handler.receiver.send)
            return handler.eraseToAnyPublisher()
        }
        .then(assert: { value in
            (value == 100).assert()
        })
    }

    func testHandler_valuesPassingUsingNonEscapingClosure() {
        given {
        }
        .whenWait { () -> AnyPublisher<Int, Never> in
            let handler = ClosureEventHandler<Int>()
            self.sut.pass(value: 100, withNonEscapingClosure: handler.receiver.send)
            return handler.eraseToAnyPublisher()
        }
        .then(assert: { value in
            (value == 100).assert()
        })
    }
}


// MARK: - Mocking

fileprivate protocol FileHandler {
    
    var isDownloading: Bool { get }
    
    func read(path: String) -> Future<String, Error>
    
    func read(path: String, complete: @escaping (String?) -> Void)
    
    func download(path: String) -> AnyPublisher<Double, Error>
}

fileprivate class MockFileManager: FileHandler, Mocking {
    
    private var _isDownloading = false
    var isDownloading: Bool {
        return _isDownloading
    }
    
    func read(path: String) -> Future<String, Error> {
        
        self.resolve("read") ?? Future{ _ in }
    }
    
    func read(path: String, complete: @escaping (String?) -> Void) {
        let result: String? = self.resolve("read:closure")
        complete(result)
    }
    
    func download(path: String) -> AnyPublisher<Double, Error> {
        
        if let progresses: [Double] = self.resolve("download") {
            
            self._isDownloading = true
            
            return progresses.publisher
                .map{ $0 }
                .mapError{ _ in NSError() as Error }
                .eraseToAnyPublisher()
        } else if let error: Error = self.resolve("download") {
            return Fail(error: error).eraseToAnyPublisher()
        } else {
            return Empty().eraseToAnyPublisher()
        }
    }
}


fileprivate class ResourceManager {
    
    private var disposebag = PublisherDisposeBag()
    private let fileHandler: FileHandler
    
    private let _percent = PassthroughSubject<Double, Never>()
    private let _occuredError = PassthroughSubject<Error, Never>()
    
    public init(fileHandler: FileHandler) {
        self.fileHandler = fileHandler
    }
}


extension ResourceManager {
    
    var isDownloading: Bool {
        return self.fileHandler.isDownloading
    }
    
    var downloadingError: AnyPublisher<Error, Never> {
        return self._occuredError
            .eraseToAnyPublisher()
    }
    
    var downloadingPercent: AnyPublisher<Double, Never> {
        return self._percent
            .eraseToAnyPublisher()
    }
    
    
    func startDownloading(path: String) {
        self.fileHandler
            .download(path: path)
            .sink(receiveCompletion: { complete in
                
                switch complete {
                case .failure(let error):
                    self._occuredError.send(error)
                    
                default:break
                }
                
            }, receiveValue: { [weak self] percent in
                self?._percent.send(percent)
            })
            .disposed(by: &self.disposebag)
    }
    
    func loadFile(path: String) -> Future<String, Error> {
        
        return self.fileHandler
            .read(path: path)
    }
    
    func loadFile(path: String, completed: @escaping (String?) -> Void) {
        return self.fileHandler
            .read(path: path, complete: completed)
    }
}