RanchForecast with Unit Testing: Swift 4.2 and Xcode 10.1


#1

Attached is the RanchForecast code updated for Swift 4.2 and Xcode 10.1. Some additions have been included as noted by others in this section (such as the correction recommended in the topic “Errata: testCreateCourseFromValidDict() is severely flawed”, and the use of @testable.). Hope it helps.

//
//  AppDelegate.swift
//  RanchForecast
//
//  Created by Juan Pablo Claude on 2/16/15.
//  Copyright (c) 2015 Big Nerd Ranch. All rights reserved.
//
// Updated by Frank on 12/02/18.

import Cocoa

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate
{

    var mainWindowController: MainWindowController?
    
    
    func applicationDidFinishLaunching(_ aNotification: Notification)
    {
        // Create a window controller
        let mainWindowController = MainWindowController()
        
        // Put the window of the window controller on screen
        mainWindowController.showWindow(self)
        
        // Set the property to point to the window controller
        self.mainWindowController = mainWindowController
    }


}



//
//  MainWindowController.swift
//  RanchForecast
//
//  Created by Juan Pablo Claude on 2/16/15.
//  Copyright (c) 2015 Big Nerd Ranch. All rights reserved.
//
// Updated by Frank on 12/02/18.

import Cocoa

class MainWindowController: NSWindowController
{
    @IBOutlet var tableView: NSTableView!
    @IBOutlet var arrayController: NSArrayController!
    
    let fetcher = ScheduleFetcher()
    @objc dynamic var courses: [Course] = []
    
    
    override var windowNibName: NSNib.Name?
    {
        return "MainWindowController"
    }
    
    
    override func windowDidLoad()
    {
        super.windowDidLoad()
        
        tableView.target = self
        tableView.doubleAction = #selector(openClass(sender:))

        fetcher.fetchCoursesUsing(completionHandler: {(result) in
                                                        switch result
                                                        {
                                                        case .Success(let courses):
                                                            print("Got courses: \(courses)")
                                                            self.courses = courses
                                                        case .Failure(let error):
                                                            print("Got error: \(String(describing: error))")
                                                            NSAlert(error: error!).runModal()
                                                            self.courses = []
                                                        }
                                                    })
    }
    
    @objc func openClass(sender: AnyObject!)
    {
        if let course = arrayController.selectedObjects.first as? Course
        {
            NSWorkspace.shared.open(course.url)
        }
    }
    
}



//
//  Course.swift
//  RanchForecast
//
//  Created by Juan Pablo Claude on 2/16/15.
//  Copyright (c) 2015 Big Nerd Ranch. All rights reserved.
//
// Updated by Frank on 12/02/18.

import Foundation

class Course: NSObject
{
    @objc dynamic var title: String         // needs to be @objc dynamic, for Cocoa Bindings
    let url: URL
    @objc dynamic var nextStartDate: Date   // needs to be @objc dynamic, for Cocoa Bindings
    
    init(title: String, url: URL, nextStartDate: Date)
    {
        self.title = title
        self.url = url
        self.nextStartDate = nextStartDate
        
        super.init()
    }
    
    
}



//
//  ScheduleFetcher.swift
//  RanchForecast
//
//  Created by Juan Pablo Claude on 2/16/15.
//  Copyright (c) 2015 Big Nerd Ranch. All rights reserved.
//
// Updated by Frank on 12/02/18.

// NB: IN XCODE 10.1, SWIFT 4.2, USE: “https”, AS CONNECTIONS MUST BE SECURE.
// ALSO: IN “App Sandbox”, CHECK “Outgoing Connections (Client)”

import Foundation

class ScheduleFetcher
{
    enum FetchCoursesResult
    {
        case Success([Course])
        case Failure(Error!)
        
        init(throwingClosure: () throws -> [Course])
        {
            do
            {
                let courses = try throwingClosure()
                self = .Success(courses)
            }
            catch
            {
                self = .Failure(error as Error)
            }
        }
    }
    
    let session: URLSession
    
    init()
    {
        let config = URLSessionConfiguration.default
        session = URLSession(configuration: config)
    }
    
    
    // MARK: - Fetch Method
    
    func fetchCoursesUsing(completionHandler: @escaping (FetchCoursesResult) -> (Void))
    {
        let url = URL(string: "https://bookapi.bignerdranch.com/courses.json")!
        let request = URLRequest(url: url)
        let task = session.dataTask(with: request,
                                    completionHandler: {(data, response, error) -> Void in
                                        let result: FetchCoursesResult
                                            = self.resultFromData(data, response: response, error: error)   // refactored code: placed now in .resultFromData
            
                                        OperationQueue.main.addOperation({completionHandler(result)})
                                    })
        task.resume()
    }
    
    
    func errorWithCode(code: Int, localizedDescription: String) -> NSError
    {
        return NSError(domain: "ScheduleFetcher", code: code, userInfo: [NSLocalizedDescriptionKey : localizedDescription])
    }
    
    
    // MARK: - Create 'Course' Object
    
    func courseFromDictionary(_ courseDict: [String : Any]) -> Course?
    {
        let title = courseDict["title"] as! String
        let urlString = courseDict["url"] as! String
        let upcomingArray = courseDict["upcoming"] as! [[String : Any]]
        let nextUpcomingDict = upcomingArray.first!
        let nextStartDateString = nextUpcomingDict["start_date"] as! String
        
        let url = URL(string: urlString)!
        
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "yyyy-MM-dd"
        let nextStartDate = dateFormatter.date(from: nextStartDateString)!
        
        return Course(title: title, url: url, nextStartDate: nextStartDate)
    }


    func coursesFromData(data: Data) throws -> [Course] {
        let topLevelDict = try JSONSerialization.jsonObject(with: data as Data,
                                                            options: [])
        as! [String : Any]
        
        let courseDicts = topLevelDict["courses"] as! [[String : Any]]
        var courses: [Course] = []
        
        for courseDict in courseDicts {
            if let course = courseFromDictionary(courseDict) {
                courses.append(course)
            }
        }
        return courses
    }
    
    
    
    func resultFromData(_ data: Data!, response: URLResponse!, error: Error!) -> FetchCoursesResult
    {
        var result: FetchCoursesResult
        
        if data == nil
        {
            result = .Failure(error)
        }
        else if let response = response as? HTTPURLResponse
        {
            print("\(data!.count) bytes, HTTP \(response.statusCode).")
            if response.statusCode == 200
            {
//                result = self.resultFromData(data!, response: response, error: error)
                result = FetchCoursesResult { try self.coursesFromData(data: data) }
            }
            else
            {
                let error = self.errorWithCode(code: 1, localizedDescription: "Bad status code \(response.statusCode)")
                result = .Failure(error)
            }
        }
        else
        {
            let error = self.errorWithCode(code: 1, localizedDescription: "Unexpected response object.")
            result = .Failure(error)
        }
        
        return result
    }
}



//
//  Constants.swift
//  RanchForecastTests
//
//  Created by Juan Pablo Claude on 2/16/15.
//  Copyright (c) 2015 Big Nerd Ranch. All rights reserved.
//
// Updated by Frank on 12/02/18.

import Foundation

class Constants
{
    static let urlString = "http://training.bignerdranch.com/classes/test-course"
    static let url = URL(string: urlString)!
    static let title = "Test Course"
    
    static let dateString = "2014-06-02"
    static let dateFormatter: DateFormatter =
    {
        let formatter = DateFormatter()
        formatter.dateFormat = "yyyy-MM-dd"
        return formatter
    }()
    
    static let date = dateFormatter.date(from: dateString)!
    
    static let validCourseDict: [String : Any] =
        [ "title" : title,
          "url" : urlString,
          "upcoming" : [["start_date" : dateString]]
        ]
    
    // Add three immutable shared fixtures for the JSON data and HTTP response:
    static let coursesDictionary = ["courses" : [validCourseDict]]
    static let okResponse = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)
    static let jsonData = try! JSONSerialization.data(withJSONObject: coursesDictionary, options: [])
}



//
//  CourseTests.swift
//  RanchForecast
//
//  Created by Juan Pablo Claude on 2/16/15.
//  Copyright (c) 2015 Big Nerd Ranch. All rights reserved.
//
// Updated by Frank on 12/02/18.


import XCTest
@testable import RanchForecast
// NB: you can now use @testable in front of "import RanchForecast", instead of adding the "public" access modifier to the Course class, it's properties and it's methods.
// NB: Xcode might still give a "RanchForecast Module" is missing warning, after including "@testable".  This is a bug?  This warning/error will go away when you begin testing or building the project.

class CourseTests: XCTestCase
{

    func testCourseInitialization()
    {
        let course = Course(title: Constants.title,
                            url: Constants.url,
                            nextStartDate: Constants.date)
        
        XCTAssertEqual(course.title, Constants.title)
        XCTAssertEqual(course.url, Constants.url)
        XCTAssertEqual(course.nextStartDate, Constants.date)
    }

}



//
//  ScheduleFetcherTests.swift
//  RanchForecastTests
//
//  Created by Juan Pablo Claude on 2/16/15.
//  Copyright (c) 2015 Big Nerd Ranch. All rights reserved.
//
// Updated by Frank on 12/02/18.

import XCTest
@testable import RanchForecast

class ScheduleFetcherTests: XCTestCase
{
    var fetcher: ScheduleFetcher!
    
    override func setUp()
    {
        super.setUp()
        fetcher = ScheduleFetcher()
    }
    
    override func tearDown()
    {
        fetcher = nil
        super.tearDown()
    }
    
    
    func testCreateCourseFromValidDictionary()
    {
//        let course: Course! = fetcher.courseFromDictionary(Constants.validCourseDict)
//        XCTAssertNotNil(course)
        
        // As noted by 'tkrajacic' on the Cocoa Programming for Mac OS X Forum:
        // The above commented out statements "will fail correctly if the course could not be parsed but execution of the test will continue.
        // The next line will then execute with course == nil and hence crash."
        //
        // Instead use:
        guard let course: Course = fetcher.courseFromDictionary(Constants.validCourseDict)
            else
        {
            XCTAssertNotNil("Parsing failed")
            return
        }


        XCTAssertEqual(course.title, Constants.title)
        XCTAssertEqual(course.url, Constants.url)
        XCTAssertEqual(course.nextStartDate, Constants.date)
    }
    
    
    func testResultFromValidHTTPResponseAndValidData()
    {
        let result = fetcher.resultFromData(Constants.jsonData, response: Constants.okResponse, error: nil)
        
        switch result
        {
            case .Success(let courses):
                XCTAssert(courses.count == 1)
                let theCourse = courses[0]
                XCTAssertEqual(theCourse.title, Constants.title)
                XCTAssertEqual(theCourse.url, Constants.url)
                XCTAssertEqual(theCourse.nextStartDate, Constants.date)
            default:
                XCTFail("Result contains Failure, but Success was expected.")
        }
    }
    
    
}



//
//  RanchForecastTests.swift
//  RanchForecastTests
//
//  Created by Juan Pablo Claude on 2/16/15.
//  Copyright (c) 2015 Big Nerd Ranch. All rights reserved.
//
// Updated by Frank on 12/02/18.

import XCTest

class RanchForecastTests: XCTestCase {

    override func setUp()
    {
        // Put setup code here. This method is called before the invocation of each test method in the class.
    }

    override func tearDown()
    {
        // Put teardown code here. This method is called after the invocation of each test method in the class.
    }

    func testExample()
    {
        // This is an example of a functional test case.
        // Use XCTAssert and related functions to verify your tests produce the correct results.
    }

    func testPerformanceExample()
    {
        // This is an example of a performance test case.
        self.measure
        {
            // Put the code you want to measure the time of here.
        }
    }

}