dart-lang / mockito

Mockito-inspired mock library for Dart
https://pub.dev/packages/mockito
Apache License 2.0
636 stars 163 forks source link

"type 'Null' is not a subtype of type" on getter #608

Open AdrienAudouard opened 1 year ago

AdrienAudouard commented 1 year ago

Hello,

I'm trying to generate a mock for a class witch contains a getter, the source code of the class is:

class AppContext {
...
  AppLocalizations get l10n => AppLocalizations.of(navigatorMaterialAppKey.currentContext!);
}

Then I generate a mock of the class with mockito and with the following code:

@GenerateNiceMocks([
  MockSpec<AppContext>(),
])

When I run the tests I have the following error: type 'Null' is not a subtype of type 'AppLocalizations', it look like in the generated code, the mock code of the getter is not generated.

Can you help me please ? 🙏

The generated code:

/// A class which mocks [AppContext].
///
/// See the documentation for Mockito's code generation for more information.
class MockAppContext extends _i1.Mock implements _i81.AppContext {
  @override
  _i28.GlobalKey<_i28.NavigatorState> get navigatorMainPageKey =>
      (super.noSuchMethod(
        Invocation.getter(#navigatorMainPageKey),
        returnValue: _FakeGlobalKey_34<_i28.NavigatorState>(
          this,
          Invocation.getter(#navigatorMainPageKey),
        ),
        returnValueForMissingStub: _FakeGlobalKey_34<_i28.NavigatorState>(
          this,
          Invocation.getter(#navigatorMainPageKey),
        ),
      ) as _i28.GlobalKey<_i28.NavigatorState>);
  @override
  _i28.GlobalKey<_i28.NavigatorState> get navigatorVaultKey =>
      (super.noSuchMethod(
        Invocation.getter(#navigatorVaultKey),
        returnValue: _FakeGlobalKey_34<_i28.NavigatorState>(
          this,
          Invocation.getter(#navigatorVaultKey),
        ),
        returnValueForMissingStub: _FakeGlobalKey_34<_i28.NavigatorState>(
          this,
          Invocation.getter(#navigatorVaultKey),
        ),
      ) as _i28.GlobalKey<_i28.NavigatorState>);
  @override
  _i28.GlobalKey<_i28.NavigatorState> get navigatorMaterialAppKey =>
      (super.noSuchMethod(
        Invocation.getter(#navigatorMaterialAppKey),
        returnValue: _FakeGlobalKey_34<_i28.NavigatorState>(
          this,
          Invocation.getter(#navigatorMaterialAppKey),
        ),
        returnValueForMissingStub: _FakeGlobalKey_34<_i28.NavigatorState>(
          this,
          Invocation.getter(#navigatorMaterialAppKey),
        ),
      ) as _i28.GlobalKey<_i28.NavigatorState>);
}
srawlins commented 1 year ago

Questions like this are best asked on a user forum like StackOverflow, with more eyes to look at the question.

yanok commented 1 year ago

Yep, it seems we don't override getters with non-nullable return type, but we have to. Thanks for reporting. I plan to re-work the codegen for Dart3 support significantly, I think this bug will be fixed too.

yanok commented 1 year ago

Yep, it seems we don't override getters with non-nullable return type, but we have to. Thanks for reporting. I plan to re-work the codegen for Dart3 support significantly, I think this bug will be fixed too.

The rewrite didn't fly, so I guess we just have to do the overrides.

AdrienAudouard commented 1 year ago

Okay, thanks for your reply, good luck 😄

yanok commented 1 year ago

Actually I can't reproduce it. And the line that does the getter override is there since 2020.

So, your generated code does indeed look wrong, but I don't understand how that could happen.

yanok commented 1 year ago

@AdrienAudouard ping, I can't fix it if I can't reproduce it :)

Ahmad-Hamwi commented 1 year ago

@yanok Hi, I have the issue and I've reproduced it in this repo. It's a very minimal project, please take a look. All required info is in the readme file.

yanok commented 1 year ago

This should be in the FAQ, I guess. You must declare variables holding mock objects to have Mock* types. In your code here https://github.com/Ahmad-Hamwi/mockito-gen-issue/blob/5879e544b97b3c08a8176b4ffd6a914331de9a68/test/app_localizations_mockable.dart#L14

  late IAppLocalization mockAppLocalization;

MUST be

  late MockIAppLocalization mockAppLocalization;
Ahmad-Hamwi commented 1 year ago

If I understand OOP correctly, this shouldn't be the problem, should it? Since the runtime type will be a Mock* anyways.

But here's a screenshot after changing what you've suggested, and running the test again, the test still fails with the same error:

Screenshot 2023-09-04 at 4 04 30 PM
yanok commented 1 year ago

Oh, sorry, my bad. While what I said is correct and you still have to declare your mocks to have Mock types (since that's static type that matters while checking if method arguments have correct type), in your case the error indeed comes from something else. There gotta be a generated override for instance getter and it's missing in the generated code.

Thanks for the reproduction. I'll take a look a bit later.

Ahmad-Hamwi commented 1 year ago

Sure no problem. Thank you for reopening the issue. Tyt.

yanok commented 1 year ago

The problem with your code is AppLocalizations having InvalidType at the time of Mockito codegen running. This is because Analyzer can't find package:flutter_gen/gen_l10n/app_localizations.dart under .dart_tool/flutter_gen. I actually wonder what makes it work when it runs "for real".

Ahmad-Hamwi commented 1 year ago

Thank you for you insights.

So basically generated code cannot be mocked. Is this an intended behavior for mockito or is it a feature that should be worked on?

Also, can you elaborate on what you mean about "for real"? Do you mean running the test in an emulated environment such as an integration test?

yanok commented 1 year ago

No, we can mock generated code, though there are also possible issues, since we have to be sure other generators run before mockito, and I think there is no generic way to ensure this. Actually this might be the cause here too, I thought the problem is the "magic" import, but maybe mockito generator just runs before flutter_gen?

By "magic" import I mean that this flutter_gen package is a bit special: it allows you to refer to the generated code via package:flutter_gen/... URLs, while it's clearly not a part of package:flutter_gen itself. So there is some magic setting that allows Dart to find these generated files.

By "for real" I mean when you run the test with flutter test or whatever vs when running the mockito codegen. Since for the codegen we spawn the Analyzer manually, so maybe we are missing some settings that make these "magic" imports work, as the problem seems to be Analyzer can't resolve these imports.