← Back to Guides

Testing with MockDevice

MockDevice enables testing MIDI-CI and Property Exchange without physical MIDI hardware. Simulate real devices with customizable presets for comprehensive unit and integration testing.

New in v1.0.5. MockDevice, LoopbackTransport, and PEResponder provide complete in-process testing capabilities.

Overview

MockDevice combines several components to simulate a MIDI 2.0 device:

Quick Start

Create a loopback transport pair and set up MockDevice:

import MIDI2Kit
import MIDI2Transport
import MIDI2CI
import MIDI2PE

// 1. Create loopback transport pair
let (initiatorTransport, responderTransport) = LoopbackTransport.createPair()

// 2. Create MockDevice with preset
let mockDevice = MockDevice(
    transport: responderTransport,
    preset: .korgModulePro
)
try await mockDevice.start()

// 3. Create initiator-side managers
let ciManager = CIManager(
    transport: initiatorTransport,
    identity: DeviceIdentity(manufacturer: 0x00, family: 0x00, model: 0x00, revision: 0x0100)
)
let peManager = PEManager(transport: initiatorTransport, ciManager: ciManager)

try await ciManager.start()
try await peManager.startReceiving()

// 4. Start discovery - MockDevice responds automatically
await ciManager.startDiscovery()

// Wait for discovery
try await Task.sleep(for: .milliseconds(100))

// 5. Use PE operations
let response = try await peManager.get("DeviceInfo", from: mockDevice.handle)
print("DeviceInfo: \(response.bodyString ?? "")")

Available Presets

MockDevice includes several presets for common device types:

PresetDescriptionResources
.korgModuleProKORG Module Pro simulationDeviceInfo, ResourceList, CMList, ChannelList
.genericGeneric MIDI 2.0 deviceDeviceInfo, ResourceList
.rolandStyleRoland-style deviceDeviceInfo, ResourceList, PatchList
.yamahaStyleYamaha-style deviceDeviceInfo, ResourceList, VoiceList
.minimalMinimal for basic testingDeviceInfo only

Custom Resources

Add custom resources to MockDevice for testing specific scenarios:

// Add a simple resource
await mockDevice.setResource("Volume", value: ["level": 80, "muted": false])

// Get the resource via PE
let response = try await peManager.get("Volume", from: mockDevice.handle)

// Modify resource value
await mockDevice.setResource("Volume", value: ["level": 50, "muted": true])

// PE GET returns updated value
let updated = try await peManager.get("Volume", from: mockDevice.handle)

Resource Types

PEResponder supports different resource types for various testing needs:

InMemoryResource

Mutable resource that stores values in memory. Supports both GET and SET.

let resource = InMemoryResource(
    name: "Volume",
    initialValue: ["level": 80]
)
await mockDevice.peResponder.registerResource(resource)

// SET updates the value
try await peManager.set("Volume", data: ["level": 50], to: mockDevice.handle)

// GET returns the updated value
let response = try await peManager.get("Volume", from: mockDevice.handle)
// Returns: {"level": 50}

StaticResource

Immutable resource that always returns the same value. GET-only.

let resource = StaticResource(
    name: "FirmwareVersion",
    value: ["version": "1.2.3", "build": 456]
)
await mockDevice.peResponder.registerResource(resource)

ComputedResource

Dynamic resource that computes value on each GET. Useful for testing time-dependent values.

let resource = ComputedResource(name: "CurrentTime") {
    return ["timestamp": Date().timeIntervalSince1970]
}
await mockDevice.peResponder.registerResource(resource)

Unit Testing Example

Complete example for unit testing with Swift Testing:

import Testing
import MIDI2Kit
import MIDI2Transport
import MIDI2CI
import MIDI2PE

@Suite struct PropertyExchangeTests {
    @Test func getDeviceInfoReturnsExpectedData() async throws {
        // Setup
        let (initiator, responder) = LoopbackTransport.createPair()
        let mockDevice = MockDevice(transport: responder, preset: .generic)
        try await mockDevice.start()

        let ciManager = CIManager(transport: initiator, ...)
        let peManager = PEManager(transport: initiator, ciManager: ciManager)
        try await ciManager.start()

        await ciManager.startDiscovery()
        try await Task.sleep(for: .milliseconds(100))

        // Test
        let response = try await peManager.get("DeviceInfo", from: mockDevice.handle)

        // Verify
        #expect(response.isSuccess)
        #expect(response.bodyString?.contains("manufacturer") == true)

        // Cleanup
        await mockDevice.stop()
        await ciManager.stop()
    }

    @Test func setResourceUpdatesValue() async throws {
        let (initiator, responder) = LoopbackTransport.createPair()
        let mockDevice = MockDevice(transport: responder, preset: .minimal)
        await mockDevice.setResource("TestProp", value: ["value": 0])
        try await mockDevice.start()

        // ... setup managers ...

        // SET operation
        try await peManager.set("TestProp", data: ["value": 42], to: mockDevice.handle)

        // Verify update
        let response = try await peManager.get("TestProp", from: mockDevice.handle)
        #expect(response.bodyString?.contains("42") == true)
    }
}

Testing Subscriptions

MockDevice supports PE subscriptions for testing notification flows:

// Subscribe to resource changes
let subscription = try await peManager.subscribe("Volume", from: mockDevice.handle)

// Trigger a notification from MockDevice
await mockDevice.notifySubscribers("Volume", value: ["level": 100])

// Notification arrives via PEManager
for await notification in await peManager.makeNotificationStream() {
    if notification.resource == "Volume" {
        print("Volume changed: \(notification.body)")
        break
    }
}

Best Practices