All Projects → Kuniwak → TestableDesignExample

Kuniwak / TestableDesignExample

Licence: other
Sample App to learn a testable design (Smalltalk flavored MVC)

Programming Languages

swift
15916 projects

Projects that are alternatives of or similar to TestableDesignExample

puremvc-swift-multicore-framework
PureMVC MultiCore Framework for Swift
Stars: ✭ 17 (-78.75%)
Mutual labels:  design-patterns, mvc-architecture
express-mvc-pattern
Example nodejs using express implementation design pattern using mvc architecture.
Stars: ✭ 52 (-35%)
Mutual labels:  design-patterns, mvc-architecture
spark-design-system
Spark Design System
Stars: ✭ 101 (+26.25%)
Mutual labels:  design-patterns
mvc-tree
🌳 A chronological visualization of the family of MVC patterns.
Stars: ✭ 40 (-50%)
Mutual labels:  design-patterns
here-we-go
Contains hundreds of samples for learning Go.
Stars: ✭ 93 (+16.25%)
Mutual labels:  design-patterns
Design-Patterns
A collection of popular design patterns with video and text explanations
Stars: ✭ 159 (+98.75%)
Mutual labels:  design-patterns
design-pattern-examples-in-python
UML model and code examples of design patterns for Python. The model is created with Astah.
Stars: ✭ 27 (-66.25%)
Mutual labels:  design-patterns
angular-atomic-app
Angular 5 app built on atomic web design principles
Stars: ✭ 12 (-85%)
Mutual labels:  design-patterns
design-patterns-for-humans-cn
Design patterns for humans 中文版 - 对设计模式超简单的解释
Stars: ✭ 2,432 (+2940%)
Mutual labels:  design-patterns
design-patterns-php7
Design Patterns com PHP 7: Desenvolva com as melhores soluções. Repositório com os exemplos de código do livro. Repository with the code examples in the book.
Stars: ✭ 51 (-36.25%)
Mutual labels:  design-patterns
enterprise-applications-patterns
Collection of enterprise application patterns
Stars: ✭ 17 (-78.75%)
Mutual labels:  design-patterns
vala
design-patterns-for-humans in Vala (programming language)
Stars: ✭ 52 (-35%)
Mutual labels:  design-patterns
Study-Notes
🤔💡 😃 😎 Algorithms, Data Structures, Design Patterns
Stars: ✭ 56 (-30%)
Mutual labels:  design-patterns
design-patterns
Simple examples of Design Patterns with PHP Examples
Stars: ✭ 75 (-6.25%)
Mutual labels:  design-patterns
Slovo
Искони бѣ Слово - already in production at https://слово.бг
Stars: ✭ 17 (-78.75%)
Mutual labels:  mvc-architecture
ezyfox
Java library supports for reflection, generic, annotations parsing, bean management and object binding
Stars: ✭ 14 (-82.5%)
Mutual labels:  design-patterns
objproxies
Proxies and wrappers for ordinary Python objects
Stars: ✭ 18 (-77.5%)
Mutual labels:  design-patterns
OOP-Design-Patterns
MET CS665 - OOP Design Patterns Code Examples
Stars: ✭ 74 (-7.5%)
Mutual labels:  design-patterns
php-design-patterns
Learn how to implement the most important Design Patterns into your PHP application. This project uses PHP 8.1. it has examples for each Pattern and an Article explaining how to use them step by step, their advantages, and disadvantages.
Stars: ✭ 151 (+88.75%)
Mutual labels:  design-patterns
DotNETCarRental
Daily car rental simulation with ASP.NET.
Stars: ✭ 13 (-83.75%)
Mutual labels:  design-patterns

Testable design example for iOS Apps

Build Status

This is a sample App to learn testable design.

You can learn the following things by reading this implementation:

  • How to make loose coupling for testing
  • How to decouple global variables
  • How to use type-checking as a test

Architecture

This App adopt Smalltalk flavored MVC (it is not Apple MVC). Smalltalk flavored MVC is a architecture that can test easily. You may know major architectures such as MVVM, MVP, Flux and VIPER, but also Smalltalk MVC can make loose coupling.

While there are a lot of architectures, but they share a common important things that we should do. So, learning this implementation is still worth the candle if you choose other architectures.

Sample Code

In our approach, we create a Xib file per UIViewController. And all UIViewControllers have a initializer that require models.

And we should create ViewBindings and Controllers and connect them to the given Model when UIViewController#loadView() is called.

Concrete implementation is below:

class FooViewController: UIViewController {
    private var model: FooModelProtocol
    private var viewBinding: FooViewBindingProtocol?
    private var controller: FooControllerProtocol?

    init(model: FooModelProtocol) {
        self.model = model
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder aDecoder: NSCoder) {
        // NOTE: In this project, we do not want to restore the VC.
        return nil
    }

    // Connect Model and ViewBinding, Controller.
    override func loadView() {
        let rootView = FooRootView()
        self.view = rootView

        let controller = FooController(
            observing: rootView.barView,
            willNotifyTo: self.model
        )
        self.controller = controller

        self.viewBinding = FooViewBinding(
            observing: self.model,
            handling: (
                bar: rootView.barView,
                baz: rootView.bazView
            )
        )
        self.viewBinding.delegate = controller
    }
}
// FooModel is a state-machine that can transit to FooModelState.
// Notify change events to others via an observable `didChange` when
// API was successfully done or failed.
class FooModel: FooModelProtocol {
    private let repository: FooRepositoryProtocol
    private let stateVariable: RxSwift.Variable<FooModelState>

    /// An Observable that will notify events when the internal state is changed.
    var didChange: RxSwift.Observable<FooModelState> {
        return self.stateVariable.asObservable()
    }

    /// The current state of the model.
    var currentState: FooModelState {
        get { return self.stateVariable.value }
        set { self.stateVariable.value = newValue }
    }

    init(
        startingWith initialState: FooModelState,
        fetchingVia repository: FooRepositoryProtocol
    ) {
        self.stateVariable = RxSwift.Variable<FooModelState>(initialState)
        self.repository = repository
    }

    func doSomething() {
        switch self.currentState {
        case .preparing:
            // NOTE: Prevent duplicated calls.
            return

        case .success, .failure:
            self.currentState = .preparing

            self.repository
                .doSomething()
                .then { entity in 
                    self.currentState = .success(entity)
                }
                .catch { error in
                    self.currentState = .failure(
                        because: .unspecified(debugInfo: "\(error)")
                    )
                }
        }
    }
}


// States that FooModel can transit to.
enum FooModelState {
    case preparing
    case success(Entity)
    case failure(because: Reason)

    enum Reason {
        case unspecified(debugInfo: String)
    }
}
class FooViewBinding: FooViewBindingProtocol {
    typealias Views = (bar: BarView, baz: BuzzView)
    private let views: Views
    private let model: FooModelProtocol
    private let disposeBag = RxSwift.DisposeBag()

    init(observing model: FooModelProtocol, handling views: Views) {
        self.model = model
        self.views = views

        // NOTE: Change visual by observing model's state transitions.
        self.model
            .didChange
            .subscribe(onNext: { [weak self] state in
                guard let this = self else { return }
                switch state {
                case .preparing:
                    this.views.bar.text = "preparing"
                case let .success(entity):
                    this.views.bar.text = "success \(entity)"
                case let .failure(because: reason):
                    this.views.bar.text = "failure \(reason)"
                }
            })
            .disposed(by: self.disposeBag)
    }
}
class FooController: FooControllerProtocol {
    private let model: FooModelProtocol
    private let view: BarView
    private let disposeBag = RxSwift.DisposeBag()

    init(
        observing view: BarView,
        willNotifyTo model: FooModelProtocol
    ) {
        self.model = model

        // NOTE: Observe UI events from BarView and notify to the FooModel.
        view.rx.tap
            .asDriver
            .drive(onNext: { [weak self] _ in 
                guard let this = self else { return }

                this.model.doSomething()
            })
            .disposed(by: self.disposeBag)
    }
}

How to Connect among UIViewControllers

In this project, use Navigator class for connecting betweren 2 UIViewControllers.

class FooViewController: UIViewController {
    private let navigator: NavigatorProtocol
    private let sharedModel: FooBarModelProtocol

    init(
        representing sharedModel: FooBarModelProtocol,
        navigatingBy navigator: NavigatorProtocol
    ) {
        self.sharedModel = sharedModel
        self.navigator = navigator
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder aDecoder: NSCoder) {
        // NOTE: We should not instantiate the ViewController by using UINibs to
        // eliminate fields that have force unwrapping types.
        return nil
    }

    @IBAction func buttonDidTap(sender: Any) {
        let nextViewController = BarViewController(
            representing: sharedModel
        )
        self.navigator.navigate(to: nextViewController)
    }
}

And also you can use UIStoryboardSegue, but using the Navigator class have two advantages:

  • We can implement easily and simply common behavior (eg. sending logs for analysis)
  • We can assert necessary objects at once

Navigator Implementation

/**
 A protocol for wrapper class of `UINavigationController#pushViewController(_:UIViewController, animated:Bool)`.
 */
protocol NavigatorProtocol {
    /**
     Push the specified UIViewController to the held UINavigationController.
     */
    func navigate(to viewController: UIViewController, animated: Bool)
}



class Navigator: NavigatorProtocol {
    private let navigationController: UINavigationController


    init (for navigationController: UINavigationController) {
        self.navigationController = navigationController
    }


    func navigate(to viewController: UIViewController, animated: Bool) {
        self.navigationController.pushViewController(
            viewController,
            animated: animated
        )
    }
}

How to Control Global Variables

In this project, we control global variables by using test doubles; Stub and Spy.

Sample code

Bad Design (fragile tests)

// BAD DESIGN
class UserDefaultsCalculator {
    func read10TimesValue() {
        return UserDefaults.standard.integer(forKey: "foo") * 10
    }


    func write10TimesValue(_ value: Int) {
        UserDefaults.standard.set(value * 10, forKey: "foo")
    }
}
// In production code:
let calc = UserDefaultsCalculator()
let value = calc.read10TimesValue()
calc.write10TimesValue(value)


// In the unit-test A, it is fragile :-(
let calc = UserDefaultsCalculator()
UserDefaults.standard.set(1, forKey: "foo")
XCTAssertEqual(calc.read10TimesValue(), 10)


// In the unit-test B, it is also fragile :-(
let calc = UserDefaultsCalculator()
calc.write10TimesValue(1)
XCTAssertEqual(UserDefaults.standard.integer(forKey: "foo"), 10)

Good Design (robust tests)

// GOOD DESIGN
class UserDefaultsCalculator {
    private let readableRepository: ReadableRepositoryProtocol
    private let writableRepository: WritableRepositoryProtocol


    init(
        reading readableRepository: ReadableRepositoryProtocol,
        writing writableRepository: WritableRepositoryProtocol
    ) {
        self.readableRepository = readableRepository
        self.writableRepository = writableRepository
    }


    func read10TimesValue() {
        return self.readableRepository.read() * 10
    }


    func write10TimesValue(value: Int) {
        self.writableRepository.write(value * 10)
    }
}


protocol ReadableRepositoryProtocol {
    func read() -> Int
}


class ReadableRepository: ReadableRepositoryProtocol {
    private let userDefaults: UserDefaults


    init(reading userDefaults: UserDefaults) {
        self.userDefaults = userDefaults
    }


    func read() -> Int {
        return self.userDefaults.integer(forKey: "foo")
    }
}


protocol WritableRepositoryProtocol {
    func write(_ value: Int)
}


class WritableRepository: WritableRepositoryProtocol {
    private let userDefaults: UserDefaults


    init(reading userDefaults: UserDefaults) {
        self.userDefaults = userDefaults
    }


    func write(_ value: Int) {
        self.userDefaults.set(value, forKey: "foo")
    }
}
// In production code:
let calc = UserDefaultsCalculator(
    reading: ReadableRepository(UserDefaults.standard),
    writing: WirtableRepository(UserDefaults.standard)
)
let value = calc.read10TimesValue()
calc.write10TimesValue(value)


// In the unit-test A, it is robust, because
// we don't touch actual UserDefaults :-D
let calc = UserDefaultsCalculator(
    reading: ReadableRepositoryStub(firstValue: 1),
    writing: WritableRepositorySpy()
)
XCTAssertEqual(calc.read10TimesValue(), 10)


// In the unit-test B, it is also robust :-D
let spy = WritableRepositorySpy()
let calc = UserDefaultsCalculator(
    reading: ReadableRepositoryStub(firstValue: 0),
    writing: spy
)
calc.write10TimesValue(1)
XCTAssertEqual(spy.callArgs.last!, 10)
// TestDoubles definitions

class ReadableRepositoryStub: ReadableRepositoryProtocol {
    var nextValue: Int

    init(firstValue: Int) {
        self.nextValue = firstValue
    }

    func read() {
        return self.nextValue
    }
}


class WritableRepositorySpy: WritableRepositoryProtocol {
    private(set) var callArgs = [Int]()

    func write(_ value: Int) {
        self.callArgs.append(value)
    }
}

Testing strategy

We stronlgy agree the blog entry; "Just Say No to More End-to-End Tests".

In 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.

For example, we can check registering UITableViewCell to UITableVIew before dequeueing by using type-checking:

class MyCell: UITableViewCell {
    /**
     A class for registration token that will create after registering the cell to the specified UITableView.
     */
    struct RegistrationToken {
        // Hide initializer to other objects.
        fileprivate init() {}
    }


    /**
     Registers the cell class to the specified UITableView and returns a registration token.
     */
    static func register(to tableView: UITableView) -> RegistrationToken {
        tableView.register(R.nib.myCell)
        return RegistrationToken()
    }


    /**
     Dequeues the cell by the specified UITableView.
     You must have a registration token (it means you must register the cell class before dequeueing).
     */
    static func dequeue(
        by tableView: UITableView,
        for indexPath: IndexPath,
        andMustHave token: RegistrationToken
    ) -> MyCell {
        guard let cell = tableView.dequeueReusableCell(
            withIdentifier: R.reuseIdentifier.myCell.identifier,
            for: indexPath
        ) as? MyCell else {
            // > dequeueReusableCell(withIdentifier:for:)
            // >
            // > A UITableViewCell object with the associated reuse identifier.
            // > This method always returns a valid cell.
            // >
            // > https://developer.apple.com/reference/uikit/uitableview/1614878-dequeuereusablecell
            fatalError("This case must be success")
        }

        // Configuring the cell.

        return cell
    }
}

Taken together, we should follow the Test Pyramid:

Ideal test volume is extremely few UI tests and few integration tests and much unit tests and much type checkings.

References

  1. xUnit Test Patterns: http://xunitpatterns.com/index.html
Note that the project description data, including the texts, logos, images, and/or trademarks, for each open source project belongs to its rightful owner. If you wish to add or remove any projects, please contact us at [email protected].