Meep
Meep

Reputation: 531

Swipe left in unit test manually to call handler to test in Swift

I am unit testing the tableview and I need to swipe logically? a row in UITableView. Is it possible to execute the swiping in unit test to invoke the handler? I looked at the UITableViewDelegate but there isn't a swiping action (didSelectRowAt is there and tested in unit test).

func createDeleteHandler(tableView : UITableView, indexPath : IndexPath) -> UIContextualAction.Handler {
        let deleteHandler =  { (ac:UIContextualAction, view:UIView, success:(Bool) -> Void) in
            let noteToBeDeleted = self.notes[indexPath.row]
            NoteManager.shared.deleteNote(note: noteToBeDeleted)
            self.notes.remove(at: indexPath.row)
            tableView.deleteRows(at: [indexPath], with: .fade)
            success(true)
        }

        return deleteHandler
    }

Upvotes: 2

Views: 1518

Answers (2)

Evan R
Evan R

Reputation: 875

As David S. said, I don't believe there's a way to trigger the swipe action in a unit test. However, since you also asked about invoking the handler and testing its actions, I'll respond. Regarding testing the UIContextualAction (or actions) for each table view cell, we want to know: Are the actions with all their properties being set up correctly (title, image, etc.) and are we calling their handlers at the proper time (when some dependent action is completed)?

I'll answer the second question first, as that's the question you posed, and then answer the first question last as more of a bonus. To test the code snippet you posted, call that function in a unit test, store the result in a property, create a closure of type (Bool) -> Void, and call the contextual action's completionHandler closure with the closure you created as its argument:

func testCompletionHandlerCalled() {
    let completionHandlerCalledExpectation = expectation(description: "Completion handler called)"

    // Do whatever it is you need to do make sure your table view data is loaded
    // and the view is added properly to your view hierarchy
    let tableView = MyTableView()
    let sut = MyTableViewDelegateObject() // In your case, the object from your code snippet
    let indexPath = IndexPath(row: 0, section: 1) // Whichever index path you want to test
    let deleteHandler = sut.createDeleteHandler(tableView: tableView, indexPath: indexPath)

    let stubCompletion: (Bool) -> Void = { success in
        XCTAssertTrue(success)
        completionHandlerCalledExpectation.fulfill()
    }

    // You could structure your code different to return the action and not just the handler,
    // which you'd then pass in here
    deleteHandler(UIContextualAction(), UIView(), stubCompletion)

    waitForExpectations(timeout: 0.001)
}

This test isn't particularly useful on its own because it doesn't really test the meat of what's happening when the user taps the delete button: the logic and actions of deleting a note. So, here's some other questions you'd want to ask yourself in regards to your code: What happens if NoteManager can't delete the note? Do you always want to return true from the completion handler, even if note deletion isn't successful? For the former, you may want to consider injecting in a NoteManager object instead of just calling a singleton instance from your code. That way, you can test that the deletion is happening when this handler is called.

Bonus:

In order to know the answer to the first question (whether the actions are being set up correctly), we can inject an object that will provide a UIContextualAction into the object that uses them (probably a view model if you're using MVVM and a view controller if you're using MVC). The use of an enum gives us some typing around each action when it's set up and helps provide some context around its use. Something like this:

enum MyTableViewCellActionTypes {
    case edit
    case delete
}

protocol UIContextualActionProviderType {
    func action(for type: FoodItemActionType, handler: @escaping UIContextualAction.Handler) -> UIContextualAction
}

struct NotesContextualActionsProvider: UIContextualActionProviderType {
    func action(for type: FoodItemActionType, handler: @escaping UIContextualAction.Handler) -> UIContextualAction {
        switch type {
        case .edit: return UIContextualAction(style: .normal, title: "Edit", handler: handler)
        case .delete: return UIContextualAction(style: .destructive, title: "Delete", handler: handler)
        }
    }
}

Now, we can inject this into the object that needs these actions and we can test that they're being set up correctly (once we call the action that triggers the swipe configuration setup).

Upvotes: 0

David S.
David S.

Reputation: 6705

You can use XCUITest tests like this:

import XCTest

class MoreUITests: XCTestCase {

    override func setUp() {
        continueAfterFailure = false
        XCUIApplication().launch()
    }

    func testUI() {

        let app = XCUIApplication()
        let tablesQuery = app.tables
        let addButton = app.navigationBars["Master"].buttons["Add"]
        let masterButton = app.navigationBars["Detail"].buttons["Master"]
        addButton.tap()  // adds Item-0
        addButton.tap()  // adds Item-1
        addButton.tap()  // adds Item-2
        addButton.tap()  // adds Item-3

        tablesQuery.staticTexts["Item-1"].tap()

        // Go back
        masterButton.tap()

        // Swipe Left on item-2
        tablesQuery.staticTexts["Item-2"].swipeLeft()

    }
}

The easiest thing to do is record them using the Xcode UI Recorder. More detail:

https://developer.apple.com/library/archive/documentation/DeveloperTools/Conceptual/testing_with_xcode/chapters/09-ui_testing.html

Here's an example I recorded with a swipe:

https://youtu.be/lHafMlIcoCY

Upvotes: 1

Related Questions