В 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()
. Поскольку у нас нет модульных тестов для этого изменения, выполните ручной тест, чтобы убедиться, что мы ничего не сломали.
Теперь наш шов на месте, поэтому мы можем начать писать тесты. Существует два типа написания тестов:
- Правильно ли мы вызываем
Connector
? - Что происходит, когда вызывается метод делегата?
Давайте сделаем 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