Search code examples
iosswiftxcodetddxctest

Errors setting up XCTest using Swift Result type


I am learning how to write tests for my API requests but having trouble setting up my test's completion code and response model.

I tried using an instance of UserResponse (let userResponse = UserResponse() ) however it required a value for its initializer (from: Decoder) and I have no idea what goes in there. The error I get is:

"Argument type 'Decoder.Protocol' does not conform to expected type 'Decoder'"

Also I am having errors in creating the test's completion handler, I am using the new Swift Result type (Result<UserResponse, Error>). The error I get is:

"Type of expression is ambiguous without more context"

This is an @escaping function but I got an error saying to remove @escaping in the test.

Any ideas on what is wrong? I have marked the trouble code below with comments.

Thank you!

// APP CODE

class SignupViewModel: ObservableObject {

  func createAccount(user: UserSignup, completion: @escaping( Result<UserResponse, Error>) -> Void) {
    AuthService.createAccount(user: user, completion: completion)
  }  

}

struct UserSignup: Encodable {
    var username: String
    var email: String
    var password: String
}


struct UserResponse: Decodable {
    var user: User
    var token: String
}


struct User: Decodable {
   var username: String
   var email: String
   // etc
   { private enum UserKeys }
   init(from decoder: Decoder) throws { container / decode code }
}

// TEST CODE

class SignupViewModelTests: XCTestCase {
    var sut: SignupViewModel!

    override func setUpWithError() throws {
        sut = SignupViewModel() 
    }

    override func tearDownWithError() throws {
        sut = nil 
    }

    func testCreateAccount_WhenGivenSuccessfulResponse_ReturnsSuccess() {

        let userSignup = UserSignup(username: "johnsmith", email: "john@test.com", password: "abc123abc")

// WHAT GOES IN from:??

        let userResponse = UserResponse(from: Decoder) 
        
// ERROR: "Type of expression is ambiguous without more context"??

        func testCreateAccount_WhenGivenSuccessfulResponse_ReturnsSuccess() {
        //arrange
        let userSignup = UserSignup(username: "johnsmith", email: "john@test.com", password: "abc123abc")
        

        sut.createAccount(user: UserSignup, completion: ( Result <UserResponse, Error> ) -> Void ) {
            
            XCTAssertEqual(UserResponse.user.username, "johnsmith")
        }
    }
      
    }
}


Solution

  • To create a UserResponse in test code, call its synthesized initializer, not the decoder initializer. Something like:

    let userResponse = UserResponse(user: User("Chris"), token: "TOKEN")
    

    And to create a closure in test code, you need to give it code. Completion closures in tests have one job, to capture how they were called. Something like:

    var capturedResponses: [Result<UserResponse, Error>] = []
    sut.createAccount(user: UserSignup, completion: { capturedResponses.append($0) })
    

    This captures the Result that createAccount(user:completion:) sends to the completion handler.

    …That said, it looks like your view model is directly calling a static function that makes a service call. If the test runs, will it create an actual account somewhere? Or do you have some boundary in place that we can't see?

    Instead of directly testing createAccount(user:completion:), what you probably want to test is:

    • That a certain action (signing up) will attempt to create an account for a given user—but not actually do so.
    • Upon success, the view model will do one thing.
    • Upon failure, the view model will do another thing.

    If my assumptions are correct, I can show you how to do this.