0

I have a login view controller that user Almofire library to get the response. I do the unit test on that controller but the test always fail. I think because take time to response.

My test case:

override func setUp() {

    super.setUp()
    continueAfterFailure = false
    let vc = UIStoryboard(name: "Main", bundle: nil)
    controllerUnderTest = vc.instantiateViewController(withIdentifier: "LoginVC") as! LoginViewController
    controllerUnderTest.loadView()

}

override func tearDown() {
    // Put teardown code here. This method is called after the invocation of each test method in the class.
    controllerUnderTest = nil
    super.tearDown()
}

func testLoginWithValidUserInfo() {
    controllerUnderTest.email?.text = "raghad"
    controllerUnderTest.pass?.text = "1234"
    controllerUnderTest.loginButton?.sendActions(for: .touchUpInside)
    XCTAssertEqual(controllerUnderTest.lblValidationMessage?.text , "logged in successfully")
}

I try to use:

waitForExpectations(timeout: 60, handler: nil)

But I got this error:

caught "NSInternalInconsistencyException"

almofire function in login presenter :

    func sendRequest(withParameters parameters: [String : String]) {
    Alamofire.request(LOGINURL, method: .post, parameters: parameters).validate ().responseJSON { response in
        debugPrint("new line : \(response)" )
        switch response.result {
        case .success(let value):
            let userJSON = JSON(value)
            self.readResponse(data: userJSON)
        case .failure(let error):
            print("Error \(String(describing: error))")
            self.delegate.showMessage("* Connection issue ")

        }
        self.delegate.removeLoadingScreen()
        //firebase log in
        Auth.auth().signIn(withEmail: parameters["email"]!, password: parameters["pass"]!) { [weak self] user, error in
            //guard let strongSelf = self else { return }
            if(user != nil){
                print("login with firebase")

            }
            else{
                print("eroor in somthing")
            }
            if(error != nil){
                print("idon now")
            }
            // ...
        }
    }

}

func readResponse(data: JSON) {
    switch data["error"].stringValue  {
    case "true":
        self.delegate.showMessage("* Invalid user name or password")
    case "false":
        if  data["state"].stringValue=="0" {
            self.delegate.showMessage("logged in successfully")

        }else {
            self.delegate.showMessage("* Inactive account")
        }
    default:

        self.delegate.showMessage("* Connection issue")

    }
}

How can I solve this problem? :(

Raghad ak
  • 15
  • 5
  • The first thing is to make sure your expectations are fulfilled. You probably have some completion handler in your login view controller. In your tests, wait for that handler to fire and call `expectation.fulfill()`. – alanpaivaa Apr 07 '19 at 13:24
  • i didn't understand you :( – Raghad ak Apr 07 '19 at 13:27
  • Well, in your login view controller you make a call to Alamofire, which runs Asynchronously. Something like `Alamofire.request ... .response { ...`. You have to make sure call `expectation.fulfill()` when the code in inside the response closure executes. If you can post the Alamofire call in your login view controller I can help you out more. – alanpaivaa Apr 07 '19 at 13:30
  • If you don't fulfill the expectation your get stuck and will never pass. – alanpaivaa Apr 07 '19 at 13:31
  • thank you so much for help . i post my almofire function to understand :( – Raghad ak Apr 07 '19 at 13:38

2 Answers2

1

Hi @Raghad ak, welcome to Stack Overflow .

Your guess about the passage of time preventing the test to succeed is correct.

Networking code is asynchronous. After the test calls .sendActions(for: .touchUpInside) on your login button it moves to the next line, without giving the callback a chance to run.

Like @ajeferson's answer suggests, in the long run I'd recommend placing your Alamofire calls behind a service class or just a protocol, so that you can replace them with a double in the tests.

Unless you are writing integration tests in which you'd be testing the behaviour of your system in the real world, hitting the network can do you more harm than good. This post goes more into details about why that's the case.

Having said all that, here's a quick way to get your test to pass. Basically, you need to find a way to have the test wait for your asynchronous code to complete, and you can do it with a refined asynchronous expectation.

In your test you can do this:

expectation(
  for: NSPredicate(
    block: { input, _ -> Bool in
      guard let label = input as? UILabel else { return false }
        return label.text == "logged in successfully"
      }
    ),
    evaluatedWith: controllerUnderTest.lblValidationMessage,
    handler: .none
)

controllerUnderTest.loginButton?.sendActions(for: .touchUpInside)

waitForExpectations(timeout: 10, handler: nil)

That expectation will run the NSPredicate on a loop, and fulfill only when the predicate returns true.

mokagio
  • 16,391
  • 3
  • 51
  • 58
0

You have to somehow signal to your tests that are safe to proceed (i.e. expectation is fulfilled). The ideal approach would be decouple that Alamofire code and mock its behavior when testing. But just to answer your question, you might want to do the following.

In your view controller:

func sendRequest(withParameters parameters: [String : String], completionHandler: (() -> Void)?) {

  ...

  Alamofire.request(LOGINURL, method: .post, parameters: parameters).validate ().responseJSON { response in

    ...

    // Put this wherever appropriate inside the responseJSON closure
    completionHandler?()
  }
}

Then in your tests:

func testLoginWithValidUserInfo() {
    controllerUnderTest.email?.text = "raghad"
    controllerUnderTest.pass?.text = "1234"
    controllerUnderTest.loginButton?.sendActions(for: .touchUpInside)
    let expectation = self.expectation(description: "logged in successfully)
    waitForExpectations(timeout: 60, handler: nil)

    controllerUnderTest.sendRequest(withParameters: [:]) {
      expectation.fulfill()
    }

    XCTAssertEqual(controllerUnderTest.lblValidationMessage?.text , "logged in successfully")
}

I know you have some intermediate functions between the button click and calling the sendRequest function, but this is just for you to get the idea. Hope it helps!

alanpaivaa
  • 1,959
  • 1
  • 14
  • 23
  • i got new error in sendRequest function : Use of unresolved identifier 'completionHandler'; did you mean 'NSAssertionHandler'? . And when i change it to NSAssertionHandler?() new error is appear : Cannot invoke initializer for type 'NSAssertionHandler?' with no arguments – Raghad ak Apr 07 '19 at 14:19
  • could you please suggest to me any tutorial that test a view controller with almofire? – Raghad ak Apr 07 '19 at 14:20
  • Take at look at https://hackernoon.com/unit-testing-our-ios-network-layer-d0a83172aa66. It may be helpful. – alanpaivaa Apr 07 '19 at 15:04