Sök…


Anmärkningar

För mer information om detta ämne, se WWDC 2015- protokollorienterad programmering i Swift .

Det finns också en bra skriftlig guide om samma: Introduktion av protokollorienterad programmering i Swift 2 .

Utnyttja protokollorienterad programmering för enhetstestning

Protokollorienterad programmering är ett användbart verktyg för att enkelt kunna skriva bättre enhetstester för vår kod.

Låt oss säga att vi vill testa en UIViewController som förlitar sig på en ViewModel-klass.

De nödvändiga stegen på produktionskoden är:

  1. Definiera ett protokoll som visar det offentliga gränssnittet för klassen ViewModel, med alla egenskaper och metoder som behövs av UIViewController.
  2. Implementera den verkliga ViewModel-klassen, i enlighet med det protokollet.
  3. Använd en beroende-injektionsteknik för att låta visningskontrollern använda den implementering vi vill, och skickar den som protokollet och inte som den konkreta instansen.
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")

Sedan vid enhetstest:

  1. Implementera en hålig ViewModel som överensstämmer med samma protokoll
  2. Vidarebefordra den till UIViewController under test med beroendeinjektion istället för den verkliga instansen.
  3. Testa!
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)
    }
}

Använda protokoll som första klassstyper

Protokollorienterad programmering kan användas som ett centralt designmönster för Swift.

Olika typer kan överensstämma med samma protokoll, värdetyper kan till och med överensstämma med flera protokoll och till och med ge standardmetodimplementering.

Ursprungligen definieras protokoll som kan representera vanliga egenskaper och / eller metoder med antingen specifika eller generiska typer.

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)
}

En standardimplementering för get-metoden kan skapas, men om så önskas kan överensstämmande typer åsidosätta implementeringen.

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 { }

En värdetyp som överensstämmer med ItemData-protokollet, denna värdetyp kan också överensstämma med andra protokoll.

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

Här utvidgas artikelstrukturen så att den överensstämmer med ett visningsobjekt.

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())
    }
}

Ett exempel samtalssida för att använda den statiska get-metoden.

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
    })
    
}

Olika användningsfall kräver olika implementeringar. Huvudtanken här är att visa överensstämmelse från olika typer där protokollet är huvudpunkten för fokus i designen. I det här exemplet sparas kanske API-data villkorligt i en kärndataenhet.

// 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
}

Här kan klassen Core Data backas också anpassas till DisplayItem-protokollet.

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
Licensierat under CC BY-SA 3.0
Inte anslutet till Stack Overflow