UIViewControllers are among the most frequent objects we use when we want to manage a view hierarchy for a UIkit app.
We can create UIViewControllers programmatically, using xib files or using storyboards. In this article will TDD a ViewController with a simple UI. Our ViewController will have two labels one with the name of a country and one with the flag of this country.
Let’s start our example:
STEP 1:
Create a new iOS project and name it TDDViewController. 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 and TDDViewControllerTests.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 CountryDetailsViewController. So we have to create the
CountryDetailsViewControllerTests TestCase:
Just make sure you added 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. The code should look like:
1 2 3 4 5 |
import XCTest class CountryDetailsViewControllerTests: XCTestCase { } |
STEP 3:
Our first test is:
1 2 3 4 5 6 7 8 |
func test_shouldShowCountryName() { let storyboard = UIStoryboard(name: "Main", bundle: nil) let sut = storyboard.instantiateViewController(withIdentifier: "CountryDetailsViewController") as! CountryDetailsViewController _ = sut.view XCTAssertEqual("Greece", sut.countryNameLabel.text) } |
We notice that we trigger the view lifecycle by calling sut.view. ViewControllers are handling the view lifecycle and we don’t want to test their implementation on how they do this. All we need is to trigger the view lifecycle and access the view is the easiest and safest way.
And now let’s make it pass:
First of all, let’s create the CountryDetailsViewController class. The code should look like:
1 2 3 4 5 |
import UIKit class CountryDetailsViewController: UIViewController { } |
Also, let’s go to the Main storyboard and add a new ViewController. Make sure you set the class to CountryDetailsViewController and also the storyboardID to CountryDetailsViewController:
Also, make sure you set it as the initial ViewController:Let’s import our main module to the test class:
1 2 |
import XCTest @testable import TDDViewController |
And let’s add an IBOutlet UIlabel with the name countryNameLabel to the ViewController. Set the constraints to be near the top of the view. Connect the IBOutlet to the CountryDetailsViewController:
Now the test compiles but it fails. Let’s add the correct text and make the test pass:
1 2 3 4 5 6 7 8 9 10 11 12 |
import UIKit class CountryDetailsViewController: UIViewController { @IBOutlet weak var countryNameLabel: UILabel! override func viewDidLoad() { super.viewDidLoad() countryNameLabel.text = "Greece" } } |
Now it’s time to refactor. Having a hardcoded string is not a good practice. Firstly, it makes the class not reusable for other countries and secondly because it represents the name of a country is better to create a country struct to encapsulate this logic there:
let’s create the Country struct:
1 2 3 |
struct Country { let name: String } |
and let’s use it on the CountryDetailsViewController
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import UIKit class CountryDetailsViewController: UIViewController { @IBOutlet weak var countryNameLabel: UILabel! var country: Country = Country(name: "Greece" override func viewDidLoad() { super.viewDidLoad() countryNameLabel.text = country.name } } |
STEP 4:
Now let’s write to our next test.
1 2 3 4 5 6 7 8 9 |
func test_shouldShowFlag() { let storyboard = UIStoryboard(name: "Main", bundle: nil) let sut = storyboard.instantiateViewController(withIdentifier: "CountryDetailsViewController") as! CountryDetailsViewController _ = sut.view let expectedFlag = try? CountryCodeStringToFlagConvert.flag(country: "gr") XCTAssertEqual(expectedFlag, sut.countryFlagLabel.text) } |
We will need another label for the flag so let’s add a countryFlagLabel UILabel in the CountryDetailsViewController and connect the IBOutlet as we did before:
CountryCodeStringToFlagConvert
And now we have to add the CountryCodeStringToFlagConverter functionality. Let’s create a struct with the following code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
struct CountryCodeStringToFlagConvert { enum CountryCodeStringToFlagConvertError: Error { case flagNotFound } static func flag(country:String) throws -> String { let base = 127397 var usv = String.UnicodeScalarView() for utfc in country.uppercased().utf16 { guard let us = UnicodeScalar(base + Int(utfc)) else { throw CountryCodeStringToFlagConvertError.flagNotFound } usv.append(us) } return String(usv) } } |
We should also unit test this struct, but to keep this article short in size we will not. If you want to check how to test a method that throws an error check this post: http://trikalabs.com/how-to-test-errors-in-swift/
Run the tests. The test compiles but fails. Let’s add the implementation to make it pass:
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 26 27 28 29 30 31 32 33 34 |
class CountryDetailsViewController: UIViewController { @IBOutlet weak var countryNameLabel: UILabel! @IBOutlet weak var countryFlagLabel: UILabel! var country: Country = Country(name: "Greece", code: "gr") override func viewDidLoad() { super.viewDidLoad() countryNameLabel.text = country.name countryFlagLabel.text = country.flag() } } struct CountryCodeStringToFlagConvert { enum CountryCodeStringToFlagConvertError: Error { case flagNotFound } static func flag(country:String) throws -> String { let base = 127397 var usv = String.UnicodeScalarView() for utfc in country.uppercased().utf16 { guard let us = UnicodeScalar(base + Int(utfc)) else { throw CountryCodeStringToFlagConvertError.flagNotFound } usv.append(us) } return String(usv) } } |
and let’s refactor. We can extract the logic of the sut creation in a separate method:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
func test_shouldShowCountryName() { let sut = createSUT() _ = sut.view XCTAssertEqual("Greece", sut.countryNameLabel.text) } func test_shouldShowFlag() { let sut = createSUT() _ = sut.view let expectedFlag = try? CountryCodeStringToFlagConvert.flag(country: "gr") XCTAssertEqual(expectedFlag, sut.countryFlagLabel.text) } func createSUT() -> CountryDetailsViewController { let storyboard = UIStoryboard(name: "Main", bundle: nil) return storyboard.instantiateViewController(withIdentifier: "CountryDetailsViewController") as! CountryDetailsViewController } |