58

I have a method in a Dart class, which accepts BuildContext parameter, as follows:

class MyClass {

  <return_type> myMethodName(BuildContext context, ...) {
        ...
        doSomething
        return something;
    }
}

I want to test that the method works as expected:

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
...

void main() {
  MyClass sut;

  setUp(() {
    sut = MyClass();
  });

  test('me testing', () {

    var actual = sut.myMethodName(...);        

    expect(actual, something);
  });
}

Of course, it won't work, because the method myMethodName needs a parameter BuildContext type. This value is available throughout the application itself, but not sure where to get that from in my unit tests.

Nicolas
  • 1,320
  • 2
  • 16
  • 28

4 Answers4

61

One way is to use testWidgets in combination with a Builder widget:

testWidgets('me testing', (WidgetTester tester) async {
  await tester.pumpWidget(
    Builder(
      builder: (BuildContext context) {
        var actual = sut.myMethodName(context, ...);
        expect(actual, something);

        // The builder function must return a widget.
        return Placeholder();
      },
    ),
  );
});
jamesdlin
  • 81,374
  • 13
  • 159
  • 204
  • Ahh, beautiful. I tested it but got an exception: `A build function returned null...`. Seems like it is needed to return a widget of any kind. I just added `return Container()` and seems to work this way – Nicolas May 24 '19 at 08:25
  • 1
    @Nicolas Oops! Sorry, I forgot about that. I've made the correction. – jamesdlin May 24 '19 at 15:42
39

Here is a simple way to retrieve a BuildContext instance inside a test case:

testWidgets('showDialog', (WidgetTester tester) async {
  await tester.pumpWidget(MaterialApp(home: Material(child: Container())));
  final BuildContext context = tester.element(find.byType(Container));

  final dialog = showDialog(
    context: context,
    builder: (context) => AlertDialog(
      content: Text('shown by showDialog'),
    ),
  );

  // apply your tests to dialog or its contents here.
});

This was inspired by Simple dialog control test from the Flutter test cases for the showDialog() function.

The whole "app" consist of a Container widget in a MaterialApp frame. The BuildContext instance is retrieved form by finding the Element instance related to the Container.

Ber
  • 40,356
  • 16
  • 72
  • 88
  • 1
    Best answer, as it allows me to tap a button and then expect after the UI has rebuilt as a result of the button tap. Using a builder doesn't allow me to do this. – David Chopin Sep 07 '21 at 19:43
36

You can actually mock the BuildContext so the test will run headless. I think it's better but might be not a solution that you are looking for.

BuildContext is an abstract class therefore it cannot be instantiated. Any abstract class can be mocked by creating implementations of that class. If I take your example then the code will look like this:

class MockBuildContext extends Mock implements BuildContext {}

void main() {
   MyClass sut;
   MockBuildContext _mockContext;

   setUp(() {
     sut = MyClass();
     _mockContext = MockBuildContext();
   });

   test('me testing', () {

   var actual = sut.myMethodName(_mockContext, ...);        

   expect(actual, something);
  });
}

(Note that this requires the Mockito package: https://pub.dev/packages/mockito).

Ahmad F
  • 30,560
  • 17
  • 97
  • 143
surga
  • 1,436
  • 21
  • 25
  • 13
    It may be a nice-to-have to point people to the `mockito` package which can be found [here — pub.dev](https://pub.dev/packages/mockito). That way, people who come across this answer would know that they are **first** expected to install the `mockito` package so they can reach the `Mock` class. Thank you :) – Alvindera97 Aug 09 '21 at 01:53
  • 3
    Returns `The argument type 'MockBuildContext' can't be assigned to the parameter type 'BuildContext'` – Clinton Jan 31 '22 at 07:44
6

I am totally fine with 'surga' answer, but in some cases, it won't be good enough. like when you want to use this BuildContext with InhiretedWidget for example: Provider or MediaQuery.

So I suggest using the Mockito default generator to generate the BuildContext class for you.

@GenerateMocks([BuildContext])
BuildContext _createContext(){
final context = MockBuildContext();
...

And add build_runner to your pubspec.yaml

dev_dependencies:
  flutter_test:
    sdk: flutter
  build_runner: any //use any version you want

Then run this command:

flutter packages pub run build_runner build

Now you can create a context object from the MockBuildContext class as it is created normally from the MaterialApp.

@GenerateMocks([BuildContext])
BuildContext _createContext(){
  final context = MockBuildContext();
  final mediaQuery = MediaQuery(
    data: MediaQueryData(),
    child: const SizedBox(),
  );
  when(context.widget).thenReturn(const SizedBox());
  when(context.findAncestorWidgetOfExactType()).thenReturn(mediaQuery);
  when(context.dependOnInheritedWidgetOfExactType<MediaQuery>())
      .thenReturn(mediaQuery);
  when(context.getElementForInheritedWidgetOfExactType())
      .thenReturn(InheritedElement(mediaQuery));
 
  return context;
}

Note: It's not required to add when..thenReturn's for this Mock, it is depends on your needs.

Abdelazeem Kuratem
  • 1,324
  • 13
  • 20