Chain multiple calls with same arguments to return different results

Viewed 1975

I'm in the process of writing a Flutter app with some extensive unit test coverage.
I'm using Mockito to mock my classes.
Coming from a Java (Android) world where I can use Mockito to chain calls to return different values on subsequent calls.
I would expect this to work.

import 'package:test/test.dart';
import 'package:mockito/mockito.dart';

void main() {
  test("some string test", () {
    StringProvider strProvider = MockStringProvider();
    when(strProvider.randomStr()).thenReturn("hello");
    when(strProvider.randomStr()).thenReturn("world");

    expect(strProvider.randomStr(), "hello");
    expect(strProvider.randomStr(), "world");
  });
}

class StringProvider {
  String randomStr() => "real implementation";
}

class MockStringProvider extends Mock implements StringProvider {}

However it throws:

Expected: 'hello'
Actual:   'world'
  Which: is different.

The only working way I found that works is by keeping track myself.

void main() {
  test("some string test", () {
    StringProvider strProvider = MockStringProvider();

    var invocations = 0;
    when(strProvider.randomStr()).thenAnswer((_) {
      var a = '';
      if (invocations == 0) {
        a = 'hello';
      } else {
        a = 'world';
      }
      invocations++;
      return a;
    });

    expect(strProvider.randomStr(), "hello");
    expect(strProvider.randomStr(), "world");
  });
}

00:01 +1: All tests passed!

Is there a better way?

3 Answers

Use a list and return the answers with removeAt:

import 'package:test/test.dart';
import 'package:mockito/mockito.dart';

void main() {
  test("some string test", () {
    StringProvider strProvider = MockStringProvider();
    var answers = ["hello", "world"];

    when(strProvider.randomStr()).thenAnswer((_) => answers.removeAt(0));

    expect(strProvider.randomStr(), "hello");
    expect(strProvider.randomStr(), "world");
  });
}

class StringProvider {
  String randomStr() => "real implementation";
}

class MockStringProvider extends Mock implements StringProvider {}

You're not forced to call when in the start of the test:

StringProvider strProvider = MockStringProvider();
when(strProvider.randomStr()).thenReturn("hello");
expect(strProvider.randomStr(), "hello");

when(strProvider.randomStr()).thenReturn("world");
expect(strProvider.randomStr(), "world");

Mockito is dart has a different behavior. Subsequent calls override the value.

Option 1 - With a Stub

I'm not sure about mockito, but you could do it using argument matchers, and creating a stub class that overrides that method instead of using a mock, like:

class StrProviderStub implements StrProvider {
    StrProviderStub(this.values);

    List<String> values;
    int _currentCall = 0;

    @override
    String randomStr() {
        final value = values[_currentCall];

        _currentCall++;

        return value;
    }
}

And then use that Stub instead of the mock on the test

Option 2 - Use an Extension with a closure (Preferred)

extension MultipleExpectations<T> on PostExpectation<T> {
  void thenAnwserInOrder(List<T> bodies) {
    final safeBody = Queue.of(bodies);

    thenAnswer((realInvocation) => safeBody.removeFirst());
  }
}

# Then... use it like:
when(mock).thenAnwserInOrder(['First call', 'second call']);

I opened this issue to see if this could be added to the framework itself https://github.com/dart-lang/mockito/issues/568

Related