Mock AVCaptureDeviceInput for unit tests
Asked Answered
B

1

1

I have a class that receives an AVCaptureDevice object as a parameter and I'd like to test it. I found the swizzle method to create an instance of AVCaptureDevice

extension AVCaptureDevice {  
  class func swizzle() {
    // Exchange class methods
    [
      (
        #selector(AVCaptureDevice.default(_:for:position:)),
        #selector(AVCaptureDevice.mockDefaultDevice(position:))
      ),
      (
        #selector(AVCaptureDevice.authorizationStatus(for:)),
        #selector(AVCaptureDevice.mockAuthorizationStatus)
      ),
      (
        #selector(AVCaptureDevice.requestAccess(for:completionHandler:)),
        #selector(AVCaptureDevice.mockRequestAccess)
      )
    ].forEach {
      if let original = class_getClassMethod(self, $0),
         let mock = class_getClassMethod(self, $1) {
        method_exchangeImplementations(original, mock)
      }
    }

    // Add NSObject.init implementation to init(mock:)
    [
      (
        #selector(AVCaptureDevice.init(mock:)),
        #selector(NSObject.init)
      )
    ].forEach {
      if let original = class_getInstanceMethod(self, $0),
         let mock = class_getInstanceMethod(self, $1) {
        print("Mock:::Init Exchange impl \(original) <-> \(mock)")
        method_setImplementation(original, method_getImplementation(mock))
      }
    }
  }

  @objc
  convenience init(mock: String) {
    fatalError("Fake method that should be replaced by NSObject.init")
  }

  @objc
  class func mockRequestAccess(completionHandler handler: @escaping (Bool) -> Void) {
    handler(true)
  }

  @objc
  class func mockAuthorizationStatus() -> AVAuthorizationStatus {
    return .authorized
  }
}

It worked well to create an instance of AVCaptureDevice using AVCaptureDevice.default(_:for:position:), but inside my class, this instance is used to create an AVCaptureDeviceInput object as follows:

let deviceInput = try AVCaptureDeviceInput(device: deviceCamera)

and passing the mocked instance of deviceCamera to AVCaptureDeviceInput is resulting in the error "Error Domain=AVFoundationErrorDomain Code=-11852 "Não pode usar (null)" UserInfo={NSLocalizedFailureReason=Este app não tem autorização para usar (null)., AVErrorDeviceKey=<AVCaptureDevice: 0x600000cf49c0 [(null)][]>, NSLocalizedDescription=Não pode usar (null)}"

I tried to use the swizzle method again to create an instance of AVCaptureDeviceInput

extension AVCaptureDeviceInput {
  class func swizzle() {
    [
      (
        #selector(AVCaptureDeviceInput.init(device:)),
        #selector(NSObject.init)
      )
    ].forEach {
      if let original = class_getInstanceMethod(self, $0),
         let mock = class_getInstanceMethod(self, $1) {
        method_setImplementation(original, method_getImplementation(mock))
      }
    }
  }
}

(and other variations of this code) but it did not work. When I activate this code calling AVCaptureDeviceInput.swizzle() my test freezes in AVCaptureDeviceInput(device:) throws

I found another question about AVCaptureDeviceInput in unit tests, but there is no solution either.

Does anyone have any idea about what I can do to test my class mocking AVCaptureDevice and AVCaptureDeviceInput? Any idea is welcome!

Bebeeru answered 5/6 at 22:13 Comment(0)
B
0

I think we need to make some changes inside the code and take help of protocols.

As you say "I have a class that receives an AVCaptureDevice object as a parameter and I'd like to test it" and "inside my class, this instance is used to create an AVCaptureDeviceInput object as follows:"

So pass both AVCaptureDevice and AVCaptureDeviceInput as parameters. And make use of protocols to create mockCaptureDevice and mockCaptureDeviceInput.

like as follows =>

protocol CaptureDeviceInputAbility {}
extension AVCaptureDeviceInput: CaptureDeviceInputAbility {}

protocol CaptureDeviceAbility {}
extension AVCaptureDevice: CaptureDeviceAbility {}

// Mock objects
class MockCaptureDeviceInput: CaptureDeviceInputAbility {}
class MockCaptureDevice: CaptureDeviceAbility {}

Note use protocols as types , instead on concrete types to add mockings! like below

func yourMethod(captureDevice: CaptureDeviceAbility, captureDeviceInput: 
CaptureDeviceInputAbility) {

}

let mockDevice = MockCaptureDevice()
let mockInput = MockCaptureDeviceInput()
yourMethod(captureDevice: mockDevice, captureDeviceInput: mockInput)
Bezoar answered 16/6 at 11:49 Comment(2)
I need the AVCaptureDevice to create the AVCaptureDeviceInput, how will I mock it? The init method of AVCaptureDeviceInput needs a valid AVCaptureDevice. See other question that follows your method: #56027678Bebeeru
Yes, I have consider that scene too. Instead of creating AVCaptureDeviceInput from AVCaptureDevice. You should try changing your code implementation by creating a method and having two parameters , one for captureDevice and one for CaptureDeviceInput. In testing pass two mock objects using protocols like I have explained in my previous comment and in actual code you should pass real objects. For that you need to modify your actual code.Bezoar

© 2022 - 2024 — McMap. All rights reserved.