mockito icon indicating copy to clipboard operation
mockito copied to clipboard

False generation of mocks containing generated entities

Open NikoBoerger opened this issue 3 years ago • 9 comments

Hi,

I'm having the following simple class and want to generate a mock for it:

class PlayerModelConverter {
  PlayerTableCompanion convertToPlayerCompanion(Player player);

  Player convertToPlayer(PlayerModel playerModel);
}

"PlayerTableCompanion" and "PlayerModel" are generated classes from the Moor library. When mocking with @GenerateMocks([PlayerModelConverter]), mockito creates the following:

class MockPlayerModelConverter extends _i1.Mock
    implements _i4.PlayerModelConverter {
  MockPlayerModelConverter() {
    _i1.throwOnMissingStub(this);
  }

  @override
  dynamic convertToPlayerCompanion(_i2.Player? player) => super
      .noSuchMethod(Invocation.method(#convertToPlayerCompanion, [player]));
  @override
  _i2.Player convertToPlayer(dynamic playerModel) =>
      (super.noSuchMethod(Invocation.method(#convertToPlayer, [playerModel]),
          returnValue: _FakePlayer()) as _i2.Player);
}

Both of the moor classes are converted to "dynamic", which obviously doesn't work.

The generated classes from moor look like this:

class PlayerModel extends DataClass implements Insertable<PlayerModel> {
  final int id;
  final String name;
  final String? imagePath;
  final KeyboardVariant? keyboardVariant;
  final ShortcutButtons? shortcutButtons;
  final bool? showCheckoutHints;
  PlayerModel(
      {required this.id,
      required this.name,
      this.imagePath,
      this.keyboardVariant,
      this.shortcutButtons,
      this.showCheckoutHints});
  factory PlayerModel.fromData(Map<String, dynamic> data, GeneratedDatabase db,
      {String? prefix}) {
    final effectivePrefix = prefix ?? '';
    final intType = db.typeSystem.forDartType<int>();
    final stringType = db.typeSystem.forDartType<String>();
    final boolType = db.typeSystem.forDartType<bool>();
    return PlayerModel(
      id: intType.mapFromDatabaseResponse(data['${effectivePrefix}id'])!,
      name: stringType.mapFromDatabaseResponse(data['${effectivePrefix}name'])!,
      imagePath: stringType
          .mapFromDatabaseResponse(data['${effectivePrefix}image_path']),
      keyboardVariant: $PlayerTableTable.$converter0.mapToDart(intType
          .mapFromDatabaseResponse(data['${effectivePrefix}keyboard_variant'])),
      shortcutButtons: $PlayerTableTable.$converter1.mapToDart(stringType
          .mapFromDatabaseResponse(data['${effectivePrefix}shortcut_buttons'])),
      showCheckoutHints: boolType.mapFromDatabaseResponse(
          data['${effectivePrefix}show_checkout_hints']),
    );
  }

[....]

Seems like a normal class to me. Is there a reason why this isn't working? Am I doing something wrong? The code generation works fine for my other mocks, that don't have dependencies to the generated classes from moor.

============================== Doctor summary (to see all details, run flutter doctor -v): [✓] Flutter (Channel stable, 2.0.1, on macOS 11.2.2 20D80 darwin-x64, locale de-DE) [✓] Android toolchain - develop for Android devices (Android SDK version 30.0.3) [✓] Xcode - develop for iOS and macOS [✓] Chrome - develop for the web [✓] Android Studio (version 4.1) [✓] Connected device (1 available)

Flutter 2.0.1 • channel stable • https://github.com/flutter/flutter.git Framework • revision c5a4b4029c (30 hours ago) • 2021-03-04 09:47:48 -0800 Engine • revision 40441def69 Tools • Dart 2.12.0

mockito: ^5.0.0-nullsafety.7

NikoBoerger avatar Mar 06 '21 00:03 NikoBoerger

Can you show me your build configuration? I suspect PlayerTableCompanion and PlayerModel are not generated when Mockito does its generation.

srawlins avatar Mar 08 '21 05:03 srawlins

What exactly do you mean by build config? How can I influence what gets generated first?

I also see this behavior, when the build runner only has to generate the mocks (and the Moor classes are already generated).

NikoBoerger avatar Mar 08 '21 07:03 NikoBoerger

Ok I see. Thanks for this hint. I will create a build config and see if that fixes my problem!

NikoBoerger avatar Mar 08 '21 07:03 NikoBoerger

Yeah sorry I don't use build_runner or generated files too often. But I suspect there is a way in build.yaml to instruct build_runner to build the other generated files before the mockito files.

If you make progress, I'd love to write up instructions in the FAQ.

srawlins avatar Mar 08 '21 17:03 srawlins

Okay I got it running with the following build.yaml configuration:

targets:
  $default:
    builders:
      # disables the SharedPartBuilder in favor of a PartBuilder from moor_generator
      moor_generator:
        enabled: false
      moor_generator|moor_generator_not_shared:
        enabled: true
      moor_generator|preparing_builder:
        enabled: true

      # Run mockito when moor is done!
      mockito|mockBuilder:
        enabled: false

  run_built_value:
    dependencies: ['MadHouse']
    builders:
      # Disable moor builders. By default, those would run on each target
      moor_generator:
        enabled: false
      moor_generator|preparing_builder:
        enabled: false
      copy_with_extension_gen|copy_with_extension_gen:
        enabled: false

Unfortunately I don't really have a clue what it does exactly. I found it very hard to find documentation about the build.yaml. I got this one from the moor documentation and adapted it for my usecase: https://moor.simonbinder.eu/docs/advanced-features/builder_options/#using-moor-classes-in-other-builders

It seems like I disable the mockito code generation at first, and somehow the "run_build_value" runs all of the build runners again, after the first builders are done generating. I had to disable all of my other builders in the run_build_value section (copy_with_extension_gen) because it runs twice and conflicts, if I don't disable it there.

The mockito generator is able to detect the classes from moor now and generates the mocks correctly. However, for some of the moor classes mockito seems to have some kind of a problem in the generated class: image

For all of the *Companion classes I get the syntax error

Object.==' ('bool Function(Object)') isn't a valid concrete implementation of 'UpdateCompanion.==' ('bool Function(dynamic)').

The mentioned "UpdateCompanion" is the super class of the from moor generated "PlayerTableCompanion". UpdateCompanion is from the moor library directly and is not generated.

I don't really understand this error, because I can't find Object.== anywhere in the generated code. Is it a problem you maybe know already? However, the tests don't seem to care about this error and they all run fine, so it's working for me now. Just the red markers in the project tree indicating the syntax errors are a little annoying :D

NikoBoerger avatar Mar 09 '21 20:03 NikoBoerger

Have similar issue with json_serializer plugin and some getters.

Is there any way of "hiding" specific fields / methods via annotation ?

radvansky-tomas avatar Mar 09 '21 22:03 radvansky-tomas

Is there any way of "hiding" specific fields / methods via annotation ?

No, the generated mock needs to be a legal implementation of the given class.

srawlins avatar Mar 09 '21 23:03 srawlins

Hi guys, I figured it out. I'm using ferry to generate model classes for GraphQL, so I need to make sure that mockito's builder runs after ferry's. Here is the full example of build.yaml. It's very similar to NikoBoerger's, but cleaner, and I added some description.

targets:
  $default:
    # This is true by default, so you can just omit this line. This means enable all builders by default. 
    auto_apply_builders: true
    builders:
      # Write your other builders' settings normally here.
      ferry_generator|graphql_builder:
        enabled: true
        options:
          schema: xxx/schema.graphql

      ferry_generator|serializer_builder:
        enabled: true
        options:
          schema: xxx/schema.graphql
          
      # Disable mockito's builder to run it after other code is generated. 
      mockito|mockBuilder:
        enabled: false

  # Adjust this name by yourself. 
  run_mockito:
    # Make sure this target runs after the target above.
    dependencies: ["$default"]
    # Only run mockito's builder. 
    auto_apply_builders: false
    builders:
      mockito|mockBuilder:
        enabled: true

vast00 avatar Jun 10 '22 08:06 vast00

@vast00 : You are godlike, this works awesome

ghost avatar Jun 21 '23 13:06 ghost