Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
Tags
- ios
- HIG
- Observable
- SWIFT
- Human interface guide
- 리펙토링
- UITextView
- 애니메이션
- tableView
- MVVM
- swift documentation
- rxswift
- UICollectionView
- map
- uitableview
- collectionview
- swiftUI
- clean architecture
- ribs
- uiscrollview
- 스위프트
- 클린 코드
- Clean Code
- Xcode
- Protocol
- 리팩토링
- Refactoring
- combine
- RxCocoa
- 리펙터링
Archives
- Today
- Total
김종권의 iOS 앱 개발 알아가기
[iOS - Swift] 1. 유닛 테스트 방법 - Dependency Injection (@Injected) 주입 구조 본문
iOS 응용 (swift)
[iOS - Swift] 1. 유닛 테스트 방법 - Dependency Injection (@Injected) 주입 구조
jake-kim 2022. 12. 10. 22:361. 유닛 테스트 방법 - Dependency Injection (@Injected) 주입 구조 <
2. 유닛 테스트 방법 - Quick과 Nimble을 이용한 테스트 코드 구현 방법
3. 유닛 테스트 방법 - RxExpect를 이용한 Rx관련 비동기 테스트 코드 구현 방법
번외) 유닛 테스트 방법 - XCTest와 RxSwift만을 이용한 비동기 테스트 구현 방법
일반적인 DI 방법
- AccessToken을 관리하는 AccessTokenService가 있을 때, 이것을 사용하는 쪽에서는 구현체에 의존하지 않고 protocol에 의존하게 만들도록 구현
- 사용하는쪽 ViewModel에서 AccessTokenServiceType에 의존
- 구현체에 의존하고 있지 않고 인터페이스에 의존하고 있으므로, 구현체를 바꾸어서 테스트가 가능
protocol AccessTokenServiceType {
}
class AccessTokenService: AccessTokenServiceType {
}
class SomeViewModel {
let accessTokenService: AccessTokenServiceType
init(accessTokenService: AccessTokenServiceType) {
self.accessTokenService = accessTokenService
}
}
- 테스트 시 AccessTokenServiceType를 준수하는 하나의 MockAccessToken이라는 것을 만들어서 사용
class MockAccessToken: AccessTokenServiceType {
}
let viewModel = SomeViewModel(accessTokenService: MockAccessToken())
- 위 방법의 문제점
- initializer에 현재 accessTokenService만 있지만 앞으로 여러개의 서비스가 생겨난다면 일일이 모두 init 구문에 선언되고, 초기화 하는 코드가 다 생겨나는 단점이 존재
- 대안 아이디어
- init에서 초기화하는 코드를 일일이 작성 안하고, service를 아래처럼 선언만 해서 사용하는 방법 (+ test 가능한 구조)
- 주입하는것은 앱 켜질때 한번만 주입해주고 (싱글톤), 사용하는 쪽에서는 property wrapper로 런타임시에 인스턴스를 싱글톤 or mock 인스턴스에 접근하도록 구현
class SomeViewModel {
@Injected let accessTokenService: AccessTokenServiceType
@Injected let authService: AuthServiceType
}
property wrapper를 통한 구현 방법
* 방법은 해당 블로그에서 참고
- @Injected 구현 - dependencyStore의 인서턴스를 가지고 있는 property wrapper
- dependency는 injected를 사용하여 생성한 인스턴스들을 모두 가지고 있는 컨테이너
// https://medium.com/streamotion-tech-blog/magic-dependency-injection-in-swift-70476c7743ec
@propertyWrapper
public class Injected<T> {
private var storage: T?
private let dependencyStore: DependencyStore
public init() {
dependencyStore = DependencyStore.shared
}
public var wrappedValue: T {
if let storage = storage { return storage }
let object: T = dependencyStore.resolve()
storage = object
return object
}
}
- DependencyStore
- register로 인스턴스 등록 (store라는 딕셔너리에 기록)
- resolve로 인스턴스 획득
- 일반 싱글톤 정의와는 다르게, shared는 let이 아닌 var로 선언하고, init도 private이 아닌 internal로 설정
- 핵심은 store 프로퍼티 - [String: Any] 타입이 아닌 [String: () -> Any]로 선언한 이유는 lazy하게 인스턴스들이 동작되도록 하기 위함
- 클로저의 특성 - 실행 지연이 가능
// https://medium.com/streamotion-tech-blog/magic-dependency-injection-in-swift-70476c7743ec
public class DependencyStore {
#if DEBUG
public static var shared = DependencyStore()
#else
public static let shared = DependencyStore()
#endif
public init() {}
/// A map of `identifier(for:)` to initializers
private var store: [Identifier: () -> Any] = [:]
func resolve<T>() -> T {
let id = identifier(for: T.self)
guard let initializer = store[id] else {
fatalError("Could not resolve for \(T.self) - did you forget to `DependencyStore.register(_:)` a concrete type?")
}
guard let value = initializer() as? T else {
// Never happens due to the register function being generic - this is needed only because `store.value` is `Any`
fatalError("Could not cast \(initializer()) to \(T.self)")
}
return value
}
/// Registers a concrete dependency against protocol `T`.
/// ```
/// DependencyStore.register(Something(), for: SomethingType.self)
/// ```
public func register<T>(_ dependency: @escaping @autoclosure () -> T, for type: T.Type) {
let id = identifier(for: T.self)
store[id] = dependency
}
}
- 앱 실행시 최초에 dependency를 초기화하는 registerAll() 구현
- mock 데이터 생성을 위한 execute 메소드 구현
- SingleTon의 취약점인, thread safe하지 않으므로 mock 데이터를 주입할때 serial queue를 이용하여 sync하게 접근하도록 구현
// https://medium.com/streamotion-tech-blog/magic-dependency-injection-in-swift-70476c7743ec
#if DEBUG
public extension DependencyStore {
static let syncingQueue = DispatchQueue(label: "Dependencies.syncer")
/// In a unit test, we need `@Injected` to read from self rather than the default `DependencyStore.shared`
/// This does `DependencyStore.shared = self` and blocks all queues until `initializations` has been performed.
/// Therefore `@Injected` items inside of `initializations` are guaranteed to initialise and maintain a reference to `self` which it will later use if needed.
@discardableResult
func execute<ReturnedValues>(_ initializations: @escaping () -> ReturnedValues) -> ReturnedValues {
var anyReturnedValues: ReturnedValues!
DependencyStore.syncingQueue.sync {
let defaultInstance = DependencyStore.shared
DependencyStore.shared = self
anyReturnedValues = initializations()
DependencyStore.shared = defaultInstance
}
return anyReturnedValues
}
}
#endif
사용하는 쪽
- service를 정의할 때도 싱글톤으로 구현
public protocol AccessTokenServiceType {
func getAccessToken() -> String?
}
public class AccessTokenService: AccessTokenServiceType {
static let shared: AccessTokenService = AccessTokenService()
public func getAccessToken() -> String? {
"access-token"
}
}
- registerAll이라는 메소드 구현 (새로운 서비스를 만들때마다 여기에 추가)
extension DependencyStore {
func registerAll() {
register(AccessTokenService.shared, for: AccessTokenServiceType.self)
}
}
- AppDelegate에서 registerAll 실행
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
DependencyStore.shared.registerAll()
return true
}
}
- 사용하는 쪽에서는 @Injected와 함께 service 프로퍼티를 선언하여 따로 init 코드 없이 바로 사용할 수 있는 구조
class ViewController: UIViewController {
@Injected private var accessTokenService: AccessTokenServiceType
override func viewDidLoad() {
super.viewDidLoad()
print(accessTokenService.getAccessToken())
}
}
* 테스트 방법은 다음 포스팅 글, Quick과 Nimble을 이용한 테스트 코드 구현 참고
* 전체 코드: https://github.com/JK0369/ExDI
* 참고
https://medium.com/streamotion-tech-blog/magic-dependency-injection-in-swift-70476c7743ec
'iOS 응용 (swift)' 카테고리의 다른 글
Comments