使用 Swift 协议提高代码的可测试性
点击右侧关注,了解黑客的世界!
点击右侧关注,掌握进阶之路!
点击右侧关注,探讨技术话题!
译者丨Bruce
原文地址:
https://medium.com/flawless-app-stories/solving-dependencies-in-swift-9ee6ad4a8941
作为开发者,我们最大的挑战就是提升代码的可测试性。对于你开发的代码按照预期的方式执行以及开发新功能时没有别的功能被破坏来说,这些测试是非常有用的。同样,当你在一个多人协作开发的团队中时也是非常有用的。所以确保你代码的完整性是非常重要的。
有很多种测试,它们不应该使事情变得困难或复杂。为什么那么多的开发者不愿意做呢?主要的原因是没有时间。我觉得我们的代码最大的问题之一就是层与层之间、类与外部依赖之间的耦合太过紧密。
我想证明创建一个框架的抽象层或解耦类不应该是困难的任务。
想象我们需要开发一个需要用户位置的应用。因此我们需要CoreLocation
.
我们的ViewController
是这样的:
import UIKit
import CoreLocation
class ViewController: UIViewController {
var locationManager: CLLocationManager
var userLocation: CLLocation?
init(locationProvider: CLLocationManager = CLLocationManager()) {
self.locationManager = locationProvider
super.init(nibName: nil, bundle: nil)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
locationManager.delegate = self
}
func requestUserLocation() {
if CLLocationManager.authorizationStatus() == .authorizedWhenInUse {
locationManager.startUpdatingLocation()
} else {
locationManager.requestWhenInUseAuthorization()
}
}
}
extension ViewController: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
if status == .authorizedWhenInUse {
manager.startUpdatingLocation()
}
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
userLocation = locations.last
manager.stopUpdatingLocation()
}
}
它有一个locationManager
,请求用户的位置或请求授权(如果合适的话)。它同时遵循了CLLocationManagerDelegate
,用来接收代理事件。
我们看到我们的ViewController
和CoreLocation
耦合了以及与职责分离有关的其他问题。
无论如何,让我们为ViewController创建测试。这可能是一个很好的例子:
class ViewControllerTests: XCTestCase {
var sut: ViewController!
override func setUp() {
super.setUp()
sut = ViewController(locationProvider: CLLocationManager())
}
override func tearDown() {
sut = nil
super.tearDown()
}
func testRequestUserLocation() {
sut.requestUserLocation()
XCTAssertNotNil(sut.userLocation)
}
}
我们可以看到sut
和一个测试方法。然后我们请求了用户位置并把它存储在userLocation
。
问题出现了。CLLocationManager
管理了请求而它并不是一个同步操作,所以检查的userLocation
是空的。我们也有可能没有权限请求位置,这种情况下,位置也是nil
。
现在,我们有一些可能的解决方案。让我们在不测试任何与位置相关的东西的情况下测试ViewController
,创建CLLocationManager
的子类并模拟这些方法,或者尝试正确地进行测试并将CLLocationManager
与我们的类解耦。我选择后者。
“At the heart of Swift’s design are two incredibly powerful ideas: protocol-oriented programming and first class value semantics” - Apple 【Swift设计的核心是两个非常强大的概念:面向协议的编程和一流的值语义】
对开发者来说POP是一个强大的工具。Swift 毫无疑问是面向协议的语言。所以我的提议是用协议来解决这些依赖。
首先,为了抽象CLLocation
,我们将定义一个协议,其中只包含代码所需的变量或函数。
typealias Coordinate = CLLocationCoordinate2D
protocol UserLocation {
var coordinate: Coordinate { get }
}
extension CLLocation: UserLocation { }
现在我们不需要CoreLocation
就可以获取位置。所以如果我们分析ViewController
,我们可以看到我们并不真的需要CLLocationManager
,只需要在我们请求时提供用户位置的人。因此我们创建一个包含我们需要的协议,任何遵循这个协议的将成为提供者。
enum UserLocationError: Swift.Error {
case canNotBeLocated
}
typealias UserLocationCompletionBlock = (UserLocation?, UserLocationError?) -> Void
protocol UserLocationProvider {
func findUserLocation(then: @escaping UserLocationCompletionBlock)
}
在本例中,我们创建了UserLocationProvider
。该协议指定,我们只需要一个方法来请求用户的位置,结果将通过我们提供的回调。
我们准备创建一个UserLocationService
,它遵守该协议并为我们提供位置。顺便我们解决了CoreLocation
的依赖问题。但是等等,UserLocationService
需要通过CLLocationManager
来请求位置,似乎问题还是没有被解决。
同样,只需创建一个新协议来指定我们的位置提供者:
protocol LocationProvider {
var isUserAuthorized: Bool { get }
func requestWhenInUseAuthorization()
func requestLocation()
}
extension CLLocationManager: LocationProvider {
var isUserAuthorized: Bool {
return CLLocationManager.authorizationStatus() == .authorizedWhenInUse
}
}
我们拓展CLLocationManager
来遵循新的协议。
现在,我们准备好创建UserLocationService
,如下
class UserLocationService: NSObject, UserLocationProvider {
fileprivate var provider: LocationProvider
fileprivate var locationCompletionBlock: UserLocationCompletionBlock?
init(with provider: LocationProvider) {
self.provider = provider
super.init()
}
func findUserLocation(then: @escaping UserLocationCompletionBlock) {
self.locationCompletionBlock = then
if provider.isUserAuthorized {
provider.requestLocation()
} else {
provider.requestWhenInUseAuthorization()
}
}
}
extension UserLocationService: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
if status == .authorizedWhenInUse {
provider.requestLocation()
}
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
manager.stopUpdatingLocation()
if let location = locations.last {
locationCompletionBlock?(location, nil)
} else {
locationCompletionBlock?(nil, .canNotBeLocated)
}
}
}
UserLocationService
有自己的位置提供者,但他不知道它是谁,他也没必要知道,他只需要请求的时候拿到用户位置就行了,剩下的不是他的责任。
需要扩展来符合CLLocationManagerDelegate
协议,因为我们将使用CoreLocation
。但是在测试中,我们并不需要它来验证我们的类是否正常工作。
我们可以在协议中添加任何类型的委托,但是对于这个例子,我认为够了。
在开始测试之前,我们来看看用UserLocationProvider
替代CLLocationManager
的ViewController
。
class ViewControllerWithoutCL: UIViewController {
var locationProvider: UserLocationProvider
var userLocation: UserLocation?
init(locationProvider: UserLocationProvider) {
self.locationProvider = locationProvider
super.init(nibName: nil, bundle: nil)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func requestUserLocation() {
locationProvider.findUserLocation { [weak self] location, error in
if error == nil {
self?.userLocation = location
} else {
print("User can not be located ?")
}
}
}
}
让我们继续测试。首先,我们将创建一些模拟类来测试ViewController
。
struct UserLocationMock: UserLocation {
var coordinate: Coordinate {
return Coordinate(latitude: 51.509865, longitude: -0.118092)
}
}
class UserLocationProviderMock: UserLocationProvider {
var locationBlockLocationValue: UserLocation?
var locationBlockErrorValue: UserLocationError?
func findUserLocation(then: @escaping UserLocationCompletionBlock) {
then(locationBlockLocationValue, locationBlockErrorValue)
}
}
使用这个我们可以注入任何我们需要的结果的模拟数据,我们将可以模拟UserLocationProvider
如何工作。因此我们可以关注我们真正的目标ViewController
。
class ViewControllerWithoutCLTests: XCTestCase {
var sut: ViewControllerWithoutCL!
var locationProvider: UserLocationProviderMock!
override func setUp() {
super.setUp()
locationProvider = UserLocationProviderMock()
sut = ViewControllerWithoutCL(locationProvider: locationProvider)
}
override func tearDown() {
sut = nil
locationProvider = nil
super.tearDown()
}
func testRequestUserLocation_NotAuthorized_ShouldFail() {
locationProvider.locationBlockLocationValue = UserLocationMock()
locationProvider.locationBlockErrorValue = UserLocationError.canNotBeLocated
sut.requestUserLocation()
XCTAssertNil(sut.userLocation)
}
func testRequestUserLocation_Authorized_ShouldReturnUserLocation() {
locationProvider.locationBlockLocationValue = UserLocationMock()
sut.requestUserLocation()
XCTAssertNotNil(sut.userLocation)
}
}
我们创建了两个测试,一个检查如果我们没有请求位置的授权,提供者就不提供任何东西。另一个,相反的情况,如果我们被授权,我们应该获得用户的位置。正如您所看到的,测试通过了!!
除了ViewController
,我们还创建了一个额外的类UserLocationService
,因此我们也应该覆盖它。
LocationProvider
应该被 mock,尽管它不是此次测试的目标。
class LocationProviderMock: LocationProvider {
var isRequestWhenInUseAuthorizationCalled = false
var isRequestLocationCalled = false
var isUserAuthorized: Bool = false
func requestWhenInUseAuthorization() {
isRequestWhenInUseAuthorizationCalled = true
}
func requestLocation() {
isRequestLocationCalled = true
}
}
可以创建许多测试,验证提供者在我们没有请求授权或者我们请求位置时,是否说我们有授权,可以是其中之一。
class UserLocationServiceTests: XCTestCase {
var sut: UserLocationService!
var locationProvider: LocationProviderMock!
override func setUp() {
super.setUp()
locationProvider = LocationProviderMock()
sut = UserLocationService(with: locationProvider)
}
override func tearDown() {
sut = nil
locationProvider = nil
super.tearDown()
}
func testRequestUserLocation_NotAuthorized_ShouldRequestAuthorization() {
locationProvider.isUserAuthorized = false
sut.findUserLocation { _, _ in }
XCTAssertTrue(locationProvider.isRequestWhenInUseAuthorizationCalled)
}
func testRequestUserLocation_Authorized_ShouldNotRequestAuthorization() {
locationProvider.isUserAuthorized = true
sut.findUserLocation { _, _ in }
XCTAssertFalse(locationProvider.isRequestWhenInUseAuthorizationCalled)
}
}
可以想象,有许多方法可以解耦代码,而本文只是其中之一。但是我认为这是一个很好的例子来说明测试并不是一项困难的任务。
推荐↓↓↓
长
按
关
注
?【16个技术公众号】都在这里!
涵盖:程序员大咖、源码共读、程序员共读、数据结构与算法、黑客技术和网络安全、大数据科技、编程前端、Java、Python、Web编程开发、Android、iOS开发、Linux、数据库研发、幽默程序员等。
万水千山总是情,点个 “在看” 行不行
本文地址:https://blog.csdn.net/olsQ93038o99S/article/details/100149264