简体   繁体   中英

Dart - how to mock a method that returns a future

I have a class that defines a method that returns a Future. The Future contains a list of class that also return a future.

    class User{
      Future<List<Album>> albums(){

      };
    }
    class Album{
      Future<List<Photos>> photos(){
      }
    };

What is the best way to mock the method in these classes when testing another class?

The class I am trying to test looks a bit like

class Presenter {
   Presenter( User user){
       user.albums().then( _processAlbums);
   }
   _processAlbums(List<Album> albums) {
      albums.forEach( (album)=>album.photos.then( _processPhotos));
  }
  _processPhotos(List<Photo> photos) {
     ....stuff
  }
}

I tried writing a unit test like this

class MockUser extends Mock implements User{}
class MockAlbum extends Mock implements Album{}
class MockPhoto extends Mock implements Photo{}

class MockFutureList<T> extends Mock implements Future<T>{

  MockFutureList( List<T> items){
    when( callsTo( "then")).thenReturn( items);
  }
}

void main(){

  test("constuctor should request the albums from the user ",(){

    MockUser user = new MockUser();

    MockAlbum album = new MockAlbum();
    List<Album> listOfAlbums = [ album];

    MockPhoto photo = new MockPhoto();
    List<Album> listOfPhotos = [ album];        
    user.when( callsTo( "albums")).thenReturn(  new MockFutureList(listOfAlbums));    
    album.when( callsTo( "photos")).thenReturn( new MockFutureList( listOfPhotos));

    PicasaPhotoPresentor underTest = new PicasaPhotoPresentor( view, user);

    user.getLogs( callsTo( "albums")).verify( happenedOnce);
    album.getLogs( callsTo( "photos")).verify( happenedOnce);

  });
}

This allowed me to test that the constructor called the user.photos() method, but not that the album.photos() method was called.

I am not sure that mocking a Future is a good idea - Would it not be better to create a 'real' Future that contains a list of Mocks?

Any ideas would be very helpful!

Since you're only interested in verifying that methods in User and Album are called, you won't need to mock the Future .

Verifying the mocks gets a bit tricky here, because you're chaining futures inside the constructor. With a little understanding of how the event loop works in Dart, I recommend using a future and calling expectAsync after you create your presenter.

The expectAsync function tells the unit test library to wait until it's called to verify your tests. Otherwise the test will complete successfully without running your expectations.

With this, here's what your test should would look like:

import 'package:unittest/unittest.dart';

class MockUser extends Mock implements User {}
class MockAlbum extends Mock implements Album {}

void main() {
  test("constuctor should request the albums from the user ", () {
    var user = new MockUser();
    var album = new MockAlbum();
    user.when(callsTo("albums")).thenReturn(new Future(() => [album]));

    var presenter = new PicasaPhotoPresentor(view, user);

    // Verify the mocks on the next event loop.
    new Future(expectAsync(() {
      album.getLogs(callsTo("photos")).verify(happendOnce);
    }));
  });
}

Here is how I managed to do it

1) Define FutureCallbackMock

class FutureCallbackMock extends Mock implements Function {
  Future<void> call();
}

2) get function from a mock and set it up

FutureCallback onPressed = FutureCallbackMock().call;
completer = Completer<void>();
future = completer.future;

when(onPressed()).thenAnswer((_) => future);

3) Verify like so

verify(onPressed()).called(1);

4) Complete the future if needed:

completer.complete();

NOTE: in flutter tests I had to wrap my test in tester.runAsync like so

      testWidgets(
          'when tapped disables underlying button until future completes',
          (WidgetTester tester) async {
        await tester.runAsync(() async {
           // test here
        });
      });

I was able to do this with Mocktail . This is the article that this is from, and explains how to integrate it into your app. This is a full widget test and depends on this gist code .

The crux is that you need to declare a Mock class that has a call method. Then, you can then mock the top-level function that returns a Future . You can use the when and verify methods with this.

//Gist code
import 'package:gist/main.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:flutter/material.dart';

class LaunchMock extends Mock {
  Future<bool> call(
    Uri url, {
    LaunchMode? mode,
    WebViewConfiguration? webViewConfiguration,
    String? webOnlyWindowName,
  });
}

void main() {
  testWidgets('Test Url Launch', (tester) async {
    //These allow default values
    registerFallbackValue(LaunchMode.platformDefault);
    registerFallbackValue(const WebViewConfiguration());

    //Create the mock
    final mock = LaunchMock();
    when(() => mock(
          flutterDevUri,
          mode: any(named: 'mode'),
          webViewConfiguration: any(named: 'webViewConfiguration'),
          webOnlyWindowName: any(named: 'webOnlyWindowName'),
        )).thenAnswer((_) async => true);

    final builder = compose()
      //Replace the launch function with a mock
      ..addSingletonService<LaunchUrl>(mock);

    await tester.pumpWidget(
      builder.toContainer()<MyApp>(),
    );

    //Tap the icon
    await tester.tap(
      find.byIcon(Icons.favorite),
    );

    await tester.pumpAndSettle();

    verify(() => mock(flutterDevUri)).called(1);
  });
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM