[TDD] Initialize unavailable init() — Swift

Muhammad Alfiansyah
4 min readNov 22, 2024

--

Photo by Max Bender on Unsplash

When you want to Test your code that depend on external framework but you can't initialize class as prameters for some method.

Example in the AVFoundation, when you implement AVCapturePhotoCaptureDelegate and want to ensure photoOutput method called properly.

func photoOutput(_: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
// MARK: Your code here
}
https://islom-babaev.medium.com/tdd-with-corebluetooth-44daf52aa04f

So, how we can solve this ?

  • First we can solve this using Dynamic Object Creation (Creator class)
  • Second we can use Dynamic Method Calls (Objective-C Runtime)
  • And third we can use Method Swizzling with private initializer.

Dynamic Object Creation (Creator class)

@import Foundation;

@interface Creator : NSObject
+(id)create:(NSString*)className;
@end
#import <Foundation/Foundation.h>
#import "Creator.h"

@implementation Creator

+(id)create:(NSString*)className {
return [[NSClassFromString(className) alloc] init];
}

@end
#import "Creator.h"
Creator.create("AVCaptureOutput") as? AVCaptureOutput

Dynamic Method Calls (Objective-C Runtime)

If the class is part of a framework like UIKit or AVFoundation and you know it inherits from NSObject, you can use the Objective-C runtime to dynamically call an initializer.

let instance = (SomeClass.self as NSObject.Type).init()

This method bypasses Swift’s compile-time checks and attempts to call the initializer dynamically. If the class doesn’t support default initialization, this will result in a runtime error.

I got this implementation from from Acceptance Test Topic

Method Swizzling with private initializer

If the class has a private initializer (e.g., for testing purposes), you can use method swizzling or runtime hacks to access it.

struct Swizzler {
private let klass: AnyClass

init(_ klass: AnyClass) {
self.klass = klass
}

func injectNSObjectInit(into selector: Selector) {
let original = [
class_getInstanceMethod(klass, selector)
].compactMap { $0 }

let swizzled = [
class_getInstanceMethod(klass, #selector(NSObject.init))
].compactMap { $0 }

zip(original, swizzled).forEach {
method_setImplementation($0.0, method_getImplementation($0.1))
}
}
}

Then we can create Fake class and inject some properties in to that class

final class FakeAVCapturePhoto: AVCapturePhoto {
private var data: Data?
private var meta: [String : Any]

override func fileDataRepresentation() -> Data? {
return data
}

override var metadata: [String : Any] {
return meta
}

@objc
private convenience init(data: Data, meta: [String : Any]) {
self.init(data: data, meta: meta)
}

private class func fake(data: Data, meta: [String : Any]) -> FakeAVCapturePhoto? {
let fake = FakeAVCapturePhoto(data: data, meta: meta)
fake.data = data
fake.meta = meta
return fake
}

static func createFake(data: Data, meta: [String : Any]) -> FakeAVCapturePhoto? {
Swizzler(self).injectNSObjectInit(into: #selector(FakeAVCapturePhoto.init(data:meta:)))
return fake(data: data, meta: meta)
}
}

You can check also in this article
https://nshint.io/blog/2019/04/08/testing-the-camera-on-the-simulator/

So How we can used it ?

Here I give you some sample UnitTest with that method

final class PhotoCaptureManagerTests: XCTestCase {
func test_init_setsManagersAsDelegateOfAVCaptureVideoDataOutput() throws {
let sut = PhotoCaptureManager()

XCTAssertNotNil(sut.aVCapturePhotoOutput)
}

func test_startCapture_image() throws {
let exp = expectation(description: "Wait for UIImage")
let bundle = Bundle.init(for: AppDelegate.self)
let image = UIImage(named: "img_avatar_1", in: bundle, compatibleWith: nil)
let sut = PhotoCaptureManager()
sut.completion = { value in
XCTAssertEqual(value.pngData(), image?.pngData())
exp.fulfill()
}
let data = image?.pngData()
let aVCapturePhoto = FakeAVCapturePhoto
.createFake(
data: data!,
meta: [(kCGImagePropertyOrientation as String): CGImagePropertyOrientation.up.rawValue]
)!
// let aVCapturePhoto = try XCTUnwrap((AVCapturePhoto.self as NSObject.Type).init() as? AVCapturePhoto)
sut.photoOutput(sut.aVCapturePhotoOutput, didFinishProcessingPhoto: aVCapturePhoto, error: nil)
wait(for: [exp], timeout: 0.1)
}
}
final class VideoCaptureManagerTests: XCTestCase {
func test_init_setsManagersAsDelegateOfAVCaptureVideoDataOutput() throws {
let sut = VideoCaptureManager()

XCTAssertNotNil(sut.aVCaptureVideoDataOutput.sampleBufferDelegate)
}

func test_captureOutput_streamSampleBuffer() throws {
let exp = expectation(description: "Wait for CMSampleBuffer")
let sut = VideoCaptureManager()
// guard let aVCaptureOutput = Creator.create("AVCaptureOutput") as? AVCaptureOutput else {
// XCTFail("Expected to create an instance of AVCaptureOutput")
// return
// }
let aVCaptureOutput = try XCTUnwrap((AVCaptureOutput.self as NSObject.Type).init() as? AVCaptureOutput)
let bundle = Bundle.init(for: AppDelegate.self)
let image = UIImage(named: "img_avatar_1", in: bundle, compatibleWith: nil)
let cMSampleBuffer = image?.convertToPixleBuffer()?.convertToCMSampleBuffer()
let aVCaptureConnection = AVCaptureConnection(inputPorts: [], output: aVCaptureOutput)
sut.completion = { sampleBuffer in
XCTAssertEqual(sampleBuffer, cMSampleBuffer)
exp.fulfill()
}
sut.aVCaptureVideoDataOutput.sampleBufferDelegate?.captureOutput?(aVCaptureOutput, didOutput: cMSampleBuffer!, from: aVCaptureConnection)
wait(for: [exp], timeout: 0.1)
}

func test_startCapture_streamSampleBuffer() async throws {
var callCount = 0
let sut = VideoCaptureManager()
let sampleBuffer = try await sut.startCapture()
Task {
for await _ in sampleBuffer {
callCount += 1
}
XCTAssertEqual(callCount, 2)
}

let aVCaptureOutput = try XCTUnwrap((AVCaptureOutput.self as NSObject.Type).init() as? AVCaptureOutput)
let bundle = Bundle.init(for: AppDelegate.self)
let image = UIImage(named: "img_avatar_1", in: bundle, compatibleWith: nil)
let cMSampleBuffer = image?.convertToPixleBuffer()?.convertToCMSampleBuffer()
let aVCaptureConnection = AVCaptureConnection(inputPorts: [], output: aVCaptureOutput)

sut.aVCaptureVideoDataOutput.sampleBufferDelegate?.captureOutput?(aVCaptureOutput, didOutput: cMSampleBuffer!, from: aVCaptureConnection)
sut.aVCaptureVideoDataOutput.sampleBufferDelegate?.captureOutput?(aVCaptureOutput, didOutput: cMSampleBuffer!, from: aVCaptureConnection)
}
}

--

--

No responses yet