mockito icon indicating copy to clipboard operation
mockito copied to clipboard

Mockito and compatibility with generic methods

Open matanlurey opened this issue 6 years ago • 10 comments

Related issue was https://github.com/dart-lang/mockito/issues/152.

In Dart2, it is impossible to implement generic methods (correctly) with Mockito.

Specifically:

  1. It isn't possible to match on type arguments, which are part of the signature:
abstract class Fetcher {
  T fetchOfType<T>();
}

class MockFetcher extends Mock implements Fetcher {}

void main() {
  var fetcher = new MockFetcher();

  // ERROR: Invalid syntax, `any` is not a type variable.
  when(fetcher.fetchOfType<any>()).thenAnswer((_) => /* ... */);    
}
  1. It isn't possible to return reified generics (without using mirrors):
 abstract class Cat { /* ... */ }
 abstract class Request<T> {
   T get responseForTesting;
 }


 abstract class RpcHandler {
   Future<T> handle<T>(Request<T> request);    
 }

 class MockRpcHandler extends Mock implements RpcHandler {}

 void main(){
   var rpcHandler = new MockRpcHandler();
   when(rpcHandler.handle(any)).thenAnswer((i) {
     final response = (i.positionalArguments.first as Request).responseForTesting;
     return new Future.value(response);
   });

   // ERROR: Future<dynamic> is not type of Future<Cat>.
   var cat = rpcHandler.handle(new TestRequest<Cat>());
 }

Unfortunately today we give no helpful hints or documentation. I think we should change this, and in default, in Mockito 4, make it impossible to mock a method with one or more generic type arguments (at least by default). We could start with making this opt-in:

void main() {
  Mock.throwOnGenericStub = true;
}

This was just hit (critically) internally, and hidden by DDC whitelisting :(

matanlurey avatar Jul 11 '18 23:07 matanlurey

One extra itch I hit internally, the latter example can be example if someone writes:

when(rpcHandler.any(any)).thenAnswer((_) => new Future.value(new Cat());

... so we might not want to ban this, perhaps this should just be documented cleanly.

matanlurey avatar Jul 12 '18 00:07 matanlurey

Is this related to this issue?

import 'package:mockito/mockito.dart';

typedef MyHandler<T>(T arg1);

defaultHandler<T>(T arg1) {}

class Foo {
  bar<T>({MyHandler<T> arg: defaultHandler}) {}
}

class MockMandrillClient extends Mock implements Foo {}

main() {}

When trying to run this "test", I get this exception:

Failed to load "/Volumes/CaseFS/mandrill-api/test/foo_test.dart":
Unable to spawn isolate: Type parameter T is not indexed
#0      TypeParameterIndexer.[] (package:kernel/binary/ast_to_binary.dart:2073)
#1      BinaryPrinter.visitTypeParameterType (package:kernel/binary/ast_to_binary.dart:1703)
#2      TypeParameterType.accept (package:kernel/ast.dart:5035)
#3      BinaryPrinter.writeNode (package:kernel/binary/ast_to_binary.dart:246)
#4      LimitedBinaryPrinter.writeNode (package:kernel/binary/limited_ast_to_binary.dart:55)
#5      BinaryPrinter.writeNodeList (package:kernel/binary/ast_to_binary.dart:238)
#6      BinaryPrinter.visitInstantiation (package:kernel/binary/ast_to_binary.dart:1332)
#7      Instantiation.accept (package:kernel/ast.dart:2959)
[...]

Without mockito, this code works fine.

enyo avatar Aug 06 '18 10:08 enyo

@enyo That is a head scratcher; can you do a dart --version? Are you running pub run test or dart my_test.dart? What version of mockito (should be in pubspec.lock)?

srawlins avatar Aug 07 '18 13:08 srawlins

@srawlins I'm running dart 2.0.0:

/usr/local/Cellar/dart/2.0.0/libexec/bin/dart

mockito v3.0.0

It's quite interesting, because when you remove implements Foo from the class definition it also doesn't fail.

enyo avatar Aug 07 '18 14:08 enyo

For better or worse, @enyo, this is a Dart SDK bug. https://github.com/dart-lang/sdk/issues/34122

srawlins avatar Aug 10 '18 13:08 srawlins

I thought that was very likely. Thanks for looking into it!

enyo avatar Aug 13 '18 11:08 enyo

Hi, how are you guys doing with this issue?

I actually ran into the same problem, I'm using this https://pub.dev/packages/injector and apparently mock classes is having problems with generic types. For instance, if I ran

injector.getDependency<A>

and

injector.getDependency<B>

it would return the same object (the first object type alphabetically) because mock's inability to match generic type arguments.

blackmenthor avatar Jan 14 '20 16:01 blackmenthor

We haven't looked into this issue in a long time. It just doesn't seem to crop up. An injector does sound like a case where it might be warranted.

One tricky catch to fixing this issue: it will be a breaking change. Where currently a method call would be matched, it may no longer, due to unequal type arguments.

srawlins avatar Jan 15 '20 23:01 srawlins

Potentially off topic tip: A dependency injector is something you really don't want to mock. There shouldn't be any behavior that is tied to something expensive or unstable, and it should be safe to set up a real injector within a test, and shouldn't have the type of side effects to warrant a verify that certain methods were called. https://github.com/dart-lang/mockito#best-practices

natebosch avatar Jan 28 '20 23:01 natebosch

This is supported in v0.3.0 of mocktail in case that helps anyone.

felangel avatar Mar 01 '22 19:03 felangel