Поиск…


замечания

Дополнительную информацию по этой теме см. В разделе « Протокольное программирование WWDC 2015» в Swift .

Существует также большое письменное руководство по тому же: Введение в протокол-ориентированное программирование в Swift 2 .

Использование программно-ориентированного программирования для модульного тестирования

Программно-ориентированное программирование - полезный инструмент, позволяющий легко писать лучшие модульные тесты для нашего кода.

Предположим, мы хотим протестировать UIViewController, который полагается на класс ViewModel.

Необходимые шаги для производственного кода:

  1. Определите протокол, который предоставляет открытый интерфейс класса ViewModel со всеми свойствами и методами, необходимыми для UIViewController.
  2. Реализовать реальный класс ViewModel, соответствующий этому протоколу.
  3. Используйте метод инъекции зависимостей, чтобы позволить контроллеру представления использовать нужную нам реализацию, передавая его как протокол, а не конкретный экземпляр.
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")

Затем, при модульном тесте:

  1. Внедрить макет ViewModel, который соответствует одному протоколу
  2. Передайте его тестируемому UIViewController с использованием инъекции зависимостей вместо реального экземпляра.
  3. Тестовое задание!
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)
    }
}

Использование протоколов в качестве типов первого класса

Программно ориентированное по протоколу программное обеспечение может использоваться в качестве основного шаблона проектирования Swift.

Различные типы могут соответствовать одному протоколу, типы значений могут даже соответствовать нескольким протоколам и даже обеспечивать реализацию метода по умолчанию.

Первоначально определены протоколы, которые могут представлять собой обычно используемые свойства и / или методы с конкретными или генерическими типами.

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

Может быть создана реализация по умолчанию для метода get, но при желании соответствующие типы могут переопределить реализацию.

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

Тип значения, который соответствует протоколу ItemData, этот тип значения также может соответствовать другим протоколам.

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

Здесь элемент struct расширяется, чтобы соответствовать элементу отображения.

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

Пример сайта вызова для использования метода статического get.

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

Различные варианты использования потребуют разных реализаций. Основная идея здесь заключается в том, чтобы показать соответствие от разных типов, где протокол является основной точкой фокусировки в дизайне. В этом примере, возможно, данные API условно сохраняются в объекте Core Data.

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

Здесь класс поддержки Core Data также может соответствовать протоколу DisplayItem.

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
Лицензировано согласно CC BY-SA 3.0
Не связан с Stack Overflow