auto_route_library icon indicating copy to clipboard operation
auto_route_library copied to clipboard

How to create Widget tests.

Open Maqcel opened this issue 2 years ago • 5 comments

Hi, I'm struggling to find a clever way to write Widget Test with auto_route included. I've searched the entire issues for some help and only managed to work with #745. The solution that was suggested there worked for my login screen but going deeper to the home screen or any other one I tried to test unfortunately failed.

The following assertion was thrown building AutoTabsScaffold:
RouteData operation requested with a context that does not include an RouteData.
The context used to retrieve the RouteData must be that of a widget that is a descendant of a
AutoRoutePage.

Test:

@GenerateMocks([StackRouter])
void main() {
  group('Home Screen Test - ', () {
    late final StackRouter mockStackRouter;

    setUpAll(() {
      mockStackRouter = MockStackRouter();
    });

    testWidgets('Home Screen has BottomNavigation', (tester) async {
      await tester.pumpWidget(giveWidgetMaterialAncestor(
        router: mockStackRouter,
        widgetToTest: const HomeScreen(),
      ));

      expect(find.byType(BottomNavigationBar), findsOneWidget);
    });
  });
}

HomeScreen:

class HomeScreen extends StatelessWidget {
  const HomeScreen({super.key});

  static const List<PageRouteInfo> _homeRoutes = [
    ProfileRouter(),
    DevicesRouter(),
    RecentTransfersRouter(),
  ];

  @override
  Widget build(BuildContext context) => AutoTabsScaffold(
        navigatorObservers: () => [AutoRouteObserver()],
        routes: _homeRoutes,
        bottomNavigationBuilder: (context, tabsRouter) =>
            _bottomNavigationBarBuilder(context, tabsRouter),
      );

  BottomNavigationBar _bottomNavigationBarBuilder(
    BuildContext context,
    TabsRouter tabsRouter,
  ) =>
      BottomNavigationBar(
        currentIndex: tabsRouter.activeIndex,
        onTap: tabsRouter.setActiveIndex,
        items: [
          _navigationBarItem(
            context,
            HomeScreenPageType.profile,
            tabsRouter.current.name == ProfileRouter.name,
          ),
          _navigationBarItem(
            context,
            HomeScreenPageType.devices,
            tabsRouter.current.name == DevicesRouter.name,
          ),
          _navigationBarItem(
            context,
            HomeScreenPageType.transfers,
            tabsRouter.current.name == RecentTransfersRouter.name,
          ),
        ],
      );

  BottomNavigationBarItem _navigationBarItem(
    BuildContext context,
    HomeScreenPageType itemType,
    bool isActive,
  ) =>
      BottomNavigationBarItem(
        icon: HomeBottomNavigationBarItemProvider.getItemIconFromType(
          itemType: itemType,
          isActive: isActive,
        ),
        label: HomeBottomNavigationBarItemProvider.getLabelFromType(
          itemType: itemType,
          context: context,
        ),
      );
}

Helper method for providing MaterialApp:

Widget giveWidgetMaterialAncestor({
  required StackRouter router,
  required Widget widgetToTest,
}) =>
    MaterialApp(
      supportedLocales: LocalizationConfig.supportedLocales,
      localizationsDelegates: LocalizationConfig.localizationDelegates,
      theme: LightTheme().themeData,
      home: StackRouterScope(
        controller: router,
        stateHash: 0,
        child: widgetToTest,
      ),
    );

As mentioned this approach was successful for the Login Screen all tests for it:

@GenerateMocks([
  AuthRepository,
  StackRouter,
])
void main() {
  group('Login Screen Tests - ', () {
    late final AuthRepository mockAuthRepository;
    late final StackRouter mockStackRouter;

    setUpAll(() {
      mockAuthRepository = MockAuthRepository();
      mockStackRouter = MockStackRouter();
    });

    testWidgets('Login Failed', (tester) async {
      when(mockAuthRepository.signIn()).thenAnswer(
        (invocation) async => Result.failure(const UnexpectedFailure()),
      );

      when(mockAuthRepository.hasValidUserSession).thenAnswer(
        (invocation) => false,
      );

      await tester.pumpWidget(giveWidgetMaterialAncestorWithBloc(
        router: mockStackRouter,
        widgetToTest: const LoginScreen(),
        cubit: LoginCubit(authRepository: mockAuthRepository),
      ));

      await tester.tap(find.byKey(const ValueKey('loginButton')));

      await tester.pumpAndSettle();

      expect(find.byType(Dialog), findsOneWidget);
    });

    testWidgets('Login Aborted', (tester) async {
      when(mockAuthRepository.signIn()).thenAnswer(
        (invocation) async => Result.failure(const SignInAbortedFailure()),
      );

      when(mockAuthRepository.hasValidUserSession).thenAnswer(
        (invocation) => false,
      );

      await tester.pumpWidget(giveWidgetMaterialAncestorWithBloc(
        router: mockStackRouter,
        widgetToTest: const LoginScreen(),
        cubit: LoginCubit(authRepository: mockAuthRepository),
      ));

      await tester.tap(find.byKey(const ValueKey('loginButton')));

      await tester.pumpAndSettle();

      expect(find.byType(Dialog), findsNothing);
    });

    testWidgets('Login Successful', (tester) async {
      when(mockAuthRepository.signIn()).thenAnswer(
        (invocation) async => Result.success(FakeSessionInfo()),
      );

      when(mockAuthRepository.hasValidUserSession).thenAnswer(
        (invocation) => false,
      );

      when(mockStackRouter.replace(const HomeRoute())).thenAnswer(
        (invocation) async => () {},
      );

      await tester.pumpWidget(giveWidgetMaterialAncestorWithBloc(
        router: mockStackRouter,
        widgetToTest: const LoginScreen(),
        cubit: LoginCubit(authRepository: mockAuthRepository),
      ));

      await tester.tap(find.byKey(const ValueKey('loginButton')));

      await tester.pumpAndSettle();

      verify(mockStackRouter.replace(const HomeRoute()));
    });
  });
}

Mocks created by mockito, I'm strongly looking for some docs/tutorials on how to create widget tests with this package

Maqcel avatar Jul 01 '22 09:07 Maqcel

I'm on the same boat right now, without auto_routes router tests find my widgets, but when I try to use it none of the tests find any widget. If someone has resources on how to set this up greatly appreciated.

Milou6 avatar Jul 01 '22 09:07 Milou6

+1

TheSmartMonkey avatar Jul 01 '22 11:07 TheSmartMonkey

I've found a way to mock auto_route using get_it

Call the router like this in your widget

getIt<AppRouter>().push(const ScreenHome()));
// intead of
context.router.push(const ScreenHome()));

main.dart

GetIt getIt = GetIt.instance;

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  getIt.registerSingleton<AppRouter>(
    AppRouter(
      checkIfAuthenticated: CheckIfAuthenticated(),
    ),
  );
  runApp(initApp()); // child: MyApp()
}

class MyApp extends StatelessWidget {
  MyApp({Key? key}) : super(key: key);
  final router = getIt<AppRouter>();
  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      routerDelegate: AutoRouterDelegate(router),
      routeInformationParser: router.defaultRouteParser(),
    );
  }
}

widget_test.dart

import 'package:projectname/routes/router.gr.dart' as router;
import 'package:projectname/screens/log_in.dart';

class AppRouterMock extends Mock implements router.AppRouter {}

void main() {
  TestWidgetsFlutterBinding.ensureInitialized();
  WidgetsFlutterBinding.ensureInitialized();

  setUpAll(() {
    final appRouterMock = AppRouterMock();
    getIt.registerSingleton<router.AppRouter>(appRouterMock);
  });

  testWidgets('Should goto home',
        (WidgetTester tester) async {
    // Given
    when(() => getIt<router.AppRouter>().push(
          const router.ScreenHome(),
        )).thenAnswer((_) async => {});

    // When
    await tester.pumpWidget(
      const MaterialApp(
        home: ScreenLogIn(),
      ),
    );
    await tester.tap(find.byType(ElevatedButton));
    await tester.pumpAndSettle();
    await tester.pump(Duration(seconds: 2));

    // Then
    verify(() => getIt<router.AppRouter>().push(const router.ScreenHome()));
  });
}

Here is a exemple how to use get_it with widget tests : https://github.com/TheSmartMonkey/flutter-http-widget-test

TheSmartMonkey avatar Jul 30 '22 11:07 TheSmartMonkey

+1

victormarques-ia avatar Aug 21 '22 13:08 victormarques-ia

@Maqcel seems i faced the same problem with AutoTabsScaffold, and successfully proceed to the next step with

return MultiProvider(
    providers: [
      ...AppDependencies.getProviders(),
],
child: Portal(
      child: MaterialApp.router(
        theme: AppTheme.buildTheme(const DesignSystem.light()),
        darkTheme: AppTheme.buildTheme(const DesignSystem.dark()),
        routeInformationParser: router.defaultRouteParser(),
        routerDelegate: router.delegate(),
        themeMode: ThemeMode.light,
        supportedLocales: I18n.supportedLocales,
        localizationsDelegates: const [
          GlobalMaterialLocalizations.delegate,
        ],
      ),
    ),
);

but stuck with error, that provider can't find one of my dependencies in child page (one of route from routes list in AutoTabsScaffold)

BlocProvider.of() called with a context that does not contain a Bloc of type
SampleBlocType

What i want to achieve - it's not a track of transition between the pages, my target - golden test of page which contain multiple tabs.

PankovSerge avatar Sep 06 '22 16:09 PankovSerge

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions

github-actions[bot] avatar Nov 18 '22 08:11 github-actions[bot]

The issue with injecting auto_route into DI (get_it) is that you can't pop from a nested route.

Navigating to a nested page (a page within BottomNavigationBar) with

getIt<AppRouter>().push(const ScreenHome()));

works fine until you want to return from ScreenHome using

getIt<AppRouter>().pop<bool>(false);

The docs mention the following:

navigating without context is not recommended in nested navigation unless you use navigate instead of push and you provide a full hierarchy. e.g router.navigate(SecondRoute(children: [SubChild2Route()]))

Using navigate instead of push means that you can't return a value. The only solution I can see now is to use callbacks on the child page.

Petri-Oosthuizen avatar Nov 23 '22 06:11 Petri-Oosthuizen