Commit de34245

derdilla <82763757+NobodyForNothing@users.noreply.github.com>
2024-05-05 08:24:54
test ble read cubit
Signed-off-by: derdilla <82763757+NobodyForNothing@users.noreply.github.com>
1 parent 68684a2
Changed files (3)
app/lib/bluetooth/ble_read_cubit.dart
@@ -1,6 +1,5 @@
 import 'dart:async';
 
-import 'package:blood_pressure_app/bluetooth/flutter_blue_plus_mockable.dart';
 import 'package:blood_pressure_app/bluetooth/logging.dart';
 import 'package:collection/collection.dart';
 import 'package:flutter/cupertino.dart';
@@ -13,15 +12,28 @@ part 'ble_read_state.dart';
 /// Logic for reading a characteristic from a device.
 ///
 /// May only be used on devices that are fully connected.
+///
+/// For using this Cubit the flow is as follows:
+/// 1. Create a instance with the initial [BleReadInProgress] state
+/// 2. Wait for either a [BleReadFailure] or a [BleReadSuccess].
+///
+/// When a read failure is emitted, the only way to try again is to create a new
+/// cubit. This should be accompanied with reconnecting to the [_device].
+///
+/// Internally the class performs multiple steps to successfully read data, if
+/// one of them fails the entire cubit fails:
+/// 1. Discover services
+/// 2. If the searched service is found read characteristics
+/// 3. If the searched characteristic is found read its value
+/// 4. If binary data is read decode it to object
+/// 5. Emit decoded object
 class BleReadCubit extends Cubit<BleRead> {
   /// Start reading a characteristic from a device.
-  BleReadCubit(this._flutterBluePlus, this._device)
+  BleReadCubit(this._device)
     : super(BleReadInProgress()) {
    unawaited(_startRead());
   }
 
-  final FlutterBluePlusMockable _flutterBluePlus;
-
   /// Bluetooth device to connect to.
   ///
   /// Must have an active established connection and support the measurement
@@ -35,8 +47,9 @@ class BleReadCubit extends Cubit<BleRead> {
     try {
       allServices = await _device.discoverServices();
     } catch (e) {
-      emit(BleReadFailure());
       Log.err('service discovery', [_device, e]);
+      emit(BleReadFailure());
+      return;
     }
 
 
@@ -52,10 +65,10 @@ class BleReadCubit extends Cubit<BleRead> {
     // https://developer.nordicsemi.com/nRF51_SDK/nRF51_SDK_v4.x.x/doc/html/group___u_u_i_d___c_h_a_r_a_c_t_e_r_i_s_t_i_c_s.html#ga95fc99c7a99cf9d991c81027e4866936
     final allCharacteristics = service.characteristics;
     final characteristic = allCharacteristics.firstWhereOrNull(
-            (c) => c.uuid.str == '2A35');
+            (c) => c.uuid.str == '2a35');
     if (characteristic == null) {
-      emit(BleReadFailure());
       Log.err('no characteristic', [_device, allServices, allCharacteristics]);
+      emit(BleReadFailure());
       return;
     }
 
@@ -63,8 +76,9 @@ class BleReadCubit extends Cubit<BleRead> {
     try {
       data = await characteristic.read();
     } catch (e) {
-      emit(BleReadFailure());
       Log.err('read error', [_device, allServices, characteristic, e]);
+      emit(BleReadFailure());
+      return;
     }
 
     // TODO: decode data before emitting success.
app/lib/bluetooth/logging.dart
@@ -2,14 +2,17 @@ import 'package:flutter/foundation.dart';
 
 /// Simple class for manually logging in debug builds.
 class Log {
+  /// Disable error logging during testing.
+  static bool testExpectError = false;
+
   /// Log an error with stack trace in debug builds.
   static void err(String message, [List<Object>? dumps]) {
-    if (kDebugMode) {
+    if (kDebugMode && !testExpectError) {
       debugPrint('-----------------------------');
-      debugPrintStack();
       debugPrint('ERROR $message:');
+      debugPrintStack();
       for (final e in dumps ?? []) {
-        debugPrint(e);
+        debugPrint(e.toString());
       }
     }
   }
app/test/bluetooth/ble_read_cubit_test.dart
@@ -0,0 +1,94 @@
+import 'package:blood_pressure_app/bluetooth/ble_read_cubit.dart';
+import 'package:blood_pressure_app/bluetooth/logging.dart';
+import 'package:flutter_blue_plus/flutter_blue_plus.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:mockito/annotations.dart';
+import 'package:mockito/mockito.dart';
+
+@GenerateNiceMocks([
+  MockSpec<BluetoothDevice>(),
+  MockSpec<BluetoothService>(),
+  MockSpec<BluetoothCharacteristic>()
+])
+import 'ble_read_cubit_test.mocks.dart';
+
+void main() {
+  test('success path works', () async {
+    final characteristic = MockBluetoothCharacteristic();
+    when(characteristic.uuid).thenReturn(Guid('2A35'));
+    when(characteristic.read()).thenAnswer((_) async => [1,2,3]); // TODO
+
+    final service = MockBluetoothService();
+    when(service.uuid).thenReturn(Guid('1810'));
+    when(service.characteristics).thenReturn([characteristic]);
+
+    final BluetoothDevice device = MockBluetoothDevice();
+    when(device.discoverServices()).thenAnswer((_) async => [service]);
+
+    final cubit = BleReadCubit(device);
+    expect(cubit.state, isA<BleReadInProgress>());
+    await expectLater(cubit.stream, emits(isA<BleReadSuccess>()));
+  });
+  test('should fail when device not connected', () async {
+    final BluetoothDevice device = MockBluetoothDevice();
+    when(device.discoverServices()).thenThrow(FlutterBluePlusException(
+        ErrorPlatform.fbp, 'discoverServices', FbpErrorCode.deviceIsDisconnected.index, 'device is not connected'
+    ));
+
+    Log.testExpectError = true;
+    final cubit = BleReadCubit(device);
+    expect(cubit.state, isA<BleReadFailure>(), reason: 'fails fast. Having a '
+        'BleReadInProgress first would also be fine.');
+    Log.testExpectError = false;
+  });
+  test('should fail without matching service', () async {
+    final service = MockBluetoothService();
+    when(service.uuid).thenReturn(Guid('1811'));
+
+    final BluetoothDevice device = MockBluetoothDevice();
+    when(device.discoverServices()).thenAnswer((_) async => [service]);
+
+    Log.testExpectError = true;
+    final cubit = BleReadCubit(device);
+    expect(cubit.state, isA<BleReadInProgress>());
+    await expectLater(cubit.stream, emits(isA<BleReadFailure>()));
+    Log.testExpectError = false;
+  });
+  test('fails without matching characteristic', () async {
+    final characteristic = MockBluetoothCharacteristic();
+    when(characteristic.uuid).thenReturn(Guid('2A34'));
+
+    final service = MockBluetoothService();
+    when(service.uuid).thenReturn(Guid('1810'));
+    when(service.characteristics).thenReturn([characteristic]);
+
+    final BluetoothDevice device = MockBluetoothDevice();
+    when(device.discoverServices()).thenAnswer((_) async => [service]);
+
+    Log.testExpectError = true;
+    final cubit = BleReadCubit(device);
+    expect(cubit.state, isA<BleReadInProgress>());
+    await expectLater(cubit.stream, emits(isA<BleReadFailure>()));
+    Log.testExpectError = false;
+  });
+  test('fails when not able to read data', () async {
+    final characteristic = MockBluetoothCharacteristic();
+    when(characteristic.uuid).thenReturn(Guid('2A35'));
+    when(characteristic.read()).thenThrow(FlutterBluePlusException(
+        ErrorPlatform.fbp, 'discoverServices', FbpErrorCode.deviceIsDisconnected.index, 'device is not connected'
+    ));
+
+    final service = MockBluetoothService();
+    when(service.uuid).thenReturn(Guid('1810'));
+    when(service.characteristics).thenReturn([characteristic]);
+
+    final BluetoothDevice device = MockBluetoothDevice();
+    when(device.discoverServices()).thenAnswer((_) async => [service]);
+
+    Log.testExpectError = true;
+    final cubit = BleReadCubit(device);
+    expect(cubit.state, isA<BleReadInProgress>());
+    await expectLater(cubit.stream, emits(isA<BleReadFailure>()));
+    Log.testExpectError = false;
+  });
+}