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 {
}
}
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 }
}
protocol CardDataStore: class {
var cardID: Int? { get set }
var card: Card? { get set }
}
protocol CardInteractorLogic: class {
func fetchCard(request: CardModels.FetchCard.Request)
}
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))
}
}
}
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) {
}
}
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()
}
}
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.
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.
}
you don't needs presetner logic implementation. it's just boundrary interface.
protocol CardPresenterLogic: class {
func presentFetchCard(response: CardModels.FetchCard.Response)
}
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
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
}
}
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))
}
}