Créer des API qui sont plus facilement testées
Imaginez que vous développez un nouveau package Swift et que vous disposez d’une API publique, afin que les utilisateurs puissent interagir avec votre package. Vous permettez aux consommateurs de votre emballage d’injecter quelques propriétés dans le init
une fonction. Vous injectez également quelques autres entités internes destinées à être exposées en dehors de votre package pour écrire des tests unitaires ultérieurement, et rendre les entités de votre package moins couplées les unes aux autres.
Une fois que vous avez fait cela et cliqué sur build, vous obtiendrez l’erreur suivante dans Xcode :
Initializer cannot be declared public because its parameter uses an internal type
Nous obtenons cette erreur car, dans nos API publiques public init
méthode, nous ne pouvons pas injecter nos entités internes comme nous le voulons.
Dans cet article, nous verrons comment convenience init
peut nous aider à réaliser la testabilité de nos API publiques dans nos packages qui utilisent des entités internes.
Nous allons créer un package simple pour stocker les transactions. Il aura une classe publique avec laquelle les consommateurs du package pourront interagir. Il aura également un public Transaction
model et une énumération pour configurer la durée de stockage des transactions.
Déclarons le modèle et l’énumération de configuration comme suit :
public struct Transaction
// some transaction related properties (irrelevant for the scope of the article)
}
public enum TransactionPersistenceDuration {
case day
case week
case month
}
Déclarez maintenant notre API publique avec son protocole associé :
public protocol TransactionStoring {
func store(transaction: Transaction)
}public final class TransactionStorage: TransactionStoring {
private let persistenceDuration: TransactionPersistenceDuration
public init(persistenceDuration: TransactionPersistenceDuration) {
self.persistenceDuration = persistenceDuration
}
public func store(transaction: Transaction) {
// transaction storage implementation
}
}
Jusqu’ici tout va bien. Nous avons maintenant notre paquet, et son public init
est suffisant pour nos besoins.
Nous aimerions maintenant introduire une nouvelle entité, un logger, spécifique à nos besoins pour le package. Mais on veut aussi l’injecter dans le init
afin que nous puissions écrire des tests unitaires pour l’interaction de TransactionStorage
avec ça.
Nous allons déclarer le logger interne avec son protocole associé comme suit :
protocol AccessLogging {
func logAccess()
}struct AccessLogger: AccessLogging {
func logAccess() {
print("user info provider accessed")
}
}
Puisque le protocole et l’implémentation sont internes, nous ne pouvons pas simplement les mettre dans notre public init
méthode. Au lieu de cela, nous allons marquer notre public init
comme public convenience init
puis mettre en œuvre un interne init
méthode pour injecter nos services, loggers et entités que nous aimerions simuler plus tard pour des tests.
Les méthodes init refactorisées et les TransactionStorage
l’ensemble sera désormais le suivant :
public final class TransactionStorage: TransactionStoring {private let persistenceDuration: TransactionPersistenceDuration
private let accessLogger: AccessLogging
public convenience init(persistenceDuration: TransactionPersistenceDuration) {
self.init(persistenceDuration: persistenceDuration, accessLogger: AccessLogger())
}
init(persistenceDuration: TransactionPersistenceDuration, accessLogger: AccessLogging) {
self.persistenceDuration = persistenceDuration
self.accessLogger = accessLogger
}
public func store(transaction: Transaction) {
accessLogger.logAccess()
// proceed with storage
}
}
Chaque fois qu’un consommateur de notre package initialise une instance de TransactionStorage
ils déclencheront le convenience init
en fournissant un persistenceDuration
. Cela appellera l’interne init
pour initialiser correctement le stockage en utilisant AccessLogger
.
Mais maintenant, avec ces changements, nous pourrons écrire des tests unitaires sur notre API publique sans avoir à marquer en interne AccessLogger
comme public
.
Un test unitaire simple peut maintenant être écrit comme suit :
import XCTest
@testable import TransactionStoragefinal class AccessLoggerMock: AccessLogging {
var didCallLogAccess = false
func logAccess() {
didCallLogAccess = true
}
}
final class TransactionStorageTests: XCTestCase {
var accessLogger: AccessLoggerMock!
override func setUp() {
super.setUp()
accessLogger = .init()
}
override func tearDown() {
accessLogger = nil
super.tearDown()
}
func test_access_is_logged_whenever_a_transaction_is_stored() {
let sut = TransactionStorage(persistenceDuration: .day, accessLogger: accessLogger)
sut.store(transaction: .init())
XCTAssertTrue(accessLogger.didCallLogAccess)
}
}
En utilisant un initialiseur de commodité public, combiné à un initialiseur interne, nous avons implémenté l’API publique de notre package de manière testable sans exposer les entités internes au monde extérieur. Bien que simple, il s’agit d’une technique puissante à appliquer aux packages et frameworks Swift. Cela peut aider à réaliser une modularisation appropriée d’un projet iOS complexe.