skip to Main Content

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: "[email protected]", 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: "[email protected]", password: "abc123abc")
        

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

2

Answers


  1. Okay there are multiple issues with the way you are unit testing your code, I wont go in detailing all of the issues, but what you need in gist to make it work is

    func testCreateAccount() {
            //given
            let userSignup = UserSignup(username: "johnsmith", email: "[email protected]", password: "abc123abc")
            let signupExpectation = expectation(description: "Sign up should succeed")
    
            //when
            sut.createAccount(user: userSignup) { (result) in
                //then
                switch result {
                case .success(let response):
                    XCTAssertTrue(response.user.email == "[email protected]", "User email should be [email protected]")
                    XCTAssertTrue(response.user.username == "johnsmith", "Username should be johnsmith")
                    XCTAssertTrue(response.token != "", "Token should not be empty")
    
                case .failure(let error):
                    XCTFail("Authorization should not fail, failed with (error.localizedDescription)")
                }
            }
    
            wait(for: [signupExpectation], timeout: 10.0)
        }
    

    You are trying to test an asynchronous API call so you cant test it with synchronous XCAssert statements you need expectation. There are ways to make the asynchronous API calls synchronous and test it with straight forward XCAssert statements if you are using third party libraries like RxTest. I think its out of scope for you considering you are still newbie to unit test with Swift.

    You can read all about expectation here : Apple doc

    I have followed a simple code structuring of Given, when and then as indicated by comment in answer, its a pretty neat way to arrange your code if you are not using any kind of third party library to write descriptive unit test like Quick and Nimble

    There is a beautiful article on unit test using plain old XCTest framework in raywnderlich tutorial here.

    Finally, as a closing remark, we dont make an actual API call to test our code in unit test, we write fakes and stubs to test out our code.

    Assume if you end up writing unit test for your entire project and your CI/CD system starts running entire test suite for all your PRs and builds, and your unit test ends up making actual API call, amount of time that will be wasted in making an actual API call will increase your test suite run time rapidly making the release process a nightmare. Also testing backend API is not the intention of your unit test, hence avoid actual API call using mocks, fakes and stubs read about it 🙂

    Unit test provided above is just to show you how to use expectation and test asynchronous API and definitely wont cover all possible cases 🙂

    Login or Signup to reply.
  2. 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.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search