Using mocks

As mentioned in the previous section, table view cells should be dequeued. To make sure that this happens, we need a test. The dequeuing is done by calling the dequeueReusableCell(withIdentifier:for:) method on the table view. The table view then checks whether there is a cell that can be reused. If not, it creates a new cell and returns it. We are going to use a table view mock to register when the method is called.

In Swift, classes can be defined within other classes. In the case of mocks, this is useful because, this way, the mocks are only visible and accessible at the point where they are needed.

Add the following code to ItemListDataProviderTests.swift, outside of the ItemListDataProviderTests class:

extension ItemListDataProviderTests { 
   
  class MockTableView: UITableView { 
    var cellGotDequeued = false 
   

    override func dequeueReusableCell( 
      withIdentifier identifier: String, 
      for indexPath: IndexPath) -> UITableViewCell { 
       

      cellGotDequeued = true 
       

      return super.dequeueReusableCell(withIdentifier: identifier, 
                                       for: indexPath) 
    } 
  } 
} 

We have used an extension of ItemListDataProviderTests to define a mock of UITableView. Our mock uses a Boolean property to register when dequeueReusableCell(withIdentifier:for:) is called.

Add the following test to ItemListDataProviderTests:

func test_CellForRow_DequeuesCellFromTableView() { 
  let mockTableView = MockTableView() 
  mockTableView.dataSource = sut 
  mockTableView.register(ItemCell.self, 
                         forCellReuseIdentifier: "ItemCell") 
   

  sut.itemManager?.add(ToDoItem(title: "Foo")) 
  mockTableView.reloadData() 

   
  _ = mockTableView.cellForRow(at: IndexPath(row: 0, section: 0)) 
  
 
  XCTAssertTrue(mockTableView.cellGotDequeued) 
} 

In the test, we first create an instance and set up our table view mock. Then, we add an item to the item manager of sut. Next, we call cellForRow(at:) to trigger the method call that we want to test. Finally, we assert that the table view cell is dequeued.

Run this test. It fails because the cell has not yet been dequeued. Replace the implementation of tableView(_:cellForRowAt:) with the following code:

func tableView(_ tableView: UITableView, 
               cellForRowAt indexPath: IndexPath) -> UITableViewCell { 
   

  let cell = tableView.dequeueReusableCell( 
    withIdentifier: "ItemCell", 
    for: indexPath) 
 
  
  return cell 
} 

Run the tests. Now, the last added test succeeds, but test_CellForRow_ReturnsItemCell() fails. The reason for this is that we need to register a cell when we want to make use of the automatic dequeuing of cells in UITableView. There are three ways to register a cell. Firstly, we can do this in code, just as we did in test_CellForRow_DequeuesCellFromTableView(). Secondly, we can do this by registering a nib for the cell. Thirdly, it can be done by adding a cell with the used reuse identifier to the storyboard. We will implement the third way because we are already using a storyboard for the app.

Open Main.storyboard in the editor and add a Table View Cell to the Table View:

In the Identity Inspector, change the class of the cell to ItemCell:

In the Attribute Inspector, set Identifier to ItemCell:

Next, we need to set up the test case such that it uses the storyboard to create the table view. First, add the following property to ItemListDataProviderTests:

 var controller: ItemListViewController!

Then, replace setUp() with the following code:

override func setUp() { 
  super.setUp() 
   

  sut = ItemListDataProvider() 
  sut.itemManager = ItemManager() 
   

  let storyboard = UIStoryboard(name: "Main", bundle: nil) 
  controller = storyboard.instantiateViewController( 
    withIdentifier: "ItemListViewController") as!  
ItemListViewController controller.loadViewIfNeeded() tableView = controller.tableView tableView.dataSource = sut }

Instead of creating a table view using an UITableView initializer, we instantiate an instance of ItemListViewController from the storyboard and use its table view. The controller.loadViewIfNeeded() call is needed because, otherwise, the table view is nil.

Run the tests. All the tests pass and there should be nothing to refactor.

After the cell is dequeued, the name, location, and due date should be set to labels in the cell. A common pattern in the implementation of table view cells in iOS is to implement a configCell(with:) method in the cell class. The table view data source then needs to call this method in tableView(_:cellForRowAt:).

To make sure that configCell(with:) is called after the cell is dequeued, we will write a test that uses a table view cell mock. Add the following mock class after the table view mock:

class MockItemCell : ItemCell { 
  var configCellGotCalled = false 
   

  func configCell(with item: ToDoItem) { 
    configCellGotCalled = true 
  } 
} 

The mock registers when configCell(with:) is called by setting configCellGotCalled to true. Add the following test to ItemListDataProviderTests:

func test_CellForRow_CallsConfigCell() { 

let mockTableView = MockTableView() mockTableView.dataSource = sut mockTableView.register(
MockItemCell.self, forCellReuseIdentifier: "ItemCell") let item = ToDoItem(title: "Foo") sut.itemManager?.add(item) mockTableView.reloadData() let cell = mockTableView .cellForRow(at: IndexPath(row: 0, section: 0)) as! MockItemCell XCTAssertTrue(cell.configCellGotCalled) }

In this test, we use a mock for the table view and for the table view cell. After setting up the table view, we add an item to the item manager. Then, we get the first cell of the table view. This triggers the call of tableView(_:cellForRowAt:). Finally, we assert that configCellGotCalled of our table view cell mock is true.

Run the tests to make sure that this test fails. A failing test means that we need to write the implementation code.

Add the following line to tableView(_:cellForRowAt:) before the cell is returned:

cell.configCell(with: ToDoItem(title: "")) 

The static analyzer will complain 'UITableViewCell' has no member 'configCell'. Obviously, we have forgotten to cast the cell to ItemCell. Add the cast at the end of the line where the cell is dequeued as follows:

let cell = tableView.dequeueReusableCell( 
  withIdentifier: "ItemCell", 
  for: indexPath) as! ItemCell 

Now, the static analyzer complains 'ItemCell' has no member 'configCell'. Open ItemCell.swift and add the following empty method definition to ItemCell:

func configCell(with item: ToDoItem) { 
} 

Run the tests. Xcode complains in MockItemCell that configCell(with:) needs the override keyword. In Swift, whenever you override a method of the superclass, you need to add this keyword. This is a safety feature. In Objective-C, you may accidentally override a method because if you don't know that the method was defined in the superclass. This is not possible in Swift.

Add the keyword to the method definition, such that it looks like this:

override func configCell(with item: ToDoItem) { 
  configCellGotCalled = true 
} 

Now run the tests. All the tests are green again.

Let's check whether there is something to refactor. Currently, the test_CellForRow_CallsConfigCell() test, just asserts that the method is called, but we can do better. The configCell(with:) method gets called with an item as a parameter. This item should be used to fill the label of the cell. We'll extend the test to also test whether the method is called with the expected item.

Replace the table view cell mock with the following code:

class MockItemCell : ItemCell { 
  var catchedItem: ToDoItem? 
  
 
  override func configCell(with item: ToDoItem) { 
    catchedItem = item 
  } 
} 

Then, replace the assertion in test_CellForRow_CallsConfigCell() with this line of code:

XCTAssertEqual(cell.catchedItem, item) 

The test now fails because we have not yet used the item from the item manager. Replace tableView(_:cellForRowAt:) with the following code:

func tableView(_ tableView: UITableView, 
               cellForRowAt indexPath: IndexPath) -> UITableViewCell { 
   

  let cell = tableView.dequeueReusableCell( 
    withIdentifier: "ItemCell", 
    for: indexPath) as! ItemCell 
   

  if let item = itemManager?.item(at: indexPath.row) { 
    cell.configCell(with: item) 
  } 
   

  return cell 
}

After dequeuing the cell, we get toDoItem from the item manager; call configCell(with:) if it succeeds.

Run the tests. All the tests pass. We are now confident that the cell is called with the right to-do item to configure its labels.

Earlier in this chapter, we tested that the number of rows in the first section corresponds to the number of unchecked to-do items, as well as the number of rows in the second section to the number of checked to-do items. Now, we need to test that the configuration of the cell in the second section passes a checked item to the configuration method.

Add the following test to ItemListDataProviderTests:

func test_CellForRow_Section2_CallsConfigCellWithDoneItem() { 

let mockTableView = MockTableView() mockTableView.dataSource = sut mockTableView.register(MockItemCell.self, forCellReuseIdentifier: "ItemCell") sut.itemManager?.add(ToDoItem(title: "Foo")) let second = ToDoItem(title: "Bar") sut.itemManager?.add(second) sut.itemManager?.checkItem(at: 1) mockTableView.reloadData() let cell = mockTableView .cellForRow(at: IndexPath(row: 0, section: 1)) as! MockItemCell XCTAssertEqual(cell.catchedItem, second) }

The test is similar to the earlier one. The main difference here is that we add two to-do items to the item manager and check the second to populate the second section of the table view.

Run the test. The test crashes because the runtime unexpectedly found nil while unwrapping an Optional value. This is strange because the similar code has worked before this. The reason for this crash is that UIKit optimizes the second section because the table view has a frame of CGRect.zero. As a result, cellForRow(at:) returns nil, and the as! forced unwrapping lets the runtime crash.

Replace the definition of the table view mock in the test with the following code:

let mockTableView = MockTableView( 
  frame: CGRect(x: 0, y:0, width: 320, height: 480), 
  style: .plain) 

Run the tests again. It doesn't crash anymore but the test fails so, we need to write some implementation code.

In the implementation of tableView(_:numberOfRowsInSection:), we introduced an enum for the table view sections, which has improved the code a lot. We will take advantage of the enum in the implementation of tableView(_:cellForRowAt:). Replace the code of tableView(_:cellForRowAt:) with the following code:

func tableView(_ tableView: UITableView, 
               cellForRowAt indexPath: IndexPath) -> UITableViewCell { 
   

  let cell = tableView.dequeueReusableCell( 
    withIdentifier: "ItemCell", 
    for: indexPath) as! ItemCell 
   

  guard let itemManager = itemManager else { fatalError() } 
  guard let section = Section(rawValue: indexPath.section) else 
  { 
    fatalError() 
  } 
   

  let item: ToDoItem 
  switch section { 
  case .toDo: 
    item = itemManager.item(at: indexPath.row) 
  case .done: 
    item = itemManager.doneItem(at: indexPath.row) 
  } 
   

  cell.configCell(with: item) 
 

  return cell 
} 

After dequeuing the cell, we use guard to make sure that the item manager is present and the index path section has a supported value. Then, we switch on the section and assign a to-do item to a constant that is used to configure the cell. Finally, the cell is returned.

Run the tests. All the tests pass.

Look at the previous tests that you have written. They have duplicated code. Let's clean it up a bit. Add the following code to MockTableView:

class func mockTableView( 
  withDataSource dataSource: UITableViewDataSource)  
  -> MockTableView { 
   

  let mockTableView = MockTableView( 
    frame: CGRect(x: 0, y: 0, width: 320, height: 480), 
    style: .plain) 
   

  mockTableView.dataSource = dataSource 
  mockTableView.register(MockItemCell.self, 
                         forCellReuseIdentifier: "ItemCell") 
   

  return mockTableView 
} 

This class method creates a mock table view, sets the data source, and registers the mock table view cell.

Now, we can replace the initialization and setup of the mock table view in test_CellForRow_DequeuesCellFromTableView(), test_CellForRow_CallsConfigCell(), and test_CellForRow_InSectionTwo_CallsConfigCellWithDoneItem() with the following:

let mockTableView = MockTableView.mockTableView(withDataSource: sut)

Run the tests to make sure that everything still works.

When a table view allows the deletion of cells and a user swipes on a cell to the left, then on the right-hand side, a red button will appear with the Delete title. In our application, we want to use this button to check and uncheck items. The button title should show the actions that the button is going to perform. Let's write a test to make sure that this is the case for the first section:

func test_DeleteButton_InFirstSection_ShowsTitleCheck() { 
  let deleteButtonTitle = tableView.delegate?.tableView?( 
    tableView, 
    titleForDeleteConfirmationButtonForRowAt: IndexPath(row: 0, 
                                                        section: 0)) 
   

  XCTAssertEqual(deleteButtonTitle, "Check") 
} 

This method is defined in the UITableViewDelegate protocol. Add the following line to setUp() right after tableView.dataSource = sut:

tableView.delegate = sut 

The static analyzer complains that ItemListDataProvider does not conform to UITableViewDelegate. Add the conformance to it like this:

class ItemListDataProvider: NSObject, UITableViewDataSource, UITableViewDelegate { 
  // ... 
} 

Run the tests. The tests fail. In ItemListDataProvider, add the method as follows:

func tableView( 
  _ tableView: UITableView, 
  titleForDeleteConfirmationButtonForRowAt indexPath: 
  IndexPath) -> String? { 
   

  return "Check" 
} 

Now, the tests pass.

In the second section, the title of the Delete button should be Uncheck. Add the following test to ItemListDataProviderTests:

func test_DeleteButton_InSecondSection_ShowsTitleUncheck() { 
  let deleteButtonTitle = tableView.delegate?.tableView?( 
    tableView, 
    titleForDeleteConfirmationButtonForRowAt: IndexPath(row: 0, 
                                                        section: 1)) 
   

  XCTAssertEqual(deleteButtonTitle, "Uncheck") 
} 

Run the tests. The last test fails because of a missing implementation. Replace tableView(_:titleForDeleteConfirmationButtonForRowAt:) with this:

func tableView( 
  _ tableView: UITableView, 
  titleForDeleteConfirmationButtonForRowAt indexPath: 
  IndexPath) -> String? { 
   

  guard let section = Section(rawValue: indexPath.section) else 
  { 
    fatalError() 
  } 
   

  let buttonTitle: String 
  switch section { 
  case .toDo: 
    buttonTitle = "Check" 
  case .done: 
    buttonTitle = "Uncheck" 
  } 
   

  return buttonTitle 
} 

Here, we used guard again, as well as the Section enum to make the code clean and easy to read.

Run the tests. All the tests pass.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.216.94.152