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

Mock functions via MockClass #234

Closed
michaeldlfx opened this issue Jan 10, 2020 · 4 comments
Closed

Mock functions via MockClass #234

michaeldlfx opened this issue Jan 10, 2020 · 4 comments

Comments

@michaeldlfx
Copy link

michaeldlfx commented Jan 10, 2020

Hey 👋

I've opened this issue so as to avoid commenting on the closed issue #62 .

This is how I am currently mocking functions when testing widgets in Flutter projects:

class Class {
  dynamic fn() {}
}

class MockClass extends Mock implements Class {}

Allowing me to use it in tests like so:

testWidgets('CustomButton calls onTap when tapped',
      (WidgetTester tester) async {
    final mock = MockClass();

    await tester.pumpWidget(CustomButton(
        onTap: mock.fn,
    ));

    await tester.tap(find.byType(CustomButton));

    verify(mock.fn()).called(1);
});

Now, I'm aware that this particular solution is only usable for no-arg functions, but perhaps it serve as a starting point for solving #62 ?

One could add more functions to the class to account for functions with varying numbers of arguments, which could look like this:

class Class {
  dynamic fn() {}
  dynamic fn1(dynamic arg) {}
  dynamic fn2(dynamic arg1, dynamic arg2) {}
  ...
}

Mocking functions with named arguments might be tough, though 😅

Thoughts?

@srawlins
Copy link
Member

srawlins commented Jan 13, 2020

I don't think I understand the problem. Why would your tear-off strategy (onTap: mock.fn) only work on zero-arg functions? If you want to mock/verify a function that takes one argument, then it would need its own function, as you show, fn1, and if you wanted to mock/verify a second function that takes one argument, then it also would need its own function, separate from fn1.

@michaeldlfx
Copy link
Author

michaeldlfx commented Jan 15, 2020

@srawlins
The problem I'm trying to address is the one mentioned in #62 - the fact that this Mockito library provides no way of mocking functions out of the box, which can be fixed 🙂

When I mentioned that it would would only work on zero-arg functions, I was referring to this specific implementation:

class Class {
  dynamic fn() {} // this one right here
}

, which is why I suggested expanding the class with more functions, each with an incremental number of args.
Sorry if that was unclear.

The implementation I've shown after, as you've mentioned, would solve the more-arg problem with the fn, fn1, ... , fnN methods.

Regarding the point you've made about mocking a second function that takes the same number of args as one already mocked in a given test, this can easily be solved by instantiating a new instance of the MockClass, like so:

testWidgets('CustomTextField calls onTap and onEditingComplete when [...]',
      (WidgetTester tester) async {
    final mock1 = MockClass();
    final mock2 = MockClass();

    final textField = CustomTextField(
      onTap: mock1.fn,
      onEditingComplete: mock2.fn,
    );

    await tester.pumpWidget(textField);

    await tester.tap(find.byType(CustomTextField));
    await tester.testTextInput.receiveAction(TextInputAction.done);

    verify(mock1.fn()).called(1);
    verify(mock2.fn()).called(1);
});

Unless you have any objections regarding this approach, I'd be happy to open a PR to contribute it in order to solve #62

@srawlins
Copy link
Member

Thanks for the explanation; I think I see what you are looking for now.

I don't think mockito is a good solution for the problem of verifying that a callback (a function expression) has been called. I think that using a real function that updates a value would be a more straightforward solution:

testWidgets('CustomTextField calls onTap and onEditingComplete when [...]',
      (WidgetTester tester) async {
    bool tapped = false;
    bool editingCompleted = false;

    final textField = CustomTextField(
      onTap: () => tapped = true,
      onEditingComplete: () => editingCompleted = true,
    );

    await tester.pumpWidget(textField);

    await tester.tap(find.byType(CustomTextField));
    await tester.testTextInput.receiveAction(TextInputAction.done);

    expect(tapped, isTrue);
    expect(editingCompleted, isTrue);
});

@natebosch
Copy link
Member

I don't think this approach is something we should be encoding directly in this package. It can't solve the case of named arguments, and it's not the way that I would solve this problem. I think it's a fine workaround if it works well for you locally.

FWIW the way that I would handle this is to make my own small closures. If I need to validate calls I would do that manually. The main value that Mock gives us is to handle entire surface area of a class at once - letting us ignore details we don't care about and reducing duplicative boilerplate. In the case of mocking a Function there only exists boilerplate if you care to do the call tracking, otherwise there isn't any at all. The code to stub the behavior with Mock is about equivalent to creating a function without mockito involved.

testWidgets('CustomButton calls onTap when tapped',
      (WidgetTester tester) async {
    var callCount = 0;
    void mockFn() {
      callCount++;
    }

    await tester.pumpWidget(CustomButton(
        onTap: mockFn,
    ));

    await tester.tap(find.byType(CustomButton));

    expect(callCount, 1);
});

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

3 participants