Mock AVCaptureDeviceInput for testing - swift

I'm trying to test how my App responds to different AVFoundation configurations. I'm using the techniques described in the WWDC video 'Engineering for Testability.'
I created a protocol to represent the pieces of an AVCaptureDevice that my app uses.
public protocol AVCaptureDeviceProperties: class {
//MARK: Properties I use and need to test
var position: AVCaptureDevice.Position { get }
var focusMode: AVCaptureDevice.FocusMode { get set }
var exposureMode: AVCaptureDevice.ExposureMode { get set }
var whiteBalanceMode: AVCaptureDevice.WhiteBalanceMode { get set }
//MARK: Functions I use use and need to test
func lockForConfiguration() throws
func unlockForConfiguration()
func isFocusModeSupported(_ focusMode: AVCaptureDevice.FocusMode) -> Bool
func isExposureModeSupported(_ exposureMode: AVCaptureDevice.ExposureMode) -> Bool
func isWhiteBalanceModeSupported(_ whiteBalanceMode: AVCaptureDevice.WhiteBalanceMode) -> Bool
}
I have an extension that makes AVCaptureDevice conform to my protocol.
extension AVCaptureDevice: AVCaptureDeviceProperties {
//Don't need anything because AVCaptureDevice already has implementations of all the properties and functions I use.
}
I can now make an object for myself where I can configure all the properties for different test cases. Works great!
However, I need to take it another step further and get a mock AVCaptureDeviceInput object. This object only has one initializer that takes a AVCaptureDevice but I want to be able to mock initialize with my protocol type. So far I have this:
extension AVCaptureDeviceInput {
convenience init?(device: AVCaptureDeviceProperties) throws {
guard let downcast = device as? AVCaptureDevice else {
return nil
}
try self.init(device: downcast)
}
}
However, I will never get a successful initialization with a mock object that conforms to my protocol. How to I solve this problem so I can test?

Related

How can I send fetched data to my ViewController?

Heres a caption of my API call:
So, I've got the abilities of the pokemons I needed, but now, idk how to get that data out of my Service class (where I'm doing all the parsing), and send it to my InfoViewController.
My purpose is to fetch that data on some label, and then show the ability names for every poke, according to their ID. Here is a caption of my app:
I wanna add an "Ability" label below Weight, and that's where I wanna assign the data. I have a whole CollectionView with all the pokemons, and the goal is assign the correct ability for each one of them.
I'm kinda struggling for a practical (and less verbose) way to reach this.
I apreciated every comment, any advice and suggestion too. Thanks!
EDIT: Heres my code:
extension InfoController: ServiceDelegate {
func finishedWithPokemonAbilities(abilities: [String], id: Int) {
self.abilities = abilities
self.ids = id
print(abilities)
}
}
You can create a custom Protocol(could call it PokemonServiceDelegate as an example) that your InfoViewController would inherit and implement. On your service object(I'm using PokemonService in the example) create a property with a type of PokemonServiceDelegate and set that property to the view controller that you want to receive the data. After the service finishes fetching the data, update the delegate by passing the data in the function declared in the protocol.
// Protocol your view controller will inherit
protocol PokemonServiceDelegate {
// Function your view controller will implement
func finishedWithPokemonAbilities(abilities: [String])
}
class InfoViewController: UIViewController {
// Reference to the service that makes the request
var service: PokemonService
override func viewDidLoad() {
...
// Set the delegate of the service to self
service.delegate = self
...
}
}
extension InfoViewController: PokemonServiceDelegate {
// Implement the protocol
func finishedWithPokemonAbilities(abilities: [String]) {
// Do something with their abilities here
}
}
struct PokemonService {
var delegate: PokemonServiceDelegate?
// The function that you call to get your abilities
func someUpdateFunc() {
...
let abilities = json[abilities].arrayValue.map {$0["ability"]["name"].stringValue}
delegate?.finishedWithPokemonAbilities(abilities: abilities)
...
}
}

Trying to call selector to static function in swift

I'm trying to achieve the following but am running into issues :-)
create a protocol that UIViewController and UIView subclass can adopt
which contain one static method to be called on this class (call it
configuration
I then want to use the objectiveC runtime to find the classes that adopt this protocol
On each of those class I want to call the configuration method
The configuration method is to return a dictionary (key: a description string, value: a selector to be called on the class)
So far I was able to create the protocol, find the class implementing the protocol but i'm running into compiling issues.
Here is the protocol
#objc public protocol MazeProtocol: NSObjectProtocol{
#objc static func configurations() -> NSDictionary
}
Here is the extension to adopt the protocol on one of my class:
extension MapCoordinatorViewController: MazeProtocol {
static func configurations() -> NSDictionary {
let returnValue = NSMutableDictionary()
returnValue.setObject(#selector(test), forKey: "test" as NSString)
return returnValue
}
#objc static func test() {
print("test")
}}
and here is the code i'm using to try to call the selector returned from the configuration method:
let selectorKey = controllerClass.configurations().allKeys[indexPath.row]
let selector = controllerClass.configurations().object(forKey: selectorKey)
controllerClass.performSelector(selector) <================ error here
ControllerClass is declared as let controllerClass: MazeProtocol.Type
I get the following compile warning:
Instance member 'performSelector' cannot be used on type 'MazeProtocol'
What am I missing?
You can technically force this to work. Please don't. This is horrible Swift. To get this to work, you have to undermine everything Swift is trying to do. But yes, with warnings, you can technically get this to compile and work. Please, please don't.
First, you need to make selector be a Selector. You're using an NSDictionary, which is terrible in Swift, and so you get Any? back. But, yes, you can as! cast it into what you want:
let selector = controllerClass.configurations().object(forKey: selectorKey) as! Selector
And then, defying all the type gods, you can just declare that classes are actually NSObjectProtocol, because why not?
(controllerClass as! NSObjectProtocol).perform(selector)
This will throw a warning "Cast from 'MapCoordinatorViewController.Type' to unrelated type 'NSObjectProtocol' always fails", but it will in fact succeed.
After all that "don't do this," how should you do this? With closures.
public protocol MazeProtocol {
static var configurations: [String: () -> Void] { get }
}
class MapCoordinatorViewController: UIViewController {}
extension MapCoordinatorViewController: MazeProtocol {
static let configurations: [String: () -> Void] = [
"test": test
]
static func test() {
print("test")
}
}
let controllerClass = MapCoordinatorViewController.self
let method = controllerClass.configurations["test"]!
method()

How to UnitTest asynchronous calls to delegate methods in Swift?

I have the following class with a static method that uses MKDirections to calculate custom routes between two coordinates. Once it finishes calculating, the method uses a delegate to pass the route (an MKPolyline object) to the View Controller which adds it to a MapView as an overlay. Every route is assigned a title that determines in which color the route is rendered on the map.
class NavigationInterface {
weak static var routeDelegate: RouteDelegate!
static func addRouteFromTo(sourceCoor: CLLocationCoordinate2D, destinationCoor: CLLocationCoordinate2D, transportTypeString: String)
{
let sourcePlacemark = MKPlacemark(coordinate: sourceCoor)
let destinationPlacemark = MKPlacemark(coordinate: destinationCoor)
//var route = MKRoute()
let request = MKDirectionsRequest()
request.source = MKMapItem(placemark: sourcePlacemark)
request.destination = MKMapItem(placemark: destinationPlacemark)
request.requestsAlternateRoutes = false
//get MKDirectionsTransportType based on String identifier
request.transportType = getTransportType(transportTypeString: transportTypeString)
let directions = MKDirections(request: request)
directions.calculate { (response, error) in
if let directionResponse = response?.routes.first {
let route = directionResponse.polyline
route.title = transportTypeString
print("Got Here")
self.routeDelegate!.didAddRoute(route: route)
}
}
}
The delegate is defined through the following protocol:
protocol RouteDelegate: class {
func didAddRoute(route: MKPolyline)
func didAddBoundary(boundary: MKPolygon)
}
The View Controller implements the delegate as follows:
class MapViewController: UIViewController {
#IBOutlet weak var mapView: MKMapView!
...
override func viewDidLoad() {
super.viewDidLoad()
NavigationInterface.routeDelegate = self
}
extension MapViewController: RouteDelegate {
// delegate Method
// called in Navigation Interface
func didAddRoute(route: MKPolyline) {
mapView.add(route)
}
func didAddBoundary(boundary: MKPolygon) {
mapView.add(boundary)
}
}
Now I have attempted to write a UnitTest which checks whether the delegate method "didAddRoute" returns the correct route
For this purpose I've created a test class "NavigationTests" which implements the RouteDelegate protocol and an test method that calculates a route and then evaluates the route returned from the "NavigationTests" protocol implementation of "didAddRoute":
class NavigationTests: XCTestCase, RouteDelegate {
var routes = [MKPolyline]()
var asyncExpectation: XCTestExpectation?
func didAddRoute(route: MKPolyline) {
routes.append(route)
asyncExpectation?.fulfill()
}
...
func testaddRouteFromTo(){
NavigationInterface.routeDelegate = self
asyncExpectation = expectation(description: "routes returned from delegate method")
NavigationInterface.addRouteFromTo(sourceCoor: CoordinateA, destinationCoor: CoordinateB, transportTypeString: "roadTravel")
let result = XCTWaiter.wait(for: [self.asyncExpectation!], timeout: 2.0)
if result == XCTWaiter.Result.completed {
let route = self.routes.first
XCTAssert(route!.title == "roadTravel", "failed to retrieve correct route")
print(route!.title)
} else {
XCTFail()
}
}
}
Now this test method randomly returns routes from the MapViewController implementation of RouteDelegate instead of the NavigationTests implementation. W
How can I avoid these unwanted references to the MapViewController and why is it created at all since I do not instantiate it in the test?
Ideally I would like to prevent the MapViewController from being instantiated when running this test class since it is not required for the Unit Test.
How can I make sure that only the NavigationTests implementation of the RouteDelegate is used?
Statics vs. tests
Because addRouteFromTo(sourceCoor:destinationCoor:transportTypeString:) is a static method, you have made NavigationInterface.routeDelegate static as well. When your tests run, they are setting a global variable. This means the tests have side effects which last beyond the scope of the tests.
Here are a couple of approaches to prevent this from happening:
a) Create a setUp() and tearDown(). In setUp(), save the old value of NavigationInterface.routeDelegate before overwriting it to self. In tearDown(), restore the old value.
b) Change from statics to an object. As a general rule, statics make things harder to test.
Prefer b). It is safer, and lets the pressure of testability improve your design.
…I don't see any references to MapViewController in your test. Was it created by your application delegate?
How to test an asynchronous call?
Now to your larger question. A test that does actual networking is slow and fragile. It depends on your network conditions. It depends on the back end. It introduces a time lag.
You would be better served by restructuring your code so that you can test the following:
Are you creating the correct MKDirectionsRequest?
Are you handling the response correctly?
This will be expressed in at least 2 tests, but probably more. Once you can independently test response handling, then you can test errors as well as successful responses.
So how do you test "create the response" independently from "handle the response"? By doing this work in separate methods. The tests can then just call these methods.
There is no need to test that Apple makes a network call, or does something on the back end, or sends a response. If you follow this approach, the need for asynchronous tests drops dramatically.
I hope this helps. If you need clarification, please ask. For more thoughts on how "the way Apple shows us to write code isn't good testable design," see https://qualitycoding.org/design-sense/

How to test different permissions of CLLocationManager in test cases

I am writing a test to test CoreLocation related function. This function will throw an error if the location services is not enabled.
func someFunction() throws {
guard CLLocationManager.locationServicesEnabled() throw NSError.init(
domain: kCLErrorDomain,
code: CLError.Code.denied.rawValue,
userInfo: nil)
}
...
}
In my tests, CLLocationManager.locationServicesEnabled() is always true. Is there a way to test the false scenario?
Instead of using static methods directly, wrap this call in a class and make that class adopt a protocol, so your code will depend on that interface instead of the concrete implementation.
protocol LocationManager {
var islocationServicesEnabled: Bool { get }
}
class CoreLocationManager: LocationManager {
var islocationServicesEnabled: Bool {
return CLLocationManager.locationServicesEnabled()
}
}
Then your function (or class) should receive that dependency instead of owning it (dependency injection):
func someFunction(locationManager: LocationManager) throws {
guard locationManager.islocationServicesEnabled else {
//...
}
}
In your tests you can pass a "Fake" LocationManager and test all the scenarios. (In your production code you pass the CoreLocationManager.)
That's a general advice for any dependency.

Convert NSSound to AVAudioPlayer

I have some NSSound objects that I'd like to convert to AVAudioPlayer instances. I have file paths (NSURLs) associated with the NSSound objects, but the original file may not exist. Here is what I have so far:
class SoundObj: NSObject {
var path: NSURL?
var sound: NSSound?
var player: AVAudioPlayer
}
let aSound = SoundObj()
aSound.path = NSURL(fileURLWithPath: "file:///path/to/sound.m4a")
aSound.sound = NSSound(contentsOfURL: aSound.path)!
do {
try aSound.player = AVAudioPlayer(contentsOfURL: aSound.path)
} catch {
// perhaps use AVAudioPlayer(data: ...)?
}
How do I convert an NSSound object to an AVAudioPlayer instance?
So I didn't see a public interface to get the URL from an NSSound object, so I went digging through the private headers to see what I could find. Turns out there are private instance method url and _url which return the URL of an NSSound. Presumably these are getters for an NSURL ivar or property.
With Objective-C this would be easy: we would just add the methods to a new interface or extension. With pure Swift things are a little trickier, and we need to expose the accessor via an Objective-C protocol:
#objc protocol NSSoundPrivate {
var url: NSURL? { get }
}
Since url is an instance method, you may get better results with func url() -> NSURL? instead of using a variable. Your milage may vary: using a var to emulate the behavior of a read-only property seemed to work for me.
I wrote a new convenience initializer in an extension on AVAudioPlayer:
extension AVAudioPlayer {
convenience init?(sound: NSSound) throws {
let privateSound = unsafeBitCast(sound, NSSoundPrivate.self)
guard let url = privateSound.url else { return nil }
do {
try self.init(contentsOfURL: url)
} catch {
throw error
}
}
}
Usage:
let url = NSURL(...)
if let sound = NSSound(contentsOfURL: url, byReference: true) {
do {
let player = try AVAudioPlayer(sound: sound)
player?.play()
} catch {
print(error)
}
}
After attempting to find anything related to NSData in the ivars, instance methods, and properties of an NSSound, I have come to the conclusion that the data portion of whatever you use to initialize an NSSound is obfuscated somewhere in the implementation of the class, and is not available like the URL is.