0

In swift, I need to test the deletion of a table view cell. Each cell has two row actions associated with it, with one of them being to delete that cell.

How do I manually call the handler on this row action? This is the function that does the deletion

private func deleteCellAtRow(row: Int) {
    let deleteCell = NSIndexPath(forRow: row, inSection: 0)

    // get the actions corresponding to the cell at that index
    let actions : [UITableViewRowAction] = self.tableView(self.queue, editActionsForRowAtIndexPath: deleteCell)!

    // Get a reference to the delete button for that cell
    let predicate : NSPredicate = NSPredicate(format: "title MATCHES[c] '.*delete'")
    if let action = (actions as NSArray).filteredArrayUsingPredicate(predicate).first as? UITableViewRowAction {
        // What to do now??
        // Call the callback 
    }
}
smac89
  • 39,374
  • 15
  • 132
  • 179

2 Answers2

4

Unfortunately I had the same need and I didn't find a way to do it in a clean way.

Anyway, since the purpose of the manual trigger is just to unit test the action (as in my case too), we can inspect the private headers of the UITableViewRowAction class and manually trigger the needed API. Obviously, this can't be done in production, but just in the test suite, or the app will be rejected!

Note: I tested this only on iOS 9.3, and since it is using a private API, it could stop working with newer versions of iOS.

Solution

Add those lines in the bridging header of the test bundle (or in a separate file, as long it is imported in the bridging header).

@interface UITableViewRowAction (Private)
@property(nonatomic, strong) void (^_handler)(UITableViewRowAction *, NSIndexPath *);
@end

In this way we publicly expose the private property _handler of UITableViewRowAction.

Now, in our swift file we can do this:

private func deleteCellAtRow(row: Int) {
    let deleteCell = NSIndexPath(forRow: row, inSection: 0)

    // get the actions corresponding to the cell at that index
    let actions : [UITableViewRowAction] = self.tableView(self.queue, editActionsForRowAtIndexPath: deleteCell)!

    // Get a reference to the delete button for that cell
    let predicate : NSPredicate = NSPredicate(format: "title MATCHES[c] '.*delete'")
    if let action = (actions as NSArray).filteredArrayUsingPredicate(predicate).first as? UITableViewRowAction {
        // OUR SOLUTION
        action._handler(action, indexPath)
        // And now you can test!
    }
}
Alessandro Orrù
  • 3,443
  • 20
  • 24
1

A more complicated, but perhaps more future proof solution would be to use a seam. For more on seams, read https://qualitycoding.org/mocking-standalone-functions/. We can use this same technique to mock out initializers. First, add the following to your project:

import UIKit

let defaultUITableViewRowActionInit: (UITableViewRowAction.Style, String?, @escaping (UITableViewRowAction, IndexPath) -> Void) -> UIKit.UITableViewRowAction = { style, title, handler in
  return UIKit.UITableViewRowAction(style: style, title: title, handler: handler)
}
var uiTableViewRowActionInit = defaultUITableViewRowActionInit

func UITableViewRowAction(style: UITableViewRowAction.Style, title: String?, handler: @escaping (UITableViewRowAction, IndexPath) -> Void) -> UIKit.UITableViewRowAction {
  return uiTableViewRowActionInit(style, title, handler)
}

From here on out, calls to UITableVeiwRowAction will go through our function instead of normal initializer. During normal prod use, the behavior will be the same. For tests, we can now add a mock first:

class UITableViewRowActionMock: UITableViewRowAction {
  var passedStyle: UITableViewRowAction.Style
  var passedTitle: String?
  var passedHandler: (UITableViewRowAction, IndexPath) -> Void

  init(style: UITableViewRowAction.Style, title: String?, handler: @escaping (UITableViewRowAction, IndexPath) -> Void) {
    passedStyle = style
    passedTitle = title
    passedHandler = handler
  }
}

This class just captures the args passed to it, including the block. From here, we need our test setup and teardown to setup and teardown the seam:

override func setUp() {
  super.setUp()

  // CODE ...

  uiTableViewRowActionInit = { style, title, handler in
    return UITableViewRowActionMock(style: style, title: title, handler: handler)
  }
}

override func tearDown() {
  // CODE ...

  uiTableViewRowActionInit = defaultUITableViewRowActionInit

  super.tearDown()
}

And from there, we can run our test:

func testEditActionSomehow() {
  let indexPath = IndexPath(row: 0, section: 0)
  // here I just grab, first, feel free to do something more complicated
  guard let action = sut.tableView(sut.tableView, editActionsForRowAt: indexPath)?.first as? UITableViewRowActionMock else {
    XCTFail("Unexpected type")
    return
  }

  action.passedHandler(action, indexPath)

  // 

Add assertion code here }

If you have a lot of cases to test though, you may want to extract most of the code from your actions to somewhere more testable.

moger777
  • 1,117
  • 11
  • 16