Skip to content

Commit

Permalink
New testing architecture (#200)
Browse files Browse the repository at this point in the history
New testing infrastructure:
- removed quick & nimble - moved to XCTests
- added scripts to generate testable RxBluetoothKit classes (all testable classes have „_” prefix)
- added script for generating CoreBluetooth mocks
- added tests for BluetoothManager.retrievePeripherals methods with use of this architecture
  • Loading branch information
pouljohn1 committed Jan 10, 2018
1 parent 5f06b46 commit 1bfd0eb
Show file tree
Hide file tree
Showing 26 changed files with 3,067 additions and 409 deletions.
3 changes: 1 addition & 2 deletions Cartfile.private
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
github "Quick/Nimble"
github "Quick/Quick"

4 changes: 1 addition & 3 deletions Cartfile.resolved
Original file line number Diff line number Diff line change
@@ -1,3 +1 @@
github "Quick/Nimble" "v7.0.2"
github "Quick/Quick" "v1.2.0"
github "ReactiveX/RxSwift" "4.0.0"
github "ReactiveX/RxSwift" "4.1.0"
239 changes: 193 additions & 46 deletions RxBluetoothKit.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

62 changes: 32 additions & 30 deletions Source/CBPeripheralDelegateWrapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import Foundation
import CoreBluetooth
import RxSwift

@objc class CBPeripheralDelegateWrapper: NSObject, CBPeripheralDelegate {
class CBPeripheralDelegateWrapper: NSObject, CBPeripheralDelegate {

let peripheralDidUpdateName = PublishSubject<String?>()
let peripheralDidModifyServices = PublishSubject<([CBService])>()
Expand All @@ -43,30 +43,30 @@ import RxSwift
let peripheralIsReadyToSendWriteWithoutResponse = PublishSubject<Void>()
let peripheralDidOpenL2CAPChannel = PublishSubject<(Any?, Error?)>()

@objc func peripheralDidUpdateName(_ peripheral: CBPeripheral) {
func peripheralDidUpdateName(_ peripheral: CBPeripheral) {
RxBluetoothKitLog.d("""
\(peripheral.logDescription) didUpdateName(name: \(String(describing: peripheral.name)))
""")
peripheralDidUpdateName.onNext(peripheral.name)
}

@objc func peripheral(_ peripheral: CBPeripheral, didModifyServices invalidatedServices: [CBService]) {
func peripheral(_ peripheral: CBPeripheral, didModifyServices invalidatedServices: [CBService]) {
RxBluetoothKitLog.d("""
\(peripheral.logDescription) didModifyServices(services:
[\(invalidatedServices.logDescription))]
""")
peripheralDidModifyServices.onNext(invalidatedServices)
}

@objc func peripheral(_ peripheral: CBPeripheral, didReadRSSI rssi: NSNumber, error: Error?) {
func peripheral(_ peripheral: CBPeripheral, didReadRSSI rssi: NSNumber, error: Error?) {
RxBluetoothKitLog.d("""
\(peripheral.logDescription) didReadRSSI(rssi: \(rssi),
error: \(String(describing: error)))
""")
peripheralDidReadRSSI.onNext((rssi.intValue, error))
}

@objc func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
RxBluetoothKitLog.d("""
\(peripheral.logDescription) didDiscoverServices(services
: \(String(describing: peripheral.services?.logDescription)),
Expand All @@ -75,9 +75,9 @@ import RxSwift
peripheralDidDiscoverServices.onNext((peripheral.services, error))
}

@objc func peripheral(_ peripheral: CBPeripheral,
didDiscoverIncludedServicesFor service: CBService,
error: Error?) {
func peripheral(_ peripheral: CBPeripheral,
didDiscoverIncludedServicesFor service: CBService,
error: Error?) {
RxBluetoothKitLog.d("""
\(peripheral.logDescription) didDiscoverIncludedServices(for:
\(service.logDescription), includedServices:
Expand All @@ -87,9 +87,9 @@ import RxSwift
peripheralDidDiscoverIncludedServicesForService.onNext((service, error))
}

@objc func peripheral(_ peripheral: CBPeripheral,
didDiscoverCharacteristicsFor service: CBService,
error: Error?) {
func peripheral(_ peripheral: CBPeripheral,
didDiscoverCharacteristicsFor service: CBService,
error: Error?) {
RxBluetoothKitLog.d("""
\(peripheral.logDescription) didDiscoverCharacteristicsFor(for:
\(service.logDescription), characteristics:
Expand All @@ -99,9 +99,9 @@ import RxSwift
peripheralDidDiscoverCharacteristicsForService.onNext((service, error))
}

@objc func peripheral(_ peripheral: CBPeripheral,
didUpdateValueFor characteristic: CBCharacteristic,
error: Error?) {
func peripheral(_ peripheral: CBPeripheral,
didUpdateValueFor characteristic: CBCharacteristic,
error: Error?) {
RxBluetoothKitLog.d("""
\(peripheral.logDescription) didUpdateValueFor(for:\(characteristic.logDescription),
value: \(String(describing: characteristic.value?.logDescription)),
Expand All @@ -111,9 +111,9 @@ import RxSwift
.onNext((characteristic, error))
}

@objc func peripheral(_ peripheral: CBPeripheral,
didWriteValueFor characteristic: CBCharacteristic,
error: Error?) {
func peripheral(_ peripheral: CBPeripheral,
didWriteValueFor characteristic: CBCharacteristic,
error: Error?) {
RxBluetoothKitLog.d("""
\(peripheral.logDescription) didWriteValueFor(for:\(characteristic.logDescription),
value: \(String(describing: characteristic.value?.logDescription)),
Expand All @@ -123,9 +123,9 @@ import RxSwift
.onNext((characteristic, error))
}

@objc func peripheral(_ peripheral: CBPeripheral,
didUpdateNotificationStateFor characteristic: CBCharacteristic,
error: Error?) {
func peripheral(_ peripheral: CBPeripheral,
didUpdateNotificationStateFor characteristic: CBCharacteristic,
error: Error?) {
RxBluetoothKitLog.d("""
\(peripheral.logDescription) didUpdateNotificationStateFor(
for:\(characteristic.logDescription), isNotifying: \(characteristic.isNotifying),
Expand All @@ -135,9 +135,9 @@ import RxSwift
.onNext((characteristic, error))
}

@objc func peripheral(_ peripheral: CBPeripheral,
didDiscoverDescriptorsFor characteristic: CBCharacteristic,
error: Error?) {
func peripheral(_ peripheral: CBPeripheral,
didDiscoverDescriptorsFor characteristic: CBCharacteristic,
error: Error?) {
RxBluetoothKitLog.d("""
\(peripheral.logDescription) didDiscoverDescriptorsFor
(for:\(characteristic.logDescription), descriptors:
Expand All @@ -148,19 +148,19 @@ import RxSwift
.onNext((characteristic, error))
}

@objc func peripheral(_ peripheral: CBPeripheral,
didUpdateValueFor descriptor: CBDescriptor,
error: Error?) {
func peripheral(_ peripheral: CBPeripheral,
didUpdateValueFor descriptor: CBDescriptor,
error: Error?) {
RxBluetoothKitLog.d("""
\(peripheral.logDescription) didUpdateValueFor(for:\(descriptor.logDescription),
value: \(String(describing: descriptor.value)), error: \(String(describing: error)))
""")
peripheralDidUpdateValueForDescriptor.onNext((descriptor, error))
}

@objc func peripheral(_ peripheral: CBPeripheral,
didWriteValueFor descriptor: CBDescriptor,
error: Error?) {
func peripheral(_ peripheral: CBPeripheral,
didWriteValueFor descriptor: CBDescriptor,
error: Error?) {
RxBluetoothKitLog.d("""
\(peripheral.logDescription) didWriteValueFor(for:\(descriptor.logDescription),
error: \(String(describing: error)))
Expand All @@ -176,11 +176,13 @@ import RxSwift
}

@available(OSX 10.13, iOS 11, *)
@objc func peripheral(_ peripheral: CBPeripheral, didOpen channel: CBL2CAPChannel?, error: Error?) {
func peripheral(_ peripheral: CBPeripheral, didOpen channel: CBL2CAPChannel?, error: Error?) {
RxBluetoothKitLog.d("""
\(peripheral.logDescription) didOpenL2CAPChannel(for:\(peripheral.logDescription),
error: \(String(describing: error)))
""")
peripheralDidOpenL2CAPChannel.onNext((channel, error))
}

func peripheralDidUpdateRSSI(_ peripheral: CBPeripheral, error: Error?) {}
}
1 change: 1 addition & 0 deletions Source/Characteristic.swift
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ public class Characteristic {
}

extension Characteristic: Equatable {}
extension Characteristic: UUIDIdentifiable {}

/// Compare two characteristics. Characteristics are the same when their UUIDs are the same.
///
Expand Down
1 change: 1 addition & 0 deletions Source/Service.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ public class Service {
}

extension Service: Equatable {}
extension Service: UUIDIdentifiable {}

/// Compare if services are equal. They are if theirs uuids are the same.
/// - parameter lhs: First service
Expand Down
3 changes: 0 additions & 3 deletions Source/UUIDIdentifiable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,6 @@ protocol UUIDIdentifiable {
var uuid: CBUUID { get }
}

extension Characteristic: UUIDIdentifiable {}
extension Service: UUIDIdentifiable {}

/// Filters an item list based on the provided UUID list. The items must conform to UUIDIdentifiable.
/// Only items returned whose UUID matches an item in the provided UUID list.
/// Each UUID should have at least one item matching in the items list. Otherwise the result is nil.
Expand Down
157 changes: 157 additions & 0 deletions Templates/Mock.swifttemplate
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import CoreBluetooth
@testable
import RxBluetoothKit
<%_
struct MethodInfo {
let name: String
let callName: String
let formattedName: String
let lastParamName: String?
}
typealias MethodName = String

class Utils {
static let classNamesToMock = ["CBCentralManager", "CBPeripheral", "CBDescriptor", "CBService", "CBCharacteristic", "CBL2CAPChannel", "CBPeer"]
static let protocolNamesToChange = ["CBPeripheralDelegate", "CBCentralManagerDelegate"]

static func capitalizeFirstLetter(_ text: String) -> String {
return text.prefix(1).uppercased() + text.dropFirst()
}

static func createMethodVariableNames(_ type: Type) -> [MethodName: MethodInfo] {
var methodVariableNames: [MethodName: MethodInfo] = [:]
for method in type.allMethods {
if let index = methodVariableNames.index(where: { _, value in value.callName == method.callName }) {
let methodInfo = methodVariableNames[index].value
let methodInfoLastParamName = methodInfo.lastParamName ?? ""
methodVariableNames[methodInfo.name] = MethodInfo(
name: methodInfo.name,
callName: methodInfo.callName,
formattedName: "\(methodInfo.callName)With\(capitalizeFirstLetter(methodInfoLastParamName))",
lastParamName: methodInfo.lastParamName
)
let methodLastParamName = method.parameters.last?.name ?? ""
methodVariableNames[method.name] = MethodInfo(
name: method.name,
callName: method.callName,
formattedName: "\(method.callName)With\(capitalizeFirstLetter(methodLastParamName))",
lastParamName: methodLastParamName
)
} else {
methodVariableNames[method.name] = MethodInfo(
name: method.name,
callName: method.callName,
formattedName: method.callName,
lastParamName: method.parameters.last?.name
)
}
}
return methodVariableNames
}

static func changeTypeNameToMock(_ typeName: TypeName) -> String {
var unwrappedTypeName = typeName.unwrappedTypeName
if classNamesToMock.contains(unwrappedTypeName) {
unwrappedTypeName = unwrappedTypeName + "Mock"
}
if protocolNamesToChange.contains(unwrappedTypeName) {
unwrappedTypeName = "_" + unwrappedTypeName
}
if let array = typeName.array {
let elementName = changeTypeNameToMock(array.elementTypeName)
unwrappedTypeName = "[\(elementName)]"
}
var typeName = typeName.isOptional ? "\(unwrappedTypeName)?" : unwrappedTypeName
return typeName
}

static func printVariable(_ variable: Variable) -> String {
let typeName = changeTypeNameToMock(variable.typeName)
let forceUnwrap = variable.isOptional ? "" : "!"
return "var \(variable.name): \(typeName)\(forceUnwrap)"
}

static func printMethodParamTypes(_ method: SourceryRuntime.Method) -> String {
return method.parameters.reduce("", { "\($0)\(changeTypeNameToMock($1.typeName)), " }).dropLast(2)
}

static func printMethodName(_ method: SourceryRuntime.Method) -> String {
var methodParams = method.parameters.reduce("", { value, parameter in
var labelPart = ""
if (value.count == 0 && parameter.argumentLabel == nil) {
labelPart = "_ "
} else if (parameter.argumentLabel != nil && parameter.argumentLabel != parameter.name) {
labelPart = "\(parameter.argumentLabel!) "
}
var typePart = changeTypeNameToMock(parameter.typeName)
var defaultPart = parameter.defaultValue != nil ? " = \(parameter.defaultValue!)" : ""
return "\(value)\(labelPart)\(parameter.name): \(typePart)\(defaultPart), "
}).dropLast(2)
return "\(method.callName)(\(methodParams))"
}
}
-%>

<%_ for classNameToMock in Utils.classNamesToMock {
let classToMock = type[classNameToMock]! -%>
class <%= classToMock.name %>Mock: NSObject {
<%_ for variable in classToMock.allVariables { -%>
<%= Utils.printVariable(variable) %>
<% } -%>

<%_ let mainInit = classToMock.initializers.filter({ !$0.isConvenienceInitializer }).first -%>
<%= mainInit != nil ? Utils.printMethodName(mainInit!) : "override init()" %> {
}

<%_ let methodVariableNames = Utils.createMethodVariableNames(classToMock)
let filteredMethods = classToMock.allMethods.filter { !$0.isInitializer }
for method in filteredMethods {
let formattedName = methodVariableNames[method.name]!.formattedName
let methodParamsName = "\(formattedName)Params"
let methodReturnsName = "\(formattedName)Returns"
let isReturningType = !method.returnTypeName.isVoid
let methodReturnDeclaration = isReturningType ? " -> \(Utils.changeTypeNameToMock(method.returnTypeName))" : "" -%>
var <%= methodParamsName %>: [(<%= Utils.printMethodParamTypes(method) %>)] = []
<% if isReturningType { -%>
var <%= methodReturnsName %>: [<%= Utils.changeTypeNameToMock(method.returnTypeName) %>] = []
<% } -%>
func <%= Utils.printMethodName(method) %><%= methodReturnDeclaration %> {
<%= methodParamsName %>.append((<%= method.parameters.reduce("", { "\($0)\($1.name), " }).dropLast(2) %>))
<% if isReturningType { -%>
if <%= methodReturnsName %>.isEmpty {
fatalError("No return value")
} else {
return <%= methodReturnsName %>.removeFirst()
}
<% } -%>
}

<% } -%>
}

extension <%= classToMock.name %>Mock: Loggable {
@objc var logDescription: String {
return "<%= classToMock.name %>Mock"
}
}

<% } -%>

<%_ for protocolNameToChange in Utils.protocolNamesToChange {
let protocolToChange = type[protocolNameToChange]!
let inheritedTypesString = protocolToChange.inheritedTypes.reduce("", { "\($0)\($1), " }).dropLast(2)
-%>
protocol _<%= protocolToChange.localName %> : <%= inheritedTypesString %> {
<%_ let methodVariableNames = Utils.createMethodVariableNames(protocolToChange)
let filteredMethods = protocolToChange.allMethods.filter { !$0.isInitializer }
for method in filteredMethods {
let isReturningType = !method.returnTypeName.isVoid
let methodReturnDeclaration = isReturningType ? " -> \(Utils.changeTypeNameToMock(method.returnTypeName))" : ""
-%>
func <%= Utils.printMethodName(method) %><%= methodReturnDeclaration %>
<%_ } -%>
}
<%_ } -%>



Loading

0 comments on commit 1bfd0eb

Please sign in to comment.