Trikalabs
  • Home
  • Best online TDD videos
  • Book Suggestions
  • About Me
  • Contact
Trikalabs
No Result
View All Result

TDD UITableView

by fragi
July 17, 2019
in TDD, Unit Testing
Share on FacebookShare on Twitter

UITableView and UICollectionView are the most common objects we use when we want to present a collection of items or hierarchical data in our app. In this article, we will TDD step by step a simple app that shows a list of countries. 

STEP 1:

Let’s start by creating a new project and name it CountriesOfTheWorld. Since we want to start from scratch we have to delete all the files that Xcode has created for us.  Let’s delete the files ViewController.swift andCountriesOfTheWorldTests.swift. Also, let’s open the Main.storyboard and delete the ViewController. Run the project. You should be able to see a blank screen. If the app crashes please make sure you have done all the above changes.

STEP 2:

 It is time for starting TDD our new ViewController. Let’s name our ViewController as CountriesListViewController. So we have to create the CountryListViewControllerTests TestCase:

Just make sure you added it on the Tests target:

Then we get the following warning:

Unless we are planning to have Objective-C code we don’t need the bridging header, so let’s select “Don’t Create”.

 Finally, let’s remove all the methods that the template creates for us. Also, let’s add the @testable import CountriesOfTheWorld. The code should look like:

Swift
1
2
3
4
5
6
import XCTest
@testable import CountriesOfTheWorld
 
class CountriesListViewControllerTests: XCTestCase {
 
}

STEP 3:

We will need tableView to present our countries, so our first test is:

Swift
1
2
3
4
5
6
7
8
func test_setsTableView() {
    let storyboard = UIStoryboard(name: "Main", bundle: nil)
    let countriesListViewCountroller = storyboard.instantiateViewController(withIdentifier: "CountriesListViewCountroller") as! CountriesListViewCountroller
    
    _ = countriesListViewCountroller.view
    
    XCTAssertNotNil(countriesListViewCountroller.tableView)
}

The code does not compile, so let’s create the CountriesListViewCountroller :

Swift
1
2
3
4
5
import UIKit
 
class CountriesListViewCountroller: UIViewController {
    
}

Let’s open our storyboard file and drag on it a UIViewController. Make sure you set it as the initial ViewController. Also, make sure you set the class to CountriesListViewCountroller and also the storyboardID to CountriesListViewCountroller:

Finally, let’s add a UIITableView and connect the IBOutlet with our CountriesListViewCountroller:

Run the test. The test pass.

STEP 4:

Now that we have created our tableView we need to give it a UITableViewDataSource. UITableViewDataSource will provide to the tableView all the data that the tableView needs to display. We can make our UIViewController be the dataSource for our tableView, but this will increase the size of the viewController. Also this decision makes our viewController to have one more responsibility — to provide the data to the tableView. 

In OOP we want to split the responsibilities into different objects. For this reason, we will create a new object to be the dataSource. 

Enough of upfront design, let’s continue with a test:

Swift
1
2
3
4
5
6
7
8
9
10
    func test_setsDataSource() {
        let storyboard = UIStoryboard(name: "Main", bundle: nil)
        let countriesListViewCountroller = storyboard.instantiateViewController(withIdentifier: "CountriesListViewCountroller") as! CountriesListViewCountroller
        
        let countriesDataSource = CountriesDataSource()
        countriesListViewCountroller.dataSource = countriesDataSource
        _ = countriesListViewCountroller.view
        
        XCTAssertTrue(countriesListViewCountroller.tableView.dataSource === countriesDataSource)
    }

 Here we create a CountriesDataSource object and we assign it to the datasource property of the viewController. All we want to test is that the viewController assigns this object as dataSource to its tableView. 

Let’s make it compile by creating the CountriesDataSource file. Make sure to make the CountriesDataSource to conform to the UITabliewDataSource protocol:

Swift
1
2
3
4
5
6
7
8
9
10
11
12
13
import UIKit
 
class CountriesDataSource: NSObject, UITableViewDataSource {
 
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 0
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        return UITableViewCell()
    }
    
}

Now let’s add the dataSource property in the CountriesListViewCountroller:

Swift
1
2
3
4
5
6
class CountriesListViewCountroller: UIViewController {
    
    @IBOutlet weak var tableView: UITableView!
    var dataSource: CountriesDataSource!
    
}

Finally, let’s assign the dataSource to the tableView:

Swift
1
2
3
4
5
6
7
8
9
10
11
class CountriesListViewCountroller: UIViewController {
    
    @IBOutlet weak var tableView: UITableView!
    var dataSource: CountriesDataSource!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        tableView.dataSource = dataSource
    }
}

Run the tests. All the tests pass!

We notice that there is some duplicated code, so we can refactor by extracting this code to a separate method:

Swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class CountriesListViewControllerTests: XCTestCase {
 
    func test_setsTableView() {
        let countriesListViewCountroller = createSUT()
        
        _ = countriesListViewCountroller.view
        
        XCTAssertNotNil(countriesListViewCountroller.tableView)
    }
 
    func test_setsDataSource() {
        let countriesListViewCountroller = createSUT()
        
        let countriesDataSource = CountriesDataSource()
        countriesListViewCountroller.dataSource = countriesDataSource
        _ = countriesListViewCountroller.view
        
        XCTAssertTrue(countriesListViewCountroller.tableView.dataSource === countriesDataSource)
    }
    
    private func createSUT() -> CountriesListViewCountroller {
        let storyboard = UIStoryboard(name: "Main", bundle: nil)
        return storyboard.instantiateViewController(withIdentifier: "CountriesListViewCountroller") as! CountriesListViewCountroller
    }
}

 

STEP 5:

Now let’s test our dataSource. Create a new testcase file and name it  CountriesDataSourceTests. Our list of countries will be in one section so let’s write our first test: 

Swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import XCTest
@testable import CountriesOfTheWorld
 
class CountriesDataSourceTests: XCTestCase {
 
    func test_hasOneSection() {
        let sut = CountriesDataSource()
        
        let tablewView = UITableView()
        tablewView.dataSource = sut
        
        let numberOfSections = tablewView.numberOfSections
        
        XCTAssertEqual(1, numberOfSections)
    }
}

The countriesDataSource is a UITableViewDataSource and we want to test that this dataSource functions as expected. So we create a tableView and we assign the sut as the datasource of the tableView. In that way, we can call the UITableView methods and assert if the results are expected.

Run the tests. The test pass. We haven’t seen the test failing, and the reason is that the default behaviour of the UITableView is to have one section. We can assert that the number of sections is two, see the test fail, and then make it pass by setting to one again. We will keep the test because we want  it to serve as an active documentation. If for some reason the number of sections on the future becomes two, we want this test to fail.

STEP 6:

Now let’s test that the number of rows is equal to the number of countries:

Swift
1
2
3
4
5
6
7
8
9
10
11
    func test_numberOfRowsAreTheCountriesCount() {
        let countries = [Country(name: "Greece"), Country(name: "United Kingdom"), Country(name: "France"), Country(name: "Spain")]
        let sut = CountriesDataSource(countries: countries)
        
        let tablewView = UITableView()
        tablewView.dataSource = sut
        
        let numberOfRows = tablewView.numberOfRows(inSection: 0)
        
        XCTAssertEqual(countries.count, numberOfRows)
    }

 Let’s make it compile. Firstly, let’s add the Country struct:

Swift
1
2
3
struct Country {
    let name: String
}

And now let’s inject the countries to the CountriesDataSource. Also let’s make it return as the number of rows the number of the countries:

Swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class CountriesDataSource: NSObject, UITableViewDataSource {
    
    private let countries: [Country]
    
    init(countries: [Country]) {
        self.countries = countries
        super.init()
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return countries.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        return UITableViewCell()
    }
    
}

And let’s fix the tests that are broken:

Swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
    func test_hasOneSection() {
        let sut = CountriesDataSource(countries: [])
        
        let tablewView = UITableView()
        tablewView.dataSource = sut
        
        let numberOfSections = tablewView.numberOfSections
        
        XCTAssertEqual(1, numberOfSections)
    }
    
    func test_numberOfRowsAreTheCountriesCount() {
        let countries = [Country(name: "Greece"), Country(name: "United Kingdom"), Country(name: "France"), Country(name: "Spain")]
        let sut = CountriesDataSource(countries: countries)
        
        let tablewView = UITableView()
        tablewView.dataSource = sut
        
        let numberOfRows = tablewView.numberOfRows(inSection: 0)
        
        XCTAssertEqual(countries.count, numberOfRows)
    }

STEP 7:

Now that we have test that the tableView has the correct number of rows, we want to test that each row shows the correct data:

Swift
1
2
3
4
5
6
7
8
9
10
11
12
    func test_rowShowsCorrectCountry() {
        let countries = [Country(name: "Greece"), Country(name: "United Kingdom"), Country(name: "France"), Country(name: "Spain")]
        let sut = CountriesDataSource(countries: countries)
        
        let tablewView = UITableView()
        tablewView.dataSource = sut
        tablewView.reloadData()
        
        let cell = tablewView.cellForRow(at: IndexPath(row: 0, section: 0))
        
        XCTAssertEqual("Greece", cell?.textLabel?.text)
    }

And let’s make it pass:

Swift
1
2
3
4
5
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = UITableViewCell()
        cell.textLabel?.text = countries[indexPath.row].name
        return cell
    }

STEP 8:

We know that creating a cell every time the cellForRow is being called can harm the performance. Let’s change our last test to make sure we dequeue cells:

Swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func test_rowShowsCorrectCountry() {
        let countries = [Country(name: "Greece"), Country(name: "United Kingdom"), Country(name: "France"), Country(name: "Spain")]
        let sut = CountriesDataSource(countries: countries)
        
        let storyboard = UIStoryboard(name: "Main", bundle: nil)
        let vc = storyboard.instantiateViewController(withIdentifier: "CountriesListViewCountroller") as! CountriesListViewCountroller
        _ = vc.view
        
        let tablewView = vc.tableView!
        tablewView.dataSource = sut
        tablewView.reloadData()
        
        let cell = tablewView.cellForRow(at: IndexPath(row: 0, section: 0))
        
        XCTAssertEqual("Greece", cell?.textLabel?.text)
        XCTAssertEqual("CountryCell", cell?.reuseIdentifier)
    }

And let’s make it pass:

Swift
1
2
3
4
5
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "CountryCell", for: indexPath)
        cell.textLabel?.text = countries[indexPath.row].name
        return cell
    }
Swift
1
2
3
4
5
6
7
8
9
10
11
12
class CountriesListViewCountroller: UIViewController {
    
    @IBOutlet weak var tableView: UITableView!
    var dataSource: CountriesDataSource!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "CountryCell")
        tableView.dataSource = dataSource
    }
}

Run the tests. All the tests pass. 

An alternative way to test the dequeue functionality is with a Spy on the UITableView that will make sure that the dequeue functionality is being called with the correct reuse identifier. I generally prefer to avoid using Spies and Mocks on UIKit objects. If there is another way to test the functionality without the Spies and the Mocks (for UIKit objects) I will choose it. 

STEP 9:

Run the app. The app shows an empty ViewController. That is because we have tests the objects as units. We will see in a following article how to test the composition of objects but for now, go to appDelegate and add the following code:

Swift
1
2
3
4
5
6
7
8
9
10
11
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        
        guard let countriesListViewCountroller = self.window?.rootViewController as? CountriesListViewCountroller else {
            return true
        }
        let countries = [Country(name: "Greece"), Country(name: "United Kingdom"), Country(name: "France"), Country(name: "Spain")]
        let countriesDataSource = CountriesDataSource(countries: countries)
        countriesListViewCountroller.dataSource = countriesDataSource
        
        return true
    }

Now run the app. Finally we can see our list of countries!

fragi

fragi

Related Posts

What is TDD?
Unit Testing

Write better Unit tests with XCTUnwrap

January 27, 2022

Testing optionals can require boilerplate code to unwrap it. It can also affect the readability of the test. Let's look...

What is TDD?
TDD

XCTestCase lifecycle

May 25, 2021

XCTestCase has many methods as part of its lifecycle. It has a class setup method that executes only once before...

What is TDD?
TDD

Make your tests more readable

May 23, 2021

In the FizzBuzz code challenge, we had to write the following test method: Although the test is easy to...

What is TDD?
TDD

@testable

May 16, 2021

At the beginning of the FizzBuzz code challenge , we deleted the @testable line of code. What does this @testable...

What is TDD?
TDD

What is SUT

May 16, 2021

In many articles about TDD, you will find the term SUT, which refers to the System Under Test. Using SUT...

What is TDD?
TDD

Why unit testing is valuable

May 16, 2021

There are many benefits of writing unit tests. The test we write can serve as a documentation of what the...

Next Post
Unit Test Result type and closure

Unit Test Result type and closure

Unit Test NotificationCenter

Unit Test NotificationCenter

Unit Test Post Notification

Unit Test Post Notification

  • Advertise
  • Privacy & Policy
  • Contact

© 2019 Trikalabs Ltd. All rights reserved.

No Result
View All Result
  • Home
  • About Me
  • A curated list with the best free online TDD videos
  • Book Suggestions
  • Pinner Code Club

© 2019 Trikalabs Ltd. All rights reserved.