如何模拟协议的通用实现?

How to mock a generic implementation of a protocol?

我正在尝试进行一些依赖注入以进行单元测试,同时也使 iOS 实时预览更容易。

我有一个 Store 协议,我想在我的 SUT classes 中使用它们,这样我就可以通过模拟实现。

protocol Store {
  associatedtype AnyData
  func load() -> AnyData?
  func save(data anyData: AnyData)
}

class FileStore<AnyData: Codable>: Store {
  func load() -> AnyData? { /* Actual code that saves to a file */ }

  func save(data anyData: AnyData) { /* Actual code that saves to a file */ }
}

class MockStore<AnyData: Codable>: Store {
  func load() -> AnyData? { /* Returns mocked value for unit testing */ }

  func save(data anyData: AnyData) { /* Returns mocked value for unit testing */ }
}

但是,在我的 SUT class 中,我有以下内容:

class MyClassToBeTested {
  // THIS DOESN'T WORK
  let bookStore: Store      // ERROR: Protocol 'Store' can only be used as a generic
                            // constraint because it has Self or associated type requirements

  // DOESN'T WORK EITHER
  let studentStore: Store<Student> // ERROR: Cannot specialize non-generic type 'Store'
}



// in real app
MyClassToBeTested(
  bookStore: FileStore<Book>()
  studentStore: FileStore<Student>()
)


// in test or preview
MyClassToBeTested(
  bookStore: MockStore<Book>()
  studentStore: MockStore<Student>()
)

好像我卡住了。我本质上是在尝试拥有一个通用协议,类似于 Java 中的通用接口。我错过了什么?

更新

按照@Jessy 的回答,我做了:

class MyClassToBeTested<BookStore: Store, StudentStore: Store>
where
  BookStore.AnyData == Book,
  StudentStore.AnyData == Student
{
  let bookStore: BookStore
  let studentStore: StudentStore

这样就解决了一半的问题。另一半是如何用它键入变量:

class OtherClass {
  var sut: MyClassToBeTested   // ERROR: Generic parameter Bookstore could not be inferred


  
  var sut2: MyClassToBeTested<  // I know this isn't supported in Swift
    Store<Book>,                // but can't figure out the right syntax
    Store<Student>              // ERROR: Cannot specialize non-generic type 'Store'
  >                             // ERROR: Protocol 'Store' as a type cannot conform to the protocol itself


  var sut3: MyClassToBeTested<  // Compiles, BUT I cannot pass a
    FileStore<Book>,            // MockStore in my tests/preview
    FileStore<Student>          // so it's useless
  >
}
  1. 从未支持 Store<Student> 语法,但最近 Swift 论坛上有很多关于它的讨论——可能很快就会支持。

  2. Stack Overflow 上还有许多其他 Q/A 的内容是关于无法使用具有关联类型(如存在)的协议——它终于可用,但仅在 Xcode 13.3(测试版)。

在我看来,你想要这样的约束:

class MyClassToBeTested<BookStore: Store, StudentStore: Store>
where
  BookStore.AnyData == Book,
  StudentStore.AnyData == Student
{
  let bookStore: BookStore
  let studentStore: StudentStore

如果不是,从 Xcode 13.3 开始可用的最新语法是

class MyClassToBeTested<StudentStore: Store>
where StudentStore.AnyData == Student {
  let bookStore: any Store
  let studentStore: StudentStore

  init(
    bookStore: any Store,
    studentStore: StudentStore
  ) {
    self.bookStore = bookStore
    self.studentStore = studentStore
  }
}