简体   繁体   中英

How to test equality of Swift enums with associated values

I want to test the equality of two Swift enum values. For example:

enum SimpleToken {
    case Name(String)
    case Number(Int)
}
let t1 = SimpleToken.Number(123)
let t2 = SimpleToken.Number(123)
XCTAssert(t1 == t2)

However, the compiler won't compile the equality expression:

error: could not find an overload for '==' that accepts the supplied arguments
    XCTAssert(t1 == t2)
    ^~~~~~~~~~~~~~~~~~~

Do I have do define my own overload of the equality operator? I was hoping the Swift compiler would handle it automatically, much like Scala and Ocaml do.

Swift 4.1+

As @jedwidz has helpfully pointed out, from Swift 4.1 (due to SE-0185 , Swift also supports synthesizing Equatable and Hashable for enums with associated values.

So if you're on Swift 4.1 or newer, the following will automatically synthesize the necessary methods such that XCTAssert(t1 == t2) works. The key is to add the Equatable protocol to your enum.

enum SimpleToken: Equatable {
    case Name(String)
    case Number(Int)
}
let t1 = SimpleToken.Number(123)
let t2 = SimpleToken.Number(123)

Before Swift 4.1

As others have noted, Swift doesn't synthesize the necessary equality operators automatically. Let me propose a cleaner (IMHO) implementation, though:

enum SimpleToken: Equatable {
    case Name(String)
    case Number(Int)
}

public func ==(lhs: SimpleToken, rhs: SimpleToken) -> Bool {
    switch (lhs, rhs) {
    case let (.Name(a),   .Name(b)),
         let (.Number(a), .Number(b)):
      return a == b
    default:
      return false
    }
}

It's far from ideal — there's a lot of repetition — but at least you don't need to do nested switches with if-statements inside.

Implementing Equatable is an overkill IMHO. Imagine you have complicated and large enum with many cases and many different parameters. These parameters will all have to have Equatable implemented, too. Furthermore, who said you compare enum cases on all-or-nothing basis? How about if you are testing value and have stubbed only one particular enum parameter? I would strongly suggest simple approach, like:

if case .NotRecognized = error {
    // Success
} else {
    XCTFail("wrong error")
}

... or in case of parameter evaluation:

if case .Unauthorized401(_, let response, _) = networkError {
    XCTAssertEqual(response.statusCode, 401)
} else {
    XCTFail("Unauthorized401 was expected")
}

Find more elaborate description here: https://mdcdeveloper.wordpress.com/2016/12/16/unit-testing-swift-enums/

enum MyEnum {
    case none
    case simple(text: String)
    case advanced(x: Int, y: Int)
}

func ==(lhs: MyEnum, rhs: MyEnum) -> Bool {
    switch (lhs, rhs) {
    case (.none, .none):
        return true
    case let (.simple(v0), .simple(v1)):
        return v0 == v1
    case let (.advanced(x0, y0), .advanced(x1, y1)):
        return x0 == x1 && y0 == y1
    default:
        return false
    }
}

There seems no compiler generated equality operator for enums, nor for structs.

“If you create your own class or structure to represent a complex data model, for example, then the meaning of “equal to” for that class or structure is not something that Swift can guess for you.” [1]

To implement equality comparison, one would write something like:

@infix func ==(a:SimpleToken, b:SimpleToken) -> Bool {
    switch(a) {

    case let .Name(sa):
        switch(b) {
        case let .Name(sb): return sa == sb
        default: return false
        }

    case let .Number(na):
        switch(b) {
        case let .Number(nb): return na == nb
        default: return false
        }
    }
}

[1] See "Equivalence Operators" at https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/AdvancedOperators.html#//apple_ref/doc/uid/TP40014097-CH27-XID_43

Here's another option. It's mainly the same as the others except it avoids the nested switch statements by using the if case syntax. I think this makes it slightly more readable(/bearable) and has the advantage of avoiding the default case altogether.

enum SimpleToken: Equatable {
    case Name(String)
    case Number(Int)
}
extension SimpleToken {
    func isEqual(st: SimpleToken)->Bool {
        switch self {
        case .Name(let v1): 
            if case .Name(let v2) = st where v1 == v2 { return true }
        case .Number(let i1): 
            if case .Number(let i2) = st where i1 == i2 { return true }
        }
        return false
    }
}

func ==(lhs: SimpleToken, rhs: SimpleToken)->Bool {
    return lhs.isEqual(rhs)
}

let t1 = SimpleToken.Number(1)
let t2 = SimpleToken.Number(2)
let t3 = SimpleToken.Name("a")
let t4 = SimpleToken.Name("b")

t1 == t1  // true
t1 == t2  // false
t3 == t3  // true
t3 == t4  // false
t1 == t3  // false

I'm using this simple workaround in unit test code:

extension SimpleToken: Equatable {}
func ==(lhs: SimpleToken, rhs: SimpleToken) -> Bool {
    return String(stringInterpolationSegment: lhs) == String(stringInterpolationSegment: rhs)
}

It uses string interpolation to perform the comparison. I would not recommend it for production code, but it's concise and does the job for unit testing.

Another option would be to compare the string representations of the cases:

XCTAssert(String(t1) == String(t2))

For example:

let t1 = SimpleToken.Number(123) // the string representation is "Number(123)"
let t2 = SimpleToken.Number(123)
let t3 = SimpleToken.Name("bob") // the string representation is "Name(\"bob\")"

String(t1) == String(t2) //true
String(t1) == String(t3) //false

Expanding on mbpro's answer, here's how I used that approach to check for equality of swift enums with associated values with some edge cases.

Of course you can do a switch statement, but sometimes it's nice to just check for one value in one line. You can do it like so:

// NOTE: there's only 1 equal (`=`) sign! Not the 2 (`==`) that you're used to for the equality operator
// 2nd NOTE: Your variable must come 2nd in the clause

if case .yourEnumCase(associatedValueIfNeeded) = yourEnumVariable {
  // success
}

If you want to compare 2 conditions in the same if clause, you need to use the comma instead of the && operator:

if someOtherCondition, case .yourEnumCase = yourEnumVariable {
  // success
}

Another approach using if case with commas, which works in Swift 3:

enum {
  case kindOne(String)
  case kindTwo(NSManagedObjectID)
  case kindThree(Int)

  static func ==(lhs: MyEnumType, rhs: MyEnumType) -> Bool {
    if case .kindOne(let l) = lhs,
        case .kindOne(let r) = rhs {
        return l == r
    }
    if case .kindTwo(let l) = lhs,
        case .kindTwo(let r) = rhs {
        return l == r
    }
    if case .kindThree(let l) = lhs,
        case .kindThree(let r) = rhs {
        return l == r
    }
    return false
  }
}

This is how I wrote in my project. But I can't remember where I got the idea. (I googled just now but didn't see such usage.) Any comment would be appreciated.

t1 and t2 are not numbers, they are instances of SimpleTokens with values associated.

You can say

var t1 = SimpleToken.Number(123)

You can then say

t1 = SimpleToken.Name(“Smith”) 

without a compiler error.

To retrieve the value from t1, use a switch statement:

switch t1 {
    case let .Number(numValue):
        println("Number: \(numValue)")
    case let .Name(strValue):
        println("Name: \(strValue)")
}

the 'advantage' when compare to accepted answer is, that there is no 'default' case in 'main' switch statement, so if you extend your enum with other cases, the compiler will force you to update the rest of code.

enum SimpleToken: Equatable {
    case Name(String)
    case Number(Int)
}
extension SimpleToken {
    func isEqual(st: SimpleToken)->Bool {
        switch self {
        case .Name(let v1):
            switch st {
            case .Name(let v2): return v1 == v2
            default: return false
            }
        case .Number(let i1):
            switch st {
            case .Number(let i2): return i1 == i2
            default: return false
            }
        }
    }
}


func ==(lhs: SimpleToken, rhs: SimpleToken)->Bool {
    return lhs.isEqual(rhs)
}

let t1 = SimpleToken.Number(1)
let t2 = SimpleToken.Number(2)
let t3 = SimpleToken.Name("a")
let t4 = SimpleToken.Name("b")

t1 == t1  // true
t1 == t2  // false
t3 == t3  // true
t3 == t4  // false
t1 == t3  // false

From Swift 4.1 just add Equatable protocol to your enum and use XCTAssert or XCTAssertEqual :

enum SimpleToken : Equatable {
    case Name(String)
    case Number(Int)
}
let t1 = SimpleToken.Number(123)
let t2 = SimpleToken.Number(123)
XCTAssertEqual(t1, t2) // OK

You can compare using switch

enum SimpleToken {
    case Name(String)
    case Number(Int)
}

let t1 = SimpleToken.Number(123)
let t2 = SimpleToken.Number(123)

switch(t1) {

case let .Number(a):
    switch(t2) {
        case let . Number(b):
            if a == b
            {
                println("Equal")
        }
        default:
            println("Not equal")
    }
default:
    println("No Match")
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM