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.
MockDevice combines several components to simulate a MIDI 2.0 device:
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 ?? "")")
MockDevice includes several presets for common device types:
| Preset | Description | Resources |
|---|---|---|
.korgModulePro | KORG Module Pro simulation | DeviceInfo, ResourceList, CMList, ChannelList |
.generic | Generic MIDI 2.0 device | DeviceInfo, ResourceList |
.rolandStyle | Roland-style device | DeviceInfo, ResourceList, PatchList |
.yamahaStyle | Yamaha-style device | DeviceInfo, ResourceList, VoiceList |
.minimal | Minimal for basic testing | DeviceInfo only |
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)
PEResponder supports different resource types for various testing needs:
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}
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)
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)
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)
}
}
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
}
}
stop() on MockDevice and managers after tests