Как макетировать классы внешнего фреймворка с делегатами в iOS? - PullRequest
2 голосов
/ 30 января 2020

Я работаю в iOS приложении под названием ConnectApp и использую фреймворк под названием Connector. Теперь Connector framework завершает фактическую задачу соединения с устройствами BLE и сообщает моему приложению вызывающей стороны (т.е. ConnectApp) о результатах запроса соединения через ConnectionDelegate. Давайте рассмотрим пример кода,

ConnectApp - хост-приложение

class ConnectionService: ConnectionDelegate {

    func connect(){
        var connector = Connector()
        connector.setDelegate(self)
        connector.connect()
    }

    func onConnected(result: ConnectionResult) {
        //connection result
    }
}

Connector Framework

public class ConnectionResult {
    // many complicated custom variables
}

public protocol ConnectionDelegate {
      func onConnected(result: ConnectionResult)
}

public class Connector {

   var delegate: ConnectionDelegate?

   func setDelegate(delegate: ConnectionDelegate) {
       self.delegate = delegate
   }

   func connect() {
        //…..
        // result = prepared from framework
        delegate?.onConnected(result)
   }
}

Проблема

Иногда разработчики не имеют устройства BLE, и мы надо издеваться над коннектором слоя каркаса. В случае простых классов (то есть с более простыми методами) мы могли бы использовать наследование и смоделировать Connector с MockConnector, который мог бы переопределить нижние задачи и вернуть статус из класса MockConnector. Но когда мне нужно иметь дело с ConnectionDelegate, который возвращает сложный объект. Как я могу решить эту проблему?

Обратите внимание, что фреймворк не предоставляет интерфейсы классов, скорее, нам нужно найти способ обойти конкретные объекты, такие как, Connector, ConnectionDelegate et c.

Обновление 1:

Пытаясь применить ответ Сквиггса, поэтому я создал протокол, например,

protocol ConnectorProtocol: Connector {
    associatedType MockResult: ConnectionResult
}

И затем внедрил реал / макет, используя шаблон стратегии, например,

class ConnectionService: ConnectionDelegate {

    var connector: ConnectorProtocol? // Getting compiler error
    init(conn: ConnectorProtocol){
        connector = conn
    }

    func connect(){
        connector.setDelegate(self)
        connector.connect()
    }

    func onConnected(result: ConnectionResult) {
        //connection result
    }
}

Теперь я получаю сообщение об ошибке компилятора,

Протокол 'ConnectorProtocol' может использоваться только как ограничение c, поскольку он имеет требования к Self или связанный тип

Что я делаю не так?

Ответы [ 2 ]

3 голосов
/ 01 февраля 2020

В Swift, самый чистый способ создать Seam (разделение, которое позволяет нам заменять различные реализации) - это определить протокол. Это требует изменения производственного кода для взаимодействия с протоколом вместо жестко закодированной зависимости, такой как Connector().

. Сначала создайте протокол. Swift позволяет нам присоединять новые протоколы к существующим типам.

protocol ConnectorProtocol {}

extension Connector: ConnectorProtocol {}

Это определяет протокол, изначально пустой. И он говорит, что Connector соответствует этому протоколу.

Что принадлежит протоколу? Вы можете обнаружить это, изменив тип var connector с неявного Connector на явный ConnectorProtocol:

var connector: ConnectorProtocol = Connector()

Xcode будет жаловаться на неизвестные методы. Удовлетворите это, скопировав подпись каждого необходимого метода в протокол. Судя по вашему примеру кода, это может быть:

protocol ConnectorProtocol {
    func setDelegate(delegate: ConnectionDelegate)
    func connect()
}

Поскольку Connector уже реализует эти методы, расширение протокола выполнено.

Далее нам нужен способ для производственного кода использовать Connector, но для тестового кода заменить другую реализацию протокола. Так как ConnectionService создает новый экземпляр при вызове connect(), мы можем использовать замыкание как простой фабричный метод. Рабочий код может предоставить замыкание по умолчанию (создание Connector), например, со свойством замыкания:

private let makeConnector: () -> ConnectorProtocol

Установите его значение, передав аргумент инициализатору. Инициализатор может указать значение по умолчанию, поэтому он принимает значение Connector, если не указано иное:

init(makeConnector: (() -> ConnectorProtocol) = { Connector() }) {
    self.makeConnector = makeConnector
    super.init()
}

В connect(), вызовите makeConnector() вместо Connector(). Поскольку у нас нет модульных тестов для этого изменения, выполните ручной тест, чтобы убедиться, что мы ничего не сломали.

Теперь наш шов на месте, поэтому мы можем начать писать тесты. Существует два типа написания тестов:

  1. Правильно ли мы вызываем Connector?
  2. Что происходит, когда вызывается метод делегата?

Давайте сделаем Mock Object, чтобы проверить первую часть. Важно, чтобы мы вызывали setDelegate(delegate:) перед вызовом connect(), поэтому давайте сделаем фиктивную запись всех вызовов в массиве. Массив дает нам возможность проверить порядок вызовов. Вместо того, чтобы тестовый код проверял массив вызовов (выступая в роли тестового шпиона, который просто записывает материал), ваш тест будет чище, если мы сделаем этот полноценный фиктивный объект - то есть он выполнит свою собственную проверку.

final class MockConnector: ConnectorProtocol {
    private enum Methods {
        case setDelegate(ConnectionDelegate)
        case connect
    }

    private var calls: [Methods] = []

    func setDelegate(delegate: ConnectionDelegate) {
        calls.append(.setDelegate(delegate))
    }

    func connect() {
        calls.append(.connect)
    }

    func verifySetDelegateThenConnect(
        expectedDelegate: ConnectionDelegate,
        file: StaticString = #file,
        line: UInt = #line
    ) {
        if calls.count != 2 {
            fail(file: file, line: line)
            return
        }
        guard case let .setDelegate(delegate) = calls[0] else {
            fail(file: file, line: line)
            return
        }
        guard case .connect = calls[1] else {
            fail(file: file, line: line)
            return
        }
        if expectedDelegate !== delegate {
            XCTFail(
                "Expected setDelegate(delegate:) with \(expectedDelegate), but was \(delegate)",
                file: file,
                line: line
            )
        }
    }

    private func fail(file: StaticString, line: UInt) {
        XCTFail("Expected setDelegate(delegate:) followed by connect(), but was \(calls)", file: file, line: line)
    }
}

(Это бизнес с обходом file и line? Это делает так, что при любом тестовом сбое будет сообщаться строка, которая вызывает verifySetDelegateThenConnect(expectedDelegate:), а не строка, которая вызывает XCTFail(_).)

Вот как вы могли бы использовать это в ConnectionServiceTests:

func test_connect_shouldMakeConnectorSettingSelfAsDelegateThenConnecting() {
    let mockConnector = MockConnector()
    let service = ConnectionService(makeConnector: { mockConnector })

    service.connect()

    mockConnector.verifySetDelegateThenConnect(expectedDelegate: service)
}

Это касается первого типа теста. Для второго типа нет необходимости проверять, что Connector вызывает делегата. Вы знаете, что это так, и это вне вашего контроля. Вместо этого напишите тест для непосредственного вызова метода делегата. (Вы все равно захотите, чтобы он сделал MockConnector, чтобы предотвратить любые вызовы действительного Connector).

func test_onConnected_withCertainResult_shouldDoSomething() {
    let service = ConnectionService(makeConnector: { MockConnector() })
    let result = ConnectionResult(…) // Whatever you need

    service.onConnected(result: result)

    // Whatever you want to verify
}

Эти методы более подробно описаны в iOS Unit Testing на примере: Советы и методы XCTest с использованием Swift https://pragprog.com/book/jrlegios/ios-unit-testing-by-example

1 голос
/ 30 января 2020

Вы можете попробовать

protocol MockConnector: Connector {
    associatedType MockResult: ConnectionResult
}

Затем для каждого соединителя, который вам нужно смоделировать, определите конкретный класс, который соответствует этому соединителю

class SomeMockConnector: MockConnector {
    struct MockResult: ConnectionResult {
        // Any mocked variables for this connection result here 
    }

    // implement any further requirements from the Connector class
    var delegate: ConnectionDelegate?

    func connect() {
        // initialise your mock result with any specific data
        let mockResult = MockResult()
        delegate?.onConnected(mockResult)
    }
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...