Skip to content

GeekTree0101/EffectiveInteractorProject

Repository files navigation

Ready to Test Driven Development

STEP 1: Make an UseCase Models

Design a use case base model for the scene. Even if some DTO(Data Transfer Object) don't have any properties, you'll be mindful of extensibility.

enum CardModels {
  
  enum FetchCard {  // Fetch Card UseCase
    
    // Request DTO(Data Transfer Object)
    // from controller to interactor
    struct Request {
    
    }
    
    // Response DTO(Data Transfer Object)
    // from interactor to presenter
    struct Response {
      
      var card: Card?
      var error: Error?
    }
    
    // Response DTO(Data Transfer Object)
    // from presenter to controller or view
    struct ViewModel {
      
    }
  }

STEP 2: DataStore

Stores the models commonly used on the scene. also you can pass stored data to other scene

protocol CardDataStore: class {
  
  var cardID: Int? { get set }
  var card: Card? { get set }
}

STEP 3: Interactor Logic

protocol CardDataStore: class {
  
  var cardID: Int? { get set }
  var card: Card? { get set }
}

protocol CardInteractorLogic: class {
  
  func fetchCard(request: CardModels.FetchCard.Request)
}

STEP 4: Worker

For some reason I use PromiseKit instead of RxSwift.

  • Almost of RESTFul service != event stream. (exception: periodic pooling)
  • In CleanSwift Case, interactor doesn't needs disposeBag
  • Easy to see and predictable such as logic flow, threading and so on.
import PromiseKit

class CardWorker {

  public func getCard(id: Int) -> Promise<Card> {
    
    return Promise<Card> { seal in
      seal.fulfill(Card.init(id: 1))
    }
  }
}

STEP 5: Interactor Implementation

class CardInteractor: CardDataStore {
  
  // MARK: - DataStore
  var cardID: Int?
  var card: Card?
  
  public var worker: CardWorker = CardWorker.init()
}

extension CardInteractor: CardInteractorLogic {
  
  func fetchCard(request: CardModels.FetchCard.Request) {
    
  }
}

Let's start testing

STEP 1: Interactor Test File

Test target is Interactor. You just put interactor initialization logic into setUp:

import XCTest
import Nimble

import PromiseKit

@testable import EffectiveInteractorProject

class CardInteractorTests: XCTestCase {

  // MARK: - Props
  var interactor: CardInteractor!
  
  override func setUp() {
    // Put setup code here. This method is called before the invocation of each test method in the class.
    self.interactor = CardInteractor.init()
  }

}

STEP 2: Spy Worker

Before designing spy worker, you need to understand Test Double first. What is Test double

import XCTest
import Nimble

import PromiseKit

@testable import EffectiveInteractorProject

class CardInteractorTests: XCTestCase {

  // Spy Worker
  class Spy_CardWorker: CardWorker {
    
    var getCardCalled: Int = 0
    
    override func getCard(id: Int) -> Promise<Card> {
      getCardCalled += 1
      return .value(Card.init(id: -1))
    }
  }
  
  // MARK: - Props
  var interactor: CardInteractor!
  
  override func setUp() {
    // Put setup code here. This method is called before the invocation of each test method in the class.
    self.interactor = CardInteractor.init()
  }

}

you can design getCardCalled property as Boolean. In Robert Cecil Martin's Clean code, he recommend use an Integer type property to logic calling test. and you just return a successful dummy response.

STEP 3: Spy Test

Af first, you can write extension of CardInteractorTests with marking use case.

IMO, Marking use case test name helps to understand test logic. It also helps your colleagues.

// MARK: - Fetch Card

extension CardInteractorTests {

}

and next, you have to define expectations such as fetch success & fetch failed

// MARK: - Fetch Card

extension CardInteractorTests {

  // success
  func testFetchCardShouldBeFailedWithoutCardID() {
  
  }
  
  // failed
  func testFetchCardShouldBeSuccessWithCardID() {
  
  }
}

and you just follow BDD

befre: Fetch Card BDD Spec

Fetch Card Must be Success!

Given: worker is defined and cardID isn't nil
When: interactor.fetchCard called
Then: worker.getCard must be called once.

after

  func testFetchCardShouldBeFailedWithoutCardID() {
    // given
    let worker = Spy_CardWorker() // worker is defined 
    self.interactor.worker = worker
    
    self.interactor.cardID = nil // cardID isn't nil
    
    // when
    self.interactor.fetchCard(request: CardModels.FetchCard.Request()) // interactor.fetchCard called
    
    // then
    expect(worker.getCardCalled).toEventually(equal(0)) // worker.getCard must be called once.
  }

with Presenter

Create a Presenter logic

you don't needs presetner logic implementation. it's just boundrary interface.

protocol CardPresenterLogic: class {
  
  func presentFetchCard(response: CardModels.FetchCard.Response)
}

Spy Presenter for testing

Go back to the test file again.

import XCTest
import Nimble

import PromiseKit

@testable import EffectiveInteractorProject

class CardInteractorTests: XCTestCase {

  // Spy Worker
  class Spy_CardWorker: CardWorker {
    
    var getCardCalled: Int = 0
    
    override func getCard(id: Int) -> Promise<Card> {
      getCardCalled += 1
      return .value(Card.init(id: -1))
    }
  }
  
  // MARK: - Props
  var interactor: CardInteractor!
  
  override func setUp() {
    // Put setup code here. This method is called before the invocation of each test method in the class.
    self.interactor = CardInteractor.init()
  }

}

For presenter spy testing, we needs two objects Stub worker & Spy presenter

Spy Presenter

Designing spy presenter is same with Spy Worker.

  // Spy Presenter
  class Spy_Presenter: CardPresenterLogic {
    
    var presentFetchCardCalled: Int = 0

    func presentFetchCard(response: CardModels.FetchCard.Response) {
      presentFetchCardCalled += 1
    }
  }

Stub Worker

Before, you already had a spy test about worker. Now you needs just stub worker for presenter testing.

  class Stub_CardWorker: CardWorker {

    var getCardValue: Promise<Card>!

    override func getCard(id: Int) -> Promise<Card> {
      return getCardValue
    }
  }

output

class CardInteractorTests: XCTestCase {

  // Spy Presenter
  class Spy_Presenter: CardPresenterLogic {
    
    var presentFetchCardCalled: Int = 0

    func presentFetchCard(response: CardModels.FetchCard.Response) {
      presentFetchCardCalled += 1
    }
  }

  // Stub Worker
  class Stub_CardWorker: CardWorker {

    var getCardValue: Promise<Card>!

    override func getCard(id: Int) -> Promise<Card> {
      return getCardValue
    }
  }

  // Spy Worker
  class Spy_CardWorker: CardWorker {
    
    var getCardCalled: Int = 0
    
    override func getCard(id: Int) -> Promise<Card> {
      getCardCalled += 1
      return .value(Card.init(id: -1))
    }
  }
  
  // MARK: - Props
  var interactor: CardInteractor!
  
  override func setUp() {
    // Put setup code here. This method is called before the invocation of each test method in the class.
    self.interactor = CardInteractor.init()
  }

}

Now let's make test code base on BDD

// MARK: - FetchCard

extension CardInteractorTests {

  // ...

  func testFetchCardShouldBeCalledPresenterOnSuccess() {
    // given
    let presenter = Spy_CardPresenter()
    let worker = Stub_CardWorker()
    
    self.interactor.presenter = presenter
    self.interactor.worker = worker
    
    self.interactor.cardID = 1

    // HERE: Stubbing!
    worker.getCardValue = Promise.value(Card.init(id: 1))
    
    // when
    self.interactor.fetchCard(request: CardModels.FetchCard.Request())
    
    // then
    expect(presenter.presentFetchCardCalled).toEventually(equal(1))
  }
  
  func testFetchCardShouldBeCalledPresenterOnError() {
    // given
    let presenter = Spy_CardPresenter()
    let worker = Stub_CardWorker()
    
    self.interactor.presenter = presenter
    self.interactor.worker = worker
    
    self.interactor.cardID = 1

    // HERE: Stubbing!
    worker.getCardValue = Promise.init(error: NSError.init(domain: "test", code: -1, userInfo: nil))
    
    
    // when
    self.interactor.fetchCard(request: CardModels.FetchCard.Request())
    
    // then
    expect(presenter.presentFetchCardCalled).toEventually(equal(1))
  }
  
  func testFetchCardShouldNotBeCalledPresenterWithoutCardID() {
    // given
    let presenter = Spy_CardPresenter()
    let worker = Stub_CardWorker()
    
    self.interactor.presenter = presenter
    self.interactor.worker = worker
    
    self.interactor.cardID = nil
    
    // when
    self.interactor.fetchCard(request: CardModels.FetchCard.Request())
    
    // then
    expect(presenter.presentFetchCardCalled).toEventually(equal(0))
  }
}

Pagination Flow (Coming soon)

About

Effective Interactor Project (CleanSwift)

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published