mockito icon indicating copy to clipboard operation
mockito copied to clipboard

Mock functions?

Open xster opened this issue 7 years ago • 13 comments

Don't really know how or if this is achievable but it would be great if there's some magical means to create a mock function rather than a mock class that can be passed into things expecting any sort of typedefs or function signatures and then verify its (dynamic) calls and args.

xster avatar Apr 25 '17 21:04 xster

A concrete example what you're trying to accomplish?

zoechi avatar Apr 26 '17 08:04 zoechi

I'm not really proficient with mocks, but you should be able to mock the call method. The problem is having the correct function type. A Dart object can only have one function type at a time (the type of the call method of its interface), so you can't have an object that has "any" function type and passes any is SomeTypedef test. You can make your mock implement a class that has a particular call method, and that makes it a function of that type, but only that type.

lrhn avatar Apr 26 '17 10:04 lrhn

Example:

typedef A SomethingIWantToMock(B b, C c);

void somethingIWantToTest(D d, E e, SomethingIWantToMock mock) {
  [the code to test...]
  A a = mock(something, something);
  [more code to test...]
}

test('check that somethingIWantToTest interacts correctly with SomethingIWantToMock', () {
  SomethingIWantToMock mock = [make a mock...];
  when(mock).thenAnswer((Invocation invocation) => [use b, use c]);
  somethingIWantToTest(mock);
  assert
  assert
  verify(mock)
  verify(mock)
});

More or less the same as class mocking but with functions.

xster avatar Apr 26 '17 18:04 xster

cc @cbracken FYI

xster avatar Apr 26 '17 18:04 xster

I think the main issue here is that it won't be possible to create a single "Mock" function that could represent (statically) any function. In this particular case, it might honestly be simpler to just create a stub implementation of your function:

test('...', () {
  void stubDoSomething(a, b, c) {
    // ...
  }
};

matanlurey avatar Apr 26 '17 18:04 matanlurey

Hi @xster yeah this is, with the current implementation, impossible ☹️ . When you wrap a method call with when(...), mockito does literally call the method, but it routes directly to the noSuchMethod method of Mock. We have no way of routing a call of a top-level method.

I think @matanlurey 's suggestion should work perfectly if you get to inject the function into somethingIWantToTest.

srawlins avatar Jul 31 '17 22:07 srawlins

If you want to just create a mocked function to use as onTap on widget test environment, with mockito you can do something like that:

abstract class MyFunction {
  void call();
}
class MyFunctionMock extends Mock implements MyFunction {}

Test:

void main() {
 final mock = MyFunctionMock();
 ...

  // This is inside the the testWidgets callback 
  await tester.pumpWidget(
          widgetToBeTested(
            item: registeredListItem,
            onTap: mock,
          ),
        );

  //Logic to tap the widgetToBeTested

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

}

manoellribeiro avatar Apr 12 '21 15:04 manoellribeiro

It's now very easy to mock functions. You just need to mock an abstract class that contains all the functions you need as methods, and then pass the methods as the needed functions.

Here's a full example:

import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';

import 'example_test.mocks.dart';

/// The function to test
Future<DateTime> getCurrentTime(
    Future<String> Function(String) postRequest) async {
  String r = await postRequest('getCurrentTime');
  return DateTime.fromMillisecondsSinceEpoch(int.parse(r) * 1000, isUtc: true);
}

abstract class MyFuncs {
  Future<String> postRequest(String name);
}

@GenerateMocks([MyFuncs])
void main() {
  test('getCurrentTime', () async {
    final myFuncs = MockMyFuncs();
    when(myFuncs.postRequest('getCurrentTime'))
        .thenAnswer((_) async => '1647249645');
    final dt = await getCurrentTime(myFuncs.postRequest);
    expect(dt, DateTime.utc(2022, 3, 14, 9, 20, 45));
  });
}

noamraph avatar Mar 14 '22 09:03 noamraph

@srawlins - I just saw the pattern of mocking an abstract class for it's tearoffs used in a code review and it surprised me at first.

Do you think it would be reasonable to more directly support mocking callbacks now that we have codegen? We could feasibly turn Future<String> Function(String) into a full mock class with a method that could be torn off, without asking the user to write the abstract class around it too.

A few points to consider:

  • How would we name them? Maybe we'd need to configure them in a map keyed by a name? {'postRequest': Future<String> Function(String)}
  • How would we surface them? We don't necessarily need to expose the fact that they are tearoffs of a regular Mock
Future<String> Function(String) mockPostRequest() => _MockPostRequest().call;

natebosch avatar Jun 03 '22 22:06 natebosch

This is a very cool idea. Something like MockSpec<Future<String> Function(String)>(as: #mockPostRequest).

srawlins avatar Aug 04 '22 16:08 srawlins

About this point, I have a slightly different problem that I don't know how to solve myself:

// application/repository/todo.dart
import 'package:flutter/foundation.dart';
import 'package:port_domain/networking.dart';
import 'package:todo/domain/todo.dart';

class TodoRepository {
  final RestClient _client;
  final String _baseUrl;

  List<Todo> todos = [];

  TodoRepository(this._client, this._baseUrl);

  Future<void> fetchFromRemote() async {
    Response groupRes = await _client.get(
      _baseUrl + 'api/todos',
      parser: (data) => compute(Todo.fromJsonList, data as List),
    );
    if (groupRes is Success) {
      todos.clear();
      todos.addAll(groupRes.result);
    }
  }
}
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:port_domain/networking.dart';
import 'package:todo/application/repository/todo.dart';

@GenerateMocks([RestClient])
import 'repository_todo_test.mocks.dart';

void main() {
  late RestClient client;
  late TodoRepository repository;

  setUp(() {
    client = MockRestClient();
    repository = TodoRepository(client, '');
  });

  group('TodoRepository', () {
    test('should have entities available after a successful fetch', () async {
      var todoList = [const Todo(id: 1)];
      when(
        client.get(
          'api/todos',
          parser: any, // <-- This fails because any isn't supported
        ),
      ).thenAnswer((_) async => Success<List<Todo>>(todoList));
      await repository.fetchFromRemote();

      expect(repository.todos, equals(todoList));
    });
  });
}

Can someone point me out to a way to test it? The problem here is that the Repository creates a closure that should be identified by mockito as the same function.

neokree avatar Aug 19 '22 03:08 neokree

@neokree You have to use anyNamed('parser') here. That's a limitation of Mockito implementation.

yanok avatar Jul 03 '23 15:07 yanok

Regarding adding support for mocking functions: definitely doable with codegen, but not sure it's a high priority for us. Will be happy to review changes.

yanok avatar Jul 03 '23 15:07 yanok