dd-sdk-flutter icon indicating copy to clipboard operation
dd-sdk-flutter copied to clipboard

Make datadog sdk mock-able for unit tests

Open MuTe33 opened this issue 2 years ago • 2 comments

Are you requesting automatic instrumentation for a framework or library? Please describe.

  • Framework or library name : [datadog_flutter_plugin]
  • Library version: [1.6.0]

Is your feature request related to a problem? Please describe. I was trying to verify logging and rum calls, which the datadog SDK is making i.e. DatadogSdk.instance.rum?.addError() or DatadogSdk.instance.logs?.debug(), in unit tests.

Turns out there is no simple way of achieving that. There is the initializeForTesting method, which is more for E2E tests as it will assign NoOp instances. However, for unit tests I need to mock all of these instances to verify their calls, and since not all classes are being exposed from the package, I couldn't do that.

I might have missed something, but what is the best way of testing this?

Describe the solution you'd like

Allow for mocking for package specific classes.

Describe alternatives you've considered N/A

Additional context N/A

MuTe33 avatar Sep 26 '23 09:09 MuTe33

Hi @MuTe33 ,

Sorry for the delay on responding.

Do you want to be able to re-assign the rum and logs instances on DatadogSdk with mock versions? And, also, are you doing that to check your own code or check that our automatic instrumentation is making the right calls? Or both?

fuzzybinary avatar Oct 11 '23 19:10 fuzzybinary

For future readers, if you'd like to test YOUR calls to the DD SDK, what I typically do in these cases provide a "shim" around the DatadogSdk.instance call.

class DatadogSdkShim {
  DatadogSdk get instance => DatadogSdk.instance;
}

In my case, I have a class that manages Datadog calls from one location

class DatadogManager {
  static DatadogSdkShim shim = DatadogSdkShim();
...
}

And now, in that class, I can call the shim, instead of Datadog.instance directly.

class DatadogManager {
  static DatadogSdkShim shim = DatadogSdkShim();
 
  static Future<void> initializeDatadog(...) async {
     return shim.instance.initialize(...);
  }
  
  void createLogger() {
    final logger = shim.instance.logs?.createLogger(...);
    ...
  }
}

Now in tests, I can easily swap out the shim for a mocked one.

final sdk = MockDatadogSdk();
final shim = MockDatadogShim();
DatadogManager.shim = shim;

when(() => shim.instance).thenReturn(sdk);
when(() => sdk.initialize(any(), any()))
        .thenAnswer((invocation) => Future.value());

You could make the shim take the instance directly so you don't have to Mock it. Whatever suits your needs. You could also just directly set some static instance on the class itself to simplify.

class DatadogManager {
  static DatadogSdk? _sdk;
  
  @visibleForTesting // Set this so that you cannot set the SDK instance outside of testing
  static set sdk(DatadogSdk sdk) {
    _sdk = sdk;
  }

  // if _sdk isn't set (in normal runtime) call `DatadogSdk.instance`
  static DatadogSdk get sdk => _sdk ?? DatadogSdk.instance;
  
  static Future<void> initializeDatadog(...) async {
     return sdk.initialize(...);
  }
  ...
}

/// In test
final sdk = MockDatadogSdk();
DatadogManager.sdk = sdk;

when(() => sdk.initialize(any(), any()))
        .thenAnswer((invocation) => Future.value());

dballance avatar Apr 23 '24 15:04 dballance

I'm going to close this for lack of activity and because @dballance has provided an excellent workaround.

fuzzybinary avatar Jul 01 '24 14:07 fuzzybinary

Hi @fuzzybinary, I also think that the mocks suggested by MuTe33 would be handy, specially to avoid boilerplate in tests. Similar to some existing mocks for firebase, for instance.

Either way, as a (shorter) alternative to mocking the singleton instance proposed by dballance, you can just define an alias for the whole datadog initialization:

typedef DatadogRunner = Future<void> Function(
    DatadogConfiguration dc, TrackingConsent tc, void Function(),);

and then pass an object of this alias as a parameter to the method that triggers Datadog. In test code, we can pass any dummy lambda, and in prod. code we pass the real DatadogSdk.runApp call.

bernat-gomez avatar Jul 16 '24 12:07 bernat-gomez