Home > OS >  How to mock a generic implementation of a protocol?
How to mock a generic implementation of a protocol?

Time:02-21

I'm trying to do some dependency injection in order to unit test, and also make iOS live preview easier.

I have a Store protocol, and I want to use these in my SUT classes so I can pass mock implementations.

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 */ }
}

However, in my SUT class I have the following:

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>()
)

Seems like I'm stuck. I'm essentially trying to have a generic protocol, similar to a generic interface in Java. What am I missing?

UPDATE

Following @Jessy answer, I did:

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

Which solves half of the issue. The other half is how to type a variable with it:

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
  >
}

CodePudding user response:

  1. The Store<Student> syntax has never been supported, but there has been a lot of talk on the Swift forum about it lately—it may be soon.

  2. There are many other Q/A's on Stack Overflow about not being able to use a protocol with an associated type like an existential—it's finally available but only in Xcode 13.3 (beta).

It seems to me that you want things constrained like this:

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

If not, the latest syntax, available starting in Xcode 13.3, is

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
  }
}
  • Related