{"id":16100198,"url":"https://github.com/kuniwak/testabledesignexample","last_synced_at":"2025-06-10T20:11:58.561Z","repository":{"id":145028261,"uuid":"84628056","full_name":"Kuniwak/TestableDesignExample","owner":"Kuniwak","description":"Sample App to learn a testable design (Smalltalk flavored MVC)","archived":false,"fork":false,"pushed_at":"2020-01-27T06:50:52.000Z","size":7943,"stargazers_count":83,"open_issues_count":1,"forks_count":6,"subscribers_count":7,"default_branch":"master","last_synced_at":"2025-04-02T08:48:04.717Z","etag":null,"topics":["design-patterns","ios","mvc-architecture","testability"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Kuniwak.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2017-03-11T06:02:59.000Z","updated_at":"2025-02-04T10:36:06.000Z","dependencies_parsed_at":null,"dependency_job_id":"c1732354-f43c-4f32-ba93-c6a9f4a7edac","html_url":"https://github.com/Kuniwak/TestableDesignExample","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Kuniwak%2FTestableDesignExample","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Kuniwak%2FTestableDesignExample/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Kuniwak%2FTestableDesignExample/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Kuniwak%2FTestableDesignExample/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Kuniwak","download_url":"https://codeload.github.com/Kuniwak/TestableDesignExample/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Kuniwak%2FTestableDesignExample/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":259144814,"owners_count":22811926,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["design-patterns","ios","mvc-architecture","testability"],"created_at":"2024-10-09T18:45:30.769Z","updated_at":"2025-06-10T20:11:58.544Z","avatar_url":"https://github.com/Kuniwak.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"Testable design example for iOS Apps\n====================================\n\n[![Build Status](https://www.bitrise.io/app/97b1fa446d801c01/status.svg?token=_uFGlK9iYeSQdtXnnPufYw\u0026branch=master)](https://www.bitrise.io/app/97b1fa446d801c01)\n\nThis is a sample App to learn testable design.\n\n![](https://raw.githubusercontent.com/Kuniwak/TestableDesignExample/master/Documentation/Images/Screenshots.png)\n\nYou can learn the following things by reading this implementation:\n\n- How to make loose coupling for testing\n- How to decouple global variables\n- How to use type-checking as a test\n\n\n\nArchitecture\n------------\n\nThis App adopt Smalltalk flavored MVC (it is not Apple MVC). Smalltalk flavored MVC is a architecture that can test easily.\nYou may know major architectures such as MVVM, MVP, Flux and VIPER, but also Smalltalk MVC can make loose coupling.\n\n![](https://raw.githubusercontent.com/Kuniwak/TestableDesignExample/master/Documentation/Images/ClassDiagram_En.png)\n\nWhile there are a lot of architectures, but they share a common important things that we should do.\nSo, learning this implementation is still worth the candle if you choose other architectures.\n\n\n\n### Sample Code\n\nIn our approach, we create a Xib file per `UIViewController`.\nAnd all `UIViewControllers` have a initializer that require models.\n\nAnd we should create ViewBindings and Controllers and connect them to the given Model when `UIViewController#loadView()` is called.\n\nConcrete implementation is below:\n\n```swift\nclass FooViewController: UIViewController {\n    private var model: FooModelProtocol\n    private var viewBinding: FooViewBindingProtocol?\n    private var controller: FooControllerProtocol?\n\n    init(model: FooModelProtocol) {\n        self.model = model\n        super.init(nibName: nil, bundle: nil)\n    }\n\n    required init?(coder aDecoder: NSCoder) {\n        // NOTE: In this project, we do not want to restore the VC.\n        return nil\n    }\n\n    // Connect Model and ViewBinding, Controller.\n    override func loadView() {\n        let rootView = FooRootView()\n        self.view = rootView\n\n        let controller = FooController(\n            observing: rootView.barView,\n            willNotifyTo: self.model\n        )\n        self.controller = controller\n\n        self.viewBinding = FooViewBinding(\n            observing: self.model,\n            handling: (\n                bar: rootView.barView,\n                baz: rootView.bazView\n            )\n        )\n        self.viewBinding.delegate = controller\n    }\n}\n```\n\n```swift\n// FooModel is a state-machine that can transit to FooModelState.\n// Notify change events to others via an observable `didChange` when\n// API was successfully done or failed.\nclass FooModel: FooModelProtocol {\n    private let repository: FooRepositoryProtocol\n    private let stateVariable: RxSwift.Variable\u003cFooModelState\u003e\n\n    /// An Observable that will notify events when the internal state is changed.\n    var didChange: RxSwift.Observable\u003cFooModelState\u003e {\n        return self.stateVariable.asObservable()\n    }\n\n    /// The current state of the model.\n    var currentState: FooModelState {\n        get { return self.stateVariable.value }\n        set { self.stateVariable.value = newValue }\n    }\n\n    init(\n        startingWith initialState: FooModelState,\n        fetchingVia repository: FooRepositoryProtocol\n    ) {\n        self.stateVariable = RxSwift.Variable\u003cFooModelState\u003e(initialState)\n        self.repository = repository\n    }\n\n    func doSomething() {\n        switch self.currentState {\n        case .preparing:\n            // NOTE: Prevent duplicated calls.\n            return\n\n        case .success, .failure:\n            self.currentState = .preparing\n\n            self.repository\n                .doSomething()\n                .then { entity in \n                    self.currentState = .success(entity)\n                }\n                .catch { error in\n                    self.currentState = .failure(\n                        because: .unspecified(debugInfo: \"\\(error)\")\n                    )\n                }\n        }\n    }\n}\n\n\n// States that FooModel can transit to.\nenum FooModelState {\n    case preparing\n    case success(Entity)\n    case failure(because: Reason)\n\n    enum Reason {\n        case unspecified(debugInfo: String)\n    }\n}\n```\n\n```swift\nclass FooViewBinding: FooViewBindingProtocol {\n    typealias Views = (bar: BarView, baz: BuzzView)\n    private let views: Views\n    private let model: FooModelProtocol\n    private let disposeBag = RxSwift.DisposeBag()\n\n    init(observing model: FooModelProtocol, handling views: Views) {\n        self.model = model\n        self.views = views\n\n        // NOTE: Change visual by observing model's state transitions.\n        self.model\n            .didChange\n            .subscribe(onNext: { [weak self] state in\n                guard let this = self else { return }\n                switch state {\n                case .preparing:\n                    this.views.bar.text = \"preparing\"\n                case let .success(entity):\n                    this.views.bar.text = \"success \\(entity)\"\n                case let .failure(because: reason):\n                    this.views.bar.text = \"failure \\(reason)\"\n                }\n            })\n            .disposed(by: self.disposeBag)\n    }\n}\n```\n\n```swift\nclass FooController: FooControllerProtocol {\n    private let model: FooModelProtocol\n    private let view: BarView\n    private let disposeBag = RxSwift.DisposeBag()\n\n    init(\n        observing view: BarView,\n        willNotifyTo model: FooModelProtocol\n    ) {\n        self.model = model\n\n        // NOTE: Observe UI events from BarView and notify to the FooModel.\n        view.rx.tap\n            .asDriver\n            .drive(onNext: { [weak self] _ in \n                guard let this = self else { return }\n\n                this.model.doSomething()\n            })\n            .disposed(by: self.disposeBag)\n    }\n}\n```\n\n\nHow to Connect among UIViewControllers\n--------------------------------------\n\nIn this project, use Navigator class for connecting betweren 2 `UIViewControllers`.\n\n\n```swift\nclass FooViewController: UIViewController {\n    private let navigator: NavigatorProtocol\n    private let sharedModel: FooBarModelProtocol\n\n    init(\n        representing sharedModel: FooBarModelProtocol,\n        navigatingBy navigator: NavigatorProtocol\n    ) {\n        self.sharedModel = sharedModel\n        self.navigator = navigator\n        super.init(nibName: nil, bundle: nil)\n    }\n\n    required init?(coder aDecoder: NSCoder) {\n        // NOTE: We should not instantiate the ViewController by using UINibs to\n        // eliminate fields that have force unwrapping types.\n        return nil\n    }\n\n    @IBAction func buttonDidTap(sender: Any) {\n        let nextViewController = BarViewController(\n            representing: sharedModel\n        )\n        self.navigator.navigate(to: nextViewController)\n    }\n}\n```\n\nAnd also you can use `UIStoryboardSegue`, but using the `Navigator` class have two advantages:\n\n- We can implement easily and simply common behavior (eg. sending logs for analysis)\n- We can assert necessary objects at once\n\n\n\n### `Navigator` Implementation\n\n```swift\n/**\n A protocol for wrapper class of `UINavigationController#pushViewController(_:UIViewController, animated:Bool)`.\n */\nprotocol NavigatorProtocol {\n    /**\n     Push the specified UIViewController to the held UINavigationController.\n     */\n    func navigate(to viewController: UIViewController, animated: Bool)\n}\n\n\n\nclass Navigator: NavigatorProtocol {\n    private let navigationController: UINavigationController\n\n\n    init (for navigationController: UINavigationController) {\n        self.navigationController = navigationController\n    }\n\n\n    func navigate(to viewController: UIViewController, animated: Bool) {\n        self.navigationController.pushViewController(\n            viewController,\n            animated: animated\n        )\n    }\n}\n```\n\n\n\nHow to Control Global Variables\n-------------------------------\nIn this project, we control global variables by using [test doubles](http://xunitpatterns.com/Test%20Double.html); Stub and Spy.\n\n![](https://raw.githubusercontent.com/Kuniwak/TestableDesignExample/master/Documentation/Images/TestDoubles_en.png)\n\n\n### Sample code\n#### Bad Design (fragile tests)\n\n```swift\n// BAD DESIGN\nclass UserDefaultsCalculator {\n    func read10TimesValue() {\n        return UserDefaults.standard.integer(forKey: \"foo\") * 10\n    }\n\n\n    func write10TimesValue(_ value: Int) {\n        UserDefaults.standard.set(value * 10, forKey: \"foo\")\n    }\n}\n```\n\n```swift\n// In production code:\nlet calc = UserDefaultsCalculator()\nlet value = calc.read10TimesValue()\ncalc.write10TimesValue(value)\n\n\n// In the unit-test A, it is fragile :-(\nlet calc = UserDefaultsCalculator()\nUserDefaults.standard.set(1, forKey: \"foo\")\nXCTAssertEqual(calc.read10TimesValue(), 10)\n\n\n// In the unit-test B, it is also fragile :-(\nlet calc = UserDefaultsCalculator()\ncalc.write10TimesValue(1)\nXCTAssertEqual(UserDefaults.standard.integer(forKey: \"foo\"), 10)\n```\n\n\n#### Good Design (robust tests)\n```swift\n// GOOD DESIGN\nclass UserDefaultsCalculator {\n    private let readableRepository: ReadableRepositoryProtocol\n    private let writableRepository: WritableRepositoryProtocol\n\n\n    init(\n        reading readableRepository: ReadableRepositoryProtocol,\n        writing writableRepository: WritableRepositoryProtocol\n    ) {\n        self.readableRepository = readableRepository\n        self.writableRepository = writableRepository\n    }\n\n\n    func read10TimesValue() {\n        return self.readableRepository.read() * 10\n    }\n\n\n    func write10TimesValue(value: Int) {\n        self.writableRepository.write(value * 10)\n    }\n}\n\n\nprotocol ReadableRepositoryProtocol {\n    func read() -\u003e Int\n}\n\n\nclass ReadableRepository: ReadableRepositoryProtocol {\n    private let userDefaults: UserDefaults\n\n\n    init(reading userDefaults: UserDefaults) {\n        self.userDefaults = userDefaults\n    }\n\n\n    func read() -\u003e Int {\n        return self.userDefaults.integer(forKey: \"foo\")\n    }\n}\n\n\nprotocol WritableRepositoryProtocol {\n    func write(_ value: Int)\n}\n\n\nclass WritableRepository: WritableRepositoryProtocol {\n    private let userDefaults: UserDefaults\n\n\n    init(reading userDefaults: UserDefaults) {\n        self.userDefaults = userDefaults\n    }\n\n\n    func write(_ value: Int) {\n        self.userDefaults.set(value, forKey: \"foo\")\n    }\n}\n```\n\n\n```swift\n// In production code:\nlet calc = UserDefaultsCalculator(\n    reading: ReadableRepository(UserDefaults.standard),\n    writing: WirtableRepository(UserDefaults.standard)\n)\nlet value = calc.read10TimesValue()\ncalc.write10TimesValue(value)\n\n\n// In the unit-test A, it is robust, because\n// we don't touch actual UserDefaults :-D\nlet calc = UserDefaultsCalculator(\n    reading: ReadableRepositoryStub(firstValue: 1),\n    writing: WritableRepositorySpy()\n)\nXCTAssertEqual(calc.read10TimesValue(), 10)\n\n\n// In the unit-test B, it is also robust :-D\nlet spy = WritableRepositorySpy()\nlet calc = UserDefaultsCalculator(\n    reading: ReadableRepositoryStub(firstValue: 0),\n    writing: spy\n)\ncalc.write10TimesValue(1)\nXCTAssertEqual(spy.callArgs.last!, 10)\n```\n\n```swift\n// TestDoubles definitions\n\nclass ReadableRepositoryStub: ReadableRepositoryProtocol {\n    var nextValue: Int\n\n    init(firstValue: Int) {\n        self.nextValue = firstValue\n    }\n\n    func read() {\n        return self.nextValue\n    }\n}\n\n\nclass WritableRepositorySpy: WritableRepositoryProtocol {\n    private(set) var callArgs = [Int]()\n\n    func write(_ value: Int) {\n        self.callArgs.append(value)\n    }\n}\n```\n\n\n\nTesting strategy\n----------------\nWe stronlgy agree the blog entry; [\"Just Say No to More End-to-End Tests\"](https://testing.googleblog.com/2015/04/just-say-no-to-more-end-to-end-tests.html).\n\nIn this project, we use type-checking instead of other tests (unit tests and integration tests and UI tests) to get feedbacks from tests rapidly. Because type-checking is higher effictiveness than other tests.\n\n![](https://raw.githubusercontent.com/Kuniwak/TestableDesignExample/master/Documentation/Images/TestEfficiency_en.png)\n\nFor example, we can check registering `UITableViewCell` to `UITableVIew` before dequeueing by using type-checking:\n\n```swift\nclass MyCell: UITableViewCell {\n    /**\n     A class for registration token that will create after registering the cell to the specified UITableView.\n     */\n    struct RegistrationToken {\n        // Hide initializer to other objects.\n        fileprivate init() {}\n    }\n\n\n    /**\n     Registers the cell class to the specified UITableView and returns a registration token.\n     */\n    static func register(to tableView: UITableView) -\u003e RegistrationToken {\n        tableView.register(R.nib.myCell)\n        return RegistrationToken()\n    }\n\n\n    /**\n     Dequeues the cell by the specified UITableView.\n     You must have a registration token (it means you must register the cell class before dequeueing).\n     */\n    static func dequeue(\n        by tableView: UITableView,\n        for indexPath: IndexPath,\n        andMustHave token: RegistrationToken\n    ) -\u003e MyCell {\n        guard let cell = tableView.dequeueReusableCell(\n            withIdentifier: R.reuseIdentifier.myCell.identifier,\n            for: indexPath\n        ) as? MyCell else {\n            // \u003e dequeueReusableCell(withIdentifier:for:)\n            // \u003e\n            // \u003e A UITableViewCell object with the associated reuse identifier.\n            // \u003e This method always returns a valid cell.\n            // \u003e\n            // \u003e https://developer.apple.com/reference/uikit/uitableview/1614878-dequeuereusablecell\n            fatalError(\"This case must be success\")\n        }\n\n        // Configuring the cell.\n\n        return cell\n    }\n}\n```\n\nTaken together, we should follow the Test Pyramid:\n\n![Ideal test volume is extremely few UI tests and few integration tests and much unit tests and much type checkings.](https://raw.githubusercontent.com/Kuniwak/TestableDesignExample/master/Documentation/Images/TestingPyramid_en.png)\n\n\n\nReferences\n----------\n\n1. xUnit Test Patterns: http://xunitpatterns.com/index.html\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkuniwak%2Ftestabledesignexample","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkuniwak%2Ftestabledesignexample","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkuniwak%2Ftestabledesignexample/lists"}