Suche…


Bemerkungen

Weitere Informationen zu diesem Thema finden Sie im WWDC 2015-Vortrag Protokollorientierte Programmierung in Swift .

Es gibt auch einen großartigen schriftlichen Leitfaden zu diesem Thema: Einführung in die protokollorientierte Programmierung in Swift 2 .

Protokollorientierte Programmierung für Komponententests nutzen

Protokollorientierte Programmierung ist ein nützliches Werkzeug, um leicht bessere Komponententests für unseren Code zu schreiben.

Angenommen, wir möchten einen UIViewController testen, der auf einer ViewModel-Klasse basiert.

Die erforderlichen Schritte auf dem Produktionscode sind:

  1. Definieren Sie ein Protokoll, das die öffentliche Schnittstelle der Klasse ViewModel mit allen vom UIViewController benötigten Eigenschaften und Methoden verfügbar macht.
  2. Implementieren Sie die echte ViewModel-Klasse, die diesem Protokoll entspricht.
  3. Verwenden Sie eine Abhängigkeitseinspritzungstechnik, damit der View-Controller die gewünschte Implementierung verwendet und als Protokoll und nicht als konkrete Instanz übergeben kann.
protocol ViewModelType {
   var title : String {get}
   func confirm()
}

class ViewModel : ViewModelType {
   let title : String

   init(title: String) {
       self.title = title
   }
   func confirm() { ... }
}

class ViewController : UIViewController {
   // We declare the viewModel property as an object conforming to the protocol
   // so we can swap the implementations without any friction.
   var viewModel : ViewModelType! 
   @IBOutlet var titleLabel : UILabel!

   override func viewDidLoad() {
       super.viewDidLoad()
       titleLabel.text = viewModel.title
   }

   @IBAction func didTapOnButton(sender: UIButton) {
       viewModel.confirm()
   }
}

// With DI we setup the view controller and assign the view model.
// The view controller doesn't know the concrete class of the view model, 
// but just relies on the declared interface on the protocol.
let viewController = //... Instantiate view controller
viewController.viewModel = ViewModel(title: "MyTitle")

Dann beim Gerätetest:

  1. Implementieren Sie ein Mock-ViewModel, das demselben Protokoll entspricht
  2. Übergeben Sie es unter Verwendung der Abhängigkeitseinspritzung anstelle der realen Instanz an den zu testenden UIViewController.
  3. Prüfung!
class FakeViewModel : ViewModelType {
   let title : String = "FakeTitle"

   var didConfirm = false
   func confirm() {
       didConfirm = true
   }
}

class ViewControllerTest : XCTestCase {
    var sut : ViewController!
    var viewModel : FakeViewModel!

    override func setUp() {
        super.setUp()

        viewModel = FakeViewModel()
        sut = // ... initialization for view controller
        sut.viewModel = viewModel

        XCTAssertNotNil(self.sut.view) // Needed to trigger view loading
    } 

    func testTitleLabel() {
        XCTAssertEqual(self.sut.titleLabel.text, "FakeTitle")
    }

    func testTapOnButton() {
        sut.didTapOnButton(UIButton())
        XCTAssertTrue(self.viewModel.didConfirm)
    }
}

Protokolle als erstklassige Typen verwenden

Protokollorientierte Programmierung kann als zentrales Swift-Entwurfsmuster verwendet werden.

Verschiedene Typen sind in der Lage, dasselbe Protokoll zu erfüllen, Werttypen können sogar mehreren Protokollen entsprechen und sogar die Implementierung von Standardmethoden bereitstellen.

Zunächst werden Protokolle definiert, die häufig verwendete Eigenschaften und / oder Methoden mit bestimmten oder generischen Typen darstellen können.

protocol ItemData {
    
    var title: String { get }
    var description: String { get }
    var thumbnailURL: NSURL { get }
    var created: NSDate { get }
    var updated: NSDate { get }
    
}

protocol DisplayItem {
    
    func hasBeenUpdated() -> Bool
    func getFormattedTitle() -> String
    func getFormattedDescription() -> String

}

protocol GetAPIItemDataOperation {
    
    static func get(url: NSURL, completed: ([ItemData]) -> Void)
}

Eine Standardimplementierung für die Get-Methode kann erstellt werden. Falls gewünscht, kann jedoch die Implementierung durch entsprechende Typen überschrieben werden.

extension GetAPIItemDataOperation {
    
    static func get(url: NSURL, completed: ([ItemData]) -> Void) {
        
        let date = NSDate(
        timeIntervalSinceNow: NSDate().timeIntervalSince1970
            + 5000)
        
        // get data from url
        let urlData: [String: AnyObject] = [
            "title": "Red Camaro",
            "desc": "A fast red car.",
            "thumb":"http://cars.images.com/red-camaro.png",
            "created": NSDate(), "updated": date]
        
        // in this example forced unwrapping is used
        // forced unwrapping should never be used in practice
        // instead conditional unwrapping should be used (guard or if/let)
        let item = Item(
            title: urlData["title"] as! String,
            description: urlData["desc"] as! String,
            thumbnailURL: NSURL(string: urlData["thumb"] as! String)!,
            created: urlData["created"] as! NSDate,
            updated: urlData["updated"] as! NSDate)
        
        completed([item])
        
    }
}

struct ItemOperation: GetAPIItemDataOperation { }

Ein Werttyp, der dem ItemData-Protokoll entspricht. Dieser Werttyp kann auch mit anderen Protokollen übereinstimmen.

struct Item: ItemData {
    
    let title: String
    let description: String
    let thumbnailURL: NSURL
    let created: NSDate
    let updated: NSDate
    
}

Hier wird die Artikelstruktur so erweitert, dass sie einem Anzeigeelement entspricht.

extension Item: DisplayItem {
    
    func hasBeenUpdated() -> Bool {
        return updated.timeIntervalSince1970 >
            created.timeIntervalSince1970
    }
    
    func getFormattedTitle() -> String {
        return title.stringByTrimmingCharactersInSet(
            .whitespaceAndNewlineCharacterSet())
    }
    
    func getFormattedDescription() -> String {
        return description.stringByTrimmingCharactersInSet(
            .whitespaceAndNewlineCharacterSet())
    }
}

Eine beispielhafte Aufrufstelle für die Verwendung der statischen Get-Methode.

ItemOperation.get(NSURL()) { (itemData) in
    
    // perhaps inform a view of new data
    // or parse the data for user requested info, etc.
    dispatch_async(dispatch_get_main_queue(), { 
        
        // self.items = itemData
    })
    
}

Unterschiedliche Anwendungsfälle erfordern unterschiedliche Implementierungen. Die Hauptidee hierbei ist, die Konformität von verschiedenen Typen zu zeigen, bei denen das Protokoll den Schwerpunkt des Designs darstellt. In diesem Beispiel werden die API-Daten möglicherweise in einer Kerndatenentität gespeichert.

// the default core data created classes + extension
class LocalItem: NSManagedObject { }

extension LocalItem {
    
    @NSManaged var title: String
    @NSManaged var itemDescription: String
    @NSManaged var thumbnailURLStr: String
    @NSManaged var createdAt: NSDate
    @NSManaged var updatedAt: NSDate
}

Hier kann die mit Core Data unterstützte Klasse auch dem DisplayItem-Protokoll entsprechen.

extension LocalItem: DisplayItem {
    
    func hasBeenUpdated() -> Bool {
        return updatedAt.timeIntervalSince1970 >
            createdAt.timeIntervalSince1970
    }
    
    func getFormattedTitle() -> String {
        return title.stringByTrimmingCharactersInSet(
            .whitespaceAndNewlineCharacterSet())
    }
    
    func getFormattedDescription() -> String {
        return itemDescription.stringByTrimmingCharactersInSet(
            .whitespaceAndNewlineCharacterSet())
    }
}

// In use, the core data results can be
// conditionally casts as a protocol
class MyController: UIViewController {

    override func viewDidLoad() {
        
        let fr: NSFetchRequest = NSFetchRequest(
        entityName: "Items")
    
        let context = NSManagedObjectContext(
        concurrencyType: .MainQueueConcurrencyType)
        
        do {
            
            let items: AnyObject = try context.executeFetchRequest(fr)
            if let displayItems = items as? [DisplayItem] {
                
                print(displayItems)
            }
        
        } catch let error as NSError {
            print(error.localizedDescription)
        }
        
    }
}


Modified text is an extract of the original Stack Overflow Documentation
Lizenziert unter CC BY-SA 3.0
Nicht angeschlossen an Stack Overflow