Implementing InputViewController

Add a test case with the name InputViewControllerTests, import the ToDo module, and remove the two template methods. If you have problems with this task, go back to the beginning of the previous sections where we explained it in more detail.

You have taken a look at the first steps of the TDD of controllers several times now. Therefore, we will perform several steps at once now and put the setup code directly in setUp(). Firstly, add the property var sut: InputViewController!. Secondly, add the View Controller InputViewController. Again, if you are unsure about how to do this, have a look at the previous sections. Next, add the following setup code to setUp():

let storyboard = UIStoryboard(name: "Main",
    bundle: nil)
sut = storyboard.instantiateViewControllerWithIdentifier(
    "InputViewController") as! InputViewController

_ = sut.view

Add the following test:

func test_HasTitleTextField() {
    XCTAssertNotNil(sut.titleTextField)
}

This test does not compile because InputViewController does not have a member called titleTextField. To make the test compile, add the property @IBOutlet weak var titleTextField: UITextField! to InputViewController. If you run the test, it still does not pass. We already know what is needed to make it pass from the implementation of DetailViewController. Firstly, add a View Controller to the storyboard. Change its Class and Storyboard ID to InputViewController. Secondly, add a text field to the storyboard scene and connect it to the outlet in InputViewController. This should be enough to make the test pass.

Now, add the rest of the text fields and the two buttons (dateTextField, locationTextField, addressTextField, descriptionTextField, saveButton, and cancelButton) in a test-driven way. Make sure that all tests pass before you move on, and don't forget to refactor your code and tests if needed.

In the address field, the user can put in addresses for the to-do items. The app should then fetch the coordinate and store it in the to-do items' location. Apple provides the CLGeocoder class in CoreLocation for this task. In the test, we want to mock this class to be independent from the Internet connection. Import the CoreLocation module (import CoreLocation), and add the following code to InputViewControllerTests.swift outside of InputViewControllerTests:

extension InputViewControllerTests {
    class MockGeocoder: CLGeocoder {
        
        var completionHandler: CLGeocodeCompletionHandler?
        
        override func geocodeAddressString(addressString: String,
            completionHandler: CLGeocodeCompletionHandler) {
                
            self.completionHandler = completionHandler
        }
    }
}

The only thing the mock does is to capture the completion handler when geocodeAddressString(_:completionHandler:) is called. This way, we can call the completion handler in the test and check whether the system under the test works as expected.

The signature of the completion handler looks like this:

public typealias CLGeocodeCompletionHandler = ([CLPlacemark]?, NSError?) -> Void

The first argument is an optional array of place marks, which are sorted from the best to worst match. In the test, we would like to return a place mark with a defined coordinate to check whether the to-do item is created correctly. The problem is that all the properties in CLPlacemark are readonly, and it does not have an initializer that we can use to set the coordinate. Therefore, we need another mock that allows us to override the location property. Add the following class definition to the InputViewControllerTests extension:

class MockPlacemark : CLPlacemark {
    
    var mockCoordinate: CLLocationCoordinate2D?
    
    override var location: CLLocation? {
        guard let coordinate = mockCoordinate else
        { return CLLocation() }
        
        return CLLocation(latitude: coordinate.latitude,
            longitude: coordinate.longitude)
    }
}

Now, we are ready for the test. The test is a bit complicated. To clearly show you what is going on, we will show the complete test, and then add implementation code until the test passes. By doing this, we are not going to follow the TDD workflow because we will get errors from the static analyzer before we have even finished writing the test method. But this way makes it easier to see what is going on. Firstly, add a property for our place mark mock to InputViewControllerTests:

var placemark: MockPlacemark!

This is needed because the test would crash since the place mark is accessed outside of its definition scope. Add the following test method to InputViewControllerTests:

func testSave_UsesGeocoderToGetCoordinateFromAddress() {
    sut.titleTextField.text = "Test Title"
    sut.dateTextField.text = "02/22/2016"
    sut.locationTextField.text = "Office"
    sut.addressTextField.text = "Infinite Loop 1, Cupertino"
    sut.descriptionTextField.text = "Test Description"
    
    let mockGeocoder = MockGeocoder()
    sut.geocoder = mockGeocoder
    
    sut.itemManager = ItemManager()
    
    sut.save()
    
    placemark = MockPlacemark()
    let coordinate = CLLocationCoordinate2DMake(37.3316851, 
        -122.0300674)
    placemark.mockCoordinate = coordinate
    mockGeocoder.completionHandler?([placemark], nil)
    
    let item = sut.itemManager?.itemAtIndex(0)
    
    let testItem = ToDoItem(title: "Test Title",
        itemDescription: "Test Description",
        timestamp: 1456095600,
        location: Location(name: "Office", coordinate: coordinate))
    
    XCTAssertEqual(item, testItem)
}

Let's take a look at what is going on here. Firstly, we set the text values to the text fields. Then, we create a geocoder mock and set it to a property of the sut. This is called a dependency injection. We inject the instance from the test that should be used to fetch the coordinate for the given address. To add an item to the list of to-do items, InputViewController needs to have an item manager. In the test, we set it to a new instance. Next, we call the method we want to test (save()). This should call geocodeAddressString(_:completionHandler:) of our geocoder mock, and as a result, the mock should capture the completion handler from the implementation. In the next step, we call the completion handler with a place mark that has a given coordinate. We expect that the completion handler uses the place mark and information from the text fields to create a to-do item. In the rest of the test methods, we assert that this is actually the case.

Now, let's make the test pass. InputViewController needs a geocoder. Import CoreLocation to InputViewController and add this property:

lazy var geocoder = CLGeocoder()

Lazy properties are set the first time they are accessed. This way, we can set our mock to geocoder before we access it in the test the first time. We inject the dependency in the test. In the implementation code, we can use geocoder as it would be a normal property.

Next, we add a property to hold a reference to the item manager:

var itemManager: ItemManager?

To make the test compilable, add the minimal implementation of the save method:

func save() {
}

Now, we need to create a to-do item and add it to the item manager within save(). Add the following code to save():

guard let titleString = titleTextField.text
    where titleString.characters.count > 0 else { return }
let date: NSDate?
if let dateText = self.dateTextField.text
    where dateText.characters.count > 0 {
        date = dateFormatter.dateFromString(dateText)
} else {
    date = nil
}
let descriptionString: String?
if descriptionTextField.text?.characters.count > 0 {
    descriptionString = descriptionTextField.text
} else {
    descriptionString = nil
}
if let locationName = locationTextField.text
    where locationName.characters.count > 0 {
        if let address = addressTextField.text
            where address.characters.count > 0 {
                
                geocoder.geocodeAddressString(address) {
                    [unowned self] (placeMarks, error) -> Void in
                    
                    let placeMark = placeMarks?.first
                    
                    let item = ToDoItem(title: titleString,
                        itemDescription: descriptionString,
                        timestamp: date?.timeIntervalSince1970,
                        location: Location(name: locationName,
                            coordinate: placeMark?.location?.coordinate))
                    
                    self.itemManager?.addItem(item)
                }
        }
}

Let's go over the code step by step.

Firstly, we use a guard to get the string from the Title text field. If there is nothing in the field, we immediately return from the method. Next, we get the date and description of the to-do item from the corresponding text fields. The date is created from the string in the text field using a date formatter. Add the date formatter right above save():

let dateFormatter: NSDateFormatter = {
    let dateFormatter = NSDateFormatter()
    dateFormatter.dateFormat = "MM/dd/yyyy"
    return dateFormatter
}()

Then, we check whether a name is given in the Location text field. If this is the case, we check whether an address is given in the Address text field. In this case, we get the coordinate from the geocoder, create the to-do item, and add it to the item manager.

Run the tests. All the tests pass and there is nothing to refactor.

The implementation of save() is not finished yet. The minimal input a user has to give is the title. Add tests for the to-do items with less information given by the user (or download the source code for the book and have a look at it).

The last test for this chapter is that the Save button is connected to the save() action. Add the following test to InputViewControllerTests:

func test_SaveButtonHasSaveAction() {
    let saveButton: UIButton = sut.saveButton
    
    guard let actions = saveButton.actionsForTarget(sut,
        forControlEvent: .TouchUpInside) else {
            XCTFail(); return
    }
    
    XCTAssertTrue(actions.contains("save"))
}

We get the Save button and guard that it has at least one action. If not, we fail the test using XCTFail(). Then, we assert that the actions array has a method, the "save" selector.

Run the tests. The last test fails.

Change the signature of the save method to @IBAction func save(), and connect it to the Save button in the storyboard scene (by control-dragging from the button in the storyboard to the IBAction in code).

Run the tests again. Now, 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
3.135.213.214