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