Relay
Relay 是一个动态依赖注入框架,它使用 IoC(控制反转)容器并在此基础上构建,使集成测试变得可靠、专注和高效。
通过传递一系列命令行参数,驱动程序可以指示系统注入特定的依赖项(通常是存根),以保持集成预定并最大限度地提高有效断言的数量。例如,当通过 XCTest 运行 iOS UI 测试时,我们可以指示应用程序注入模拟的后端服务,以便我们的 UI 测试仅验证前端行为和布局。
本框架旨在捍卫 测试金字塔。对于大多数应用程序,这允许将 UI 测试 与 端到端测试 分离。
要求
- iOS 8.0+ / macOS 10.10+ / tvOS 9.0+ / watchOS 2.0+ / Linux
- Xcode 10+ / Swift 4.2+
安装
Carthage
将 Relay
添加到您的 Cartfile
github 'mindbody/Relay'
Swift 包管理器
将 Relay
添加到您的 Package.swift
// swift-tools-version:4.2
import PackageDescription
let package = Package(
dependencies: [
.package(url: "https://github.com/mindbody/Relay.git", from: "0.2.1")
]
)
入门
虽然 Relay 提供了强大的工具,但开发者需要按照一定结构来组织代码以使用这些工具。请参阅最佳实践了解如何实现这一点。
Relay 以 IoC 容器为基础。有关详细信息,请参阅Relay 架构。
创建依赖注册表
DependencyRegistry 负责在应用程序中将具体的工厂注册到可解析的类型中。每个注册表中都通过与 DependencyContainers(IoC 容器)交互来实现。在驱动程序可以注入其依赖项之前,您必须设置您的项目以进行依赖项注入。通常是单个注册表就足够了。
/// DefaultDependencyRegistry.swift
final class DefaultDependencyRegistry: DependencyRegistryType {
func registerDependencies() throws {
DependencyContainer.global.register(MyBackendServiceType.self) { _ in
MyBackendService()
}
/// Recursive dependencies are lazily resolved
DependencyContainer.global.register(MyViewControllerDataStoreType.self) { container in
MyViewControllerDataStore(backendService: container.resolve(MyBackendServiceType.self))
}
/// etc.
}
}
注册依赖项
您的默认 DependencyRegistryType
应在程序开始时执行。在一个应用程序中,这通常属于您的 AppDelegate
/// AppDelegate.swift
final class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
registerDependencies();
/// Configure app...
}
private func registerDependencies() {
do {
let defaultRegistry = DefaultDependencyRegistry()
try defaultRegistry.registerDependencies()
}
catch {
fatalError(error.localizedDescription)
}
}
}
使用已注册的依赖项
如果您遵循了我们的最佳实践,那么这应该相当简单。对于手动解析或注入的依赖项,应将其替换为来自IoC容器的解析
/// SampleViewController.swift
final class SampleViewController {
private func showNextViewController() {
let container = DependencyContainer.global
let dataStore = container.resolve(MyViewControllerDataStoreType.self)
let viewController = MyViewController(dataStore: dataStore)
navigationController?.pushViewController(viewController, animated: true)
}
}
动态依赖项
Relay为动态依赖项提供了一个自定义类型的DependencyRegistry
,称为DynamicDependencyRegistry。它是通过从提供的DynamicDependencyIndex解析一系列可识别的类型和工厂来实现的,默认情况下为DynamicDependencyIndex.shared
。在大多数情况下,您不需要直接与DynamicDependencyRegistry
交互。
索引类型和工厂
在Relay中,抽象类型通过一个唯一的DependencyTypeKey来识别,而具体的工厂通过一个唯一的DependencyFactoryKey来识别。为了对动态注册的类型和工厂进行索引,您必须向目标DynamicDependencyIndex
提供一个包含DependencyTypeIndexable和DependencyFactoryIndexable的列表。类似于DependencyRegistryType
,这些类型定义了一个类型和工厂的列表,除了它们不注册它们。
由于类型是通用的,您可能只需要一个单个的DependencyTypeIndexable
/// DefaultDependencyTypeIndex.swift
final class DefaultDependencyTypeIndex: DependencyTypeIndexable {
let index: [DependencyTypeKey: Any.Type] = [
.myViewControllerDataStore: MyViewControllerDataStoreType.self,
.myBackendService: MyBackendServiceType.self,
/// etc.
]
}
类型与工厂是一对多,所以您可能需要创建多个实现DependencyFactoryIndexable的类型。一种组织方法是根据在每个DependencyFactoryKey
中定义的相同目的分开工厂索引,如果遵循我们的最佳实践。这个目的可以描述一个功能单元,描述特定的行为,或识别特定的测试或测试套件
/// TestSuite12345DependencyFactoryIndex.swift
final class TestSuite12345DependencyFactoryIndex: DependencyFactoryIndexable {
let index: [DependencyFactoryKey: (DependencyContainer) -> Any] = TestSuite12345DependencyFactoryIndex.makeIndex()
private static func makeIndex() -> [DependencyFactoryKey: (DependencyContainer) -> Any] {
let backendServiceFactory: (DependencyContainer) -> Any = { _ in
let backendServiceStub = MyBackendServiceStub()
backendServiceStub.responseUnderTest = [SampleData(), SampleData()]
return backendServiceStub
}
return [
.testCase56789BackendService: backendServiceFactory
]
}
}
命令行注入
综合以上,Relay提供工具通过命令行参数来注册这些动态依赖项。这意味着,如UI测试运行器之类的驱动程序可以指示目标应用程序注入特定的依赖项。
这些参数被格式化为如下
[program-run] [-d, --dependency] type=<type>,factory=<factory>[,scope=<scope>][,lifecycle=<lifecyle>]
传递给--dependency
的值被称为DependencyInjectionInstruction。输入参数描述了
type
:类型标识符,应与目标DependencyTypeKey
匹配factory
:工厂标识符,应与目标DependencyFactoryKey
匹配scope
:作用域标识符,如果未指定则为“global”lifecycle
:依赖生命周期类型(singleton|transient),如果未指定则为“singleton”
这些参数被提供给一个InjectDependenciesArgumentParser,该解析器与DynamicDependencyRegistry
通信。对于Xcode项目,您需要更新您的AppDelegate
/// AppDelegate.swift
final class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
registerDependencies();
/// Configure app...
}
private func registerDependencies() {
do {
let defaultRegistry = DefaultDependencyRegistry()
try defaultRegistry.registerDependencies()
#if DEBUG
/// Release builds likely should avoid promoting this behavior
DynamicDependencyIndex.shared.add(DefaultDependencyTypeIndex())
DynamicDependencyIndex.shared.add(TestSuite12345DependencyFactoryIndex())
DynamicDependencyIndex.shared.add(AnotherTestFactoryIndex())
/// etc.
try CommandLine.parse(with: [InjectDependenciesArgumentParser()])
#endif
}
catch {
fatalError(error.localizedDescription)
}
}
}
驱动测试
为了效率和清晰的格式,Relay提供了一个LaunchArgumentBuilder,这对于构建要将依赖指令发送到命令行的指令列表非常有用。LaunchArgumentBuilder
接受一个符合LaunchArgument的类型的列表。对于大多数常见情况,Relay提供了DependencyInstructionLaunchArgument。
例如,如果我们的由Xcode构建的应用程序有一套需要存根服务的UI测试,我们可以将其组织如下
final class Suite12345Tests: XCTestCase {
func testCase56789ShowsCorrectNumberOfCells() throws {
let injectionInstructionArgument = DependencyInstructionLaunchArgument(type: .myBackendService, factory: .testCase56789BackendService)
let builder = LaunchArgumentBuilder(arguments: [injectionInstructionArgument])
let app = XCUIApplication()
app.launchArguments = builder.build()
app.launch()
/// Add assertions, etc.
}
}
这只是一个粗略示例,但将端到端测试转换为聚焦UI测试的起点。
高级主题
依赖生命周期
默认情况下,Relay依赖项通过其类型映射的具体工厂懒惰创建。由于它们只创建一次,每个DependencyContainer
一次,它们具有LifecycleType
类型的singleton
。
虽然这通常对于服务来说是足够的,但在应用底层进行更精细的DI时,更喜欢短期依赖项。这些被称为transient
依赖项,只能依赖注册时进行配置
final class DefaultDependencyRegistry: DependencyRegistryType {
func registerDependencies() throws {
/// This factory will be called every time MyViewControllerDataStoreType is resolved
DependencyContainer.global.register(MyViewControllerDataStoreType.self, lifecycle: .transient) { _ in
MyViewControllerDataStore()
}
/// etc.
}
依赖项生命周期还可以通过命令行进行配置。有关lifecycle
参数的文档,请参阅命令行注入。
容器作用域
由于Relay使用类型映射进行强类型依赖解析,一旦具体的工厂无法满足所有调用者,你的代码可能会变得有点丑陋。对于这些情况,我们可以使用嵌套容器,通过DependencyContainerScope
进行键控。
通常,随着依赖变得更加细粒度,容器作用域就越有用。例如,我们可能有一个视图控制器,其展示的数据可以消耗多个不同的服务。这些细节可以通过为视图控制器注入多个数据源来处理。
/// MyDependencyScopes.swift
extension DependencyContainerScope {
static var useCase1 = DependencyContainerScope("useCase1")
static var useCase2 = DependencyContainerScope("useCase2")
}
/// MyScopedDependencyRegistry.swift
MyScopedDependencyRegistry: DependencyRegistryType {
func registerDependencies() throws {
DependencyContainer.useCase1.register(MyViewControllerDataStoreType.self, lifecycle: .transient) { _ in
MyViewControllerFirstUseCaseDataStore()
}
DependencyContainer.useCase2.register(MyViewControllerDataStoreType.self, lifecycle: .transient) { _ in
MyViewControllerSecondUseCaseDataStore()
}
/// etc.
}
}
当作用域容器无法解析类型时,解析将回退到全局容器。作用域和全局容器共同形成依赖图。为了强制执行简单的依赖图,并且为了在Relay架构中保持一致性,依赖作用域只扩展到全局容器之下的一层。有关详细信息,请参阅Relay架构。
依赖作用域也可以通过命令行进行配置。有关关于作用域
参数的文档,请参阅命令行注入。
致谢
Relay由MINDBODY,Inc.拥有,并由我们的贡献者持续维护。