skip to Main Content

Suppose I have:

protocol MyError: Error, Equatable {
  var errorDispalyTitle: String { get }
  var errorDisplayMessage: String { get }
}

enum ContentState {
  case .loading
  case .error(any MyError)
  case .contentLoaded
}

If i were to implement Equatable in ContentState so I can compare during unit tests I end up with a problem because I have to compare two any MyError types which are boxed and might be of two different underlying types.

extension ContentState: Equatable {
  static func == (lhs: ContentState, rhs: ContentState) -> Bool {
    switch (lhs, rhs) {
    case (.loading, .loading):
      return true
    case (.contentLoaded, .contentLoaded):
      return true
    case (.error(let lhsError), .error(let rhsError)):
      // TODO: have to compare if both underlying types are match and then if they are equal in value
    default:
      return false
    }
  }
}

How do I do this?

I tried lifting the generic from the existential type there to the type (e.g. ContentState<Error: MyError>) which lets it compile when implementing equatable as it knows how to infer the type, but the problem is for whichever class uses that enum it doesnt matter which type is receiving of it, only that is any type of it, and if I don’t implement the any existential it starts requiring the generic to be propagated up the chain.

3

Answers


  1. You can write a wrapper that wraps an Equatable, and wrap the LHS and RHS errors before comparing.

    // resembling a similar behaviour to AnyHashable...
    class AnyEquatable: Equatable {
        let value: Any
        private let equals: (Any) -> Bool
    
        init<E: Equatable>(_ value: E) {
            self.value = value
            self.equals = { type(of: $0) == type(of: value) && $0 as? E == value }
        }
    
        static func == (lhs: AnyEquatable, rhs: AnyEquatable) -> Bool {
            lhs.equals(rhs.value)
        }
    }
    

    Then in your switch you can do:

    case (.error(let lhsError), .error(let rhsError)):
      return AnyEquatable(lhsError) == AnyEquatable(rhsError)
    

    Note that if MyError inherits from Hashable instead of Equatable, you can use the builtin AnyHashable instead of writing your own AnyEquatable.

    Login or Signup to reply.
  2. As of Swift 5.7, Swift automatically “opens” an existential when you pass it as an argument of generic type. The implicit self argument can be opened (in fact Swift has always opened the self argument), and Swift can open multiple arguments in a single invocation. So we can write an isEqual(to:) function that compares any Equatable to any other Equatable like this:

    
    extension Equatable {
        func isEqual<B: Equatable>(to b: B) -> Bool {
            return b as? Self == self
        }
    }
    

    And then we can complete your ContentState conformance like this:

    extension ContentState: Equatable {
      static func == (lhs: ContentState, rhs: ContentState) -> Bool {
        switch (lhs, rhs) {
        case (.loading, .loading):
          return true
        case (.contentLoaded, .contentLoaded):
          return true
        case (.error(let lhsError), .error(let rhsError)):
            return lhsError.isEqual(to: rhsError)
        default:
          return false
        }
      }
    }
    
    Login or Signup to reply.
  3. Four unit testing, I would recommend extending your type with Bool‘s as much as possible:

    extension ContentState {
        var isLoading: Bool {
            if case .loading = self { return true }
            else { return false }
        }
        
        var isError: Bool {
            if case .error = self { return true }
            else { return false }
        }
        
        var isContentLoaded: Bool {
            if case .contentLoaded = self { return true }
            else { return false }
        }
    }
    

    , as I assume you’re mostly interested in testing if some object properly updates its state.

    This makes unit tests easier to write and read, and they better transmit the intent.

    If you want to also explicitly check the error, you could make use of generics:

    extension ContentState {
        func wrappedError<T: MyError>() -> T? {
            if case let .error(error as T) = self { return error }
            else { return nil }
        }
    }
    

    , and assert on that:

    XCTAssertEqual(state.wrappedError(), TestError.error1)
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search