Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

@GenerateMocks does not generate for void function without parameter #367

Closed
EhiltonKazuo opened this issue Mar 16, 2021 · 20 comments
Closed

Comments

@EhiltonKazuo
Copy link

I'm new in mockito and null safety operators, but i followed the NULL_SAFETY_README. This is the annotation that im using:
@GenerateMocks([], customMocks: [MockSpec<LoginPresenter>(as: #LoginPresenterSpy)])

This is the sample of LoginPresenter:

abstract class LoginPresenter {
  Stream<String> get emailErrorStream;
  Stream<String> get passwordErrorStream;
  Stream<String> get mainErrorStream;
  Stream<bool> get isFormValidStream;
  Stream<bool> get isLoadingStream;

  void validateEmail(String email);
  void validatePassword(String password);
  void auth();
  void dispose();
}

And this is the generate code build_runner:

class LoginPresenterSpy extends _i1.Mock implements _i2.LoginPresenter {
  LoginPresenterSpy() {
    _i1.throwOnMissingStub(this);
  }

  @override
  _i3.Stream<String> get emailErrorStream =>
      (super.noSuchMethod(Invocation.getter(#emailErrorStream),
          returnValue: Stream<String>.empty()) as _i3.Stream<String>);
  @override
  _i3.Stream<String> get passwordErrorStream =>
      (super.noSuchMethod(Invocation.getter(#passwordErrorStream),
          returnValue: Stream<String>.empty()) as _i3.Stream<String>);
  @override
  _i3.Stream<String> get mainErrorStream =>
      (super.noSuchMethod(Invocation.getter(#mainErrorStream),
          returnValue: Stream<String>.empty()) as _i3.Stream<String>);
  @override
  _i3.Stream<bool> get isFormValidStream =>
      (super.noSuchMethod(Invocation.getter(#isFormValidStream),
          returnValue: Stream<bool>.empty()) as _i3.Stream<bool>);
  @override
  _i3.Stream<bool> get isLoadingStream =>
      (super.noSuchMethod(Invocation.getter(#isLoadingStream),
          returnValue: Stream<bool>.empty()) as _i3.Stream<bool>);
  @override
  void validateEmail(String? email) =>
      super.noSuchMethod(Invocation.method(#validateEmail, [email]),
          returnValueForMissingStub: null);
  @override
  void validatePassword(String? password) =>
      super.noSuchMethod(Invocation.method(#validatePassword, [password]),
          returnValueForMissingStub: null);
}

But doesn't exist the auth and dispose functions. Am i doing wrong?

@srawlins
Copy link
Member

Nope; you are not doing anything wrong. Mock.noSuchMethod will handle those methods.

@mateusdeap
Copy link

mateusdeap commented Apr 1, 2021

I'm not sure I understand. I'm coming across a problem where I have methods similar to @EhiltonKazuo's which return void and have no parameters, and the generated mock class does not override them. Thus, when I call flutter test I get:

MissingStubError: 'start'
  No stub was found which matches the arguments of this method call:
  start()
  
  Add a stub for this method using Mockito's 'when' API, or generate the mock for MockTomato with 'returnNullOnMissingStub: true'.
  package:mockito/src/mock.dart 190:7                         Mock._noSuchMethod
  package:mockito/src/mock.dart 184:45                        Mock.noSuchMethod
  package:focus_index/domain/entities/tomato.dart 2:8         MockTomato.start
  package:focus_index/domain/usecases/start_timer.dart 11:18  StartTimer.execute
  test/usecases/start_timer_test.dart 29:13                   main.<fn>.<fn>.<fn>

By what you're saying, mockito should automatically handle these cases?

My test file looks like this, btw:

import 'package:focus_index/domain/entities/timer_controller.dart';
import 'package:focus_index/domain/entities/tomato.dart';
import 'package:focus_index/domain/usecases/start_timer.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:test/test.dart';

import 'start_timer_test.mocks.dart';

@GenerateMocks([Tomato])
main() {
  group("Start Timer", () {
    group("execute", () {
      TimerController timerController;
      MockTomato tomato;
      StartTimer sut;

      setUp(() {
        timerController = TimerController.instance;
        tomato = MockTomato();
        sut = StartTimer(tomato);
      });

      test("should start a new tomato given the last timer was a rest period",
          () {
        timerController.increaseTomatoStreak();
        timerController.increaseRestStreak();

        sut.execute();

        verify(tomato.start());
      });

      tearDown(() {
        timerController.reset();
      });
    });
  });
}

@mateusdeap
Copy link

So I got it to pass by modifying the annotation as suggested in the NULL_SAFETY_README:
@GenerateMocks([], customMocks: [MockSpec<Tomato>(returnNullOnMissingStub: true)])

However, I don't know if this is what I should be doing since it is not the case that my method returns null. But I don't really know if a void return type is equivalent to returning null, so...

@EhiltonKazuo
Copy link
Author

I think you only use this, if your code is legacy. I will wait the new update to solve this.

@natebosch
Copy link
Member

However, I don't know if this is what I should be doing since it is not the case that my method returns null. But I don't really know if a void return type is equivalent to returning null, so...

This is the behavior of mockito before the null safety migration. void is a type which any value can flow into. You can return any value at all through a void return type, the value null happens to be the most common one. If you don't have an explicit return statement, a null will still be returned.

The returnNullOnMissingStub configuration could have been named something like allowMissingStubs and we chose the default of false because we think it's a better practice that behavior is explicitly stubbed instead of implicitly allowing interactions, especially since the implicit behavior can't work consistently between nullable and non-nullable return types. The name returnNullOnMissingStub is intended to make it more clear what the behavior is so that folks who are opted in to null safety understand why a non-nullable return type and a missing stub aren't compatible.

From the standpoint of avoiding magic in your tests I'd personally recommend you avoid returnNullOnMissingStub: true and explicitly stub all the behavior that gets used in your test. If you goal is to retain as much backward compatibility with the pre-migration mockito it is fine to use.

I will wait the new update to solve this.

@srawlins can correct me if I'm wrong, but I don't think we have any changes planned here. This is working as intended.

If you are using GenerateMocks you should either stub all behavior, or pass the returnNullOnMissingStub argument.

@mateusdeap
Copy link

@natebosch Thanks! So, by what I'm understanding, it would be best practice to actually return something for my methods, is that right?

@srawlins
Copy link
Member

srawlins commented Apr 7, 2021

Actually, for #305, I did land changes to allow calling functions which return void or Future<void>, even if they have not been stubbed. The rationale is that stubbing is all about setting a return value, but regardless of the arguments passed to a void method, the "result" is... always void.

These shipped in 5.0.0-nullsafety.6. @mateusdeap can you show what version you are running?

@srawlins
Copy link
Member

srawlins commented Apr 7, 2021

@mateusdeap I can reproduce this in a test, at HEAD; this bug slipped through the new tests that landed with that change.

@srawlins srawlins reopened this Apr 7, 2021
@mateusdeap
Copy link

@srawlins Awesome then! I know you reproduced it at HEAD, but just for the information, I'm using 5.0.3

srawlins added a commit that referenced this issue Apr 7, 2021
Having to stub a `void` function before calling it confuses developers. They write `when(mock.methodReturnsVoid(any)).thenReturn(` and then might really have to scratch their heads about what makes sense to return from a mock of a void function. And all this just to satisfy a requirement to have sane return values, which I think just doesn't apply to void (and `Future<void>` and `Future<void>?`) functions.

This CL changes the logic that determines whether a method (or field, or setter) needs to be overridden. It now includes the case for these void-ish methods.

Fixes #367

PiperOrigin-RevId: 367247168
@flocbit
Copy link

flocbit commented Apr 11, 2021

@srawlins Is there an ETA for your fix? Experiencing the same thing as @mateusdeap...

@srawlins
Copy link
Member

This fix has landed on HEAD. I'm waiting for internal review of one more change + a version bump, to be synced to GitHub. Likely tomorrow or Tuesday.

srawlins added a commit that referenced this issue Apr 12, 2021
Having to stub a `void` function before calling it confuses developers. They write `when(mock.methodReturnsVoid(any)).thenReturn(` and then might really have to scratch their heads about what makes sense to return from a mock of a void function. And all this just to satisfy a requirement to have sane return values, which I think just doesn't apply to void (and `Future<void>` and `Future<void>?`) functions.

This CL changes the logic that determines whether a method (or field, or setter) needs to be overridden. It now includes the case for these void-ish methods.

Fixes #367

PiperOrigin-RevId: 367247168
srawlins added a commit that referenced this issue Apr 12, 2021
Having to stub a `void` function before calling it confuses developers. They write `when(mock.methodReturnsVoid(any)).thenReturn(` and then might really have to scratch their heads about what makes sense to return from a mock of a void function. And all this just to satisfy a requirement to have sane return values, which I think just doesn't apply to void (and `Future<void>` and `Future<void>?`) functions.

This CL changes the logic that determines whether a method (or field, or setter) needs to be overridden. It now includes the case for these void-ish methods.

Fixes #367

PiperOrigin-RevId: 367247168
@MilanObrenovic
Copy link

Sorry but this still isnt fixed. My test fails because of this issue.

@srawlins
Copy link
Member

@MilanObrenovic can you please provide:

  • the version of mockito you are using
  • a minimal reproduction case
  • the generated mock file you see.

Since this bug is so old, you are probably hitting a new, different issue, and you should open a new issue.

@MilanObrenovic
Copy link

@MilanObrenovic can you please provide:

  • the version of mockito you are using
  • a minimal reproduction case
  • the generated mock file you see.

Since this bug is so old, you are probably hitting a new, different issue, and you should open a new issue.

version of mockito: ^5.0.16 (latest as of now)

my test:

test("Should perform database batch delete of chat.", () async {
  // Arrange
  const String chatId = "111";
  when(mockDatabase.batch()).thenReturn(mockBatch);

  // Act
  await dataSourceImpl.deleteChat(
    chatId: chatId,
  );

  // Assert
  verifyInOrder([
    mockDatabase.batch(),
    mockBatch.delete(
      "messages",
      where: anyNamed("where"),
      whereArgs: [chatId],
    ),
    mockBatch.delete(
      "chats",
      where: anyNamed("where"),
      whereArgs: [chatId],
    ),
    mockBatch.commit(
      noResult: true,
    ),
  ]);
});

deleteChat method:

@override
Future<void> deleteChat({required String chatId}) async {
  final Batch batch = _database.batch();
  batch.delete(
    "messages",
    where: "chat_id = ?",
    whereArgs: [chatId],
  );
  batch.delete(
    "chats",
    where: "id = ?",
    whereArgs: [chatId],
  );
  await batch.commit( // <--- THE CRASH HAPPENS BECAUSE OF THIS LINE !!!
    noResult: true,
  );
}

error:

MissingStubError: 'commit'
No stub was found which matches the arguments of this method call:
commit({exclusive: null, noResult: true, continueOnError: null})

Add a stub for this method using Mockito's 'when' API, or generate the mock for MockBatch with 'returnNullOnMissingStub: true'.

The error message looks pretty straightforward.

I tried adding those optional nullable parameters "exclusive" and "continueOnError" to be non-nullable, but got the same error.

I also i tried replacing the batch with nullable on stub generation:

@GenerateMocks([Database], customMocks: [MockSpec<Batch>(returnNullOnMissingStub: true)])

But this prints a different error message:

type 'Null' is not a subtype of type 'Future<List<Object?>>' in type cast

No idea how to fix this for over several weeks. Let me know if i should create a new issue.

@srawlins
Copy link
Member

@MilanObrenovic This is unrelated to #367.

MissingStubError: 'commit'
No stub was found which matches the arguments of this method call:
commit({exclusive: null, noResult: true, continueOnError: null})

Add a stub for this method using Mockito's 'when' API, or generate the mock for MockBatch with 'returnNullOnMissingStub: true'.

The error message looks pretty straightforward.

When you don't provide a stub, and use returnNullOnMissingStub: true, mockito will return null on a missing stub, but the commit method must return a non-nullable value.

@MilanObrenovic
Copy link

@MilanObrenovic This is unrelated to #367.

MissingStubError: 'commit'
No stub was found which matches the arguments of this method call:
commit({exclusive: null, noResult: true, continueOnError: null})

Add a stub for this method using Mockito's 'when' API, or generate the mock for MockBatch with 'returnNullOnMissingStub: true'.

The error message looks pretty straightforward.

When you don't provide a stub, and use returnNullOnMissingStub: true, mockito will return null on a missing stub, but the commit method must return a non-nullable value.

so how do i fix this crash?

@srawlins
Copy link
Member

Did you try the other suggestion from the error?

Add a stub for this method using Mockito's 'when' API

@MilanObrenovic
Copy link

Did you try the other suggestion from the error?

Add a stub for this method using Mockito's 'when' API

but i thought i already added that with this piece of code?

when(mockDatabase.batch()).thenReturn(mockBatch);

@srawlins
Copy link
Member

No that is stubbing the batch method of mockDatabase.

You need to stub the commit method of mockBatch.

@MilanObrenovic
Copy link

No that is stubbing the batch method of mockDatabase.

You need to stub the commit method of mockBatch.

now it worked...

when(mockBatch.commit(
  noResult: true,
)).thenAnswer((_) async => []);

solved the issue. thank you very much.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants