felangel / mocktail

A mock library for Dart inspired by mockito
https://pub.dev/packages/mocktail
MIT License
588 stars 80 forks source link

The "when" function does not create a stub #199

Closed InAnadea closed 3 weeks ago

InAnadea commented 1 year ago

Bug description I'm trying to mock the use case for the bloc. But I got the error: Bad state: No method stub was called from withinwhen(). Was a real method called, or perhaps an extension method?

Steps to reproduce the behavior:

import 'package:bloc_test/bloc_test.dart';
import 'package:dartz/dartz.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:imagilabs_edu_flutter/data/models/admin/teacher_admin_model.dart';
import 'package:imagilabs_edu_flutter/data/models/admin/teacher_subscription_model.dart';
import 'package:imagilabs_edu_flutter/domain/usecases/get_google_sheet_link_usecase.dart';
import 'package:imagilabs_edu_flutter/domain/usecases/search_teachers_usecase.dart';
import 'package:imagilabs_edu_flutter/presentation/screens/admin/teachers/bloc/admin_teachers_screen_bloc.dart';
import 'package:imagilabs_edu_flutter/util/constants.dart';
import 'package:mocktail/mocktail.dart';

class MockSearchTeachersUsecase extends Mock implements SearchTeachersUsecase {}

class MockGetGoogleSheetLinkUsecase extends Mock
    implements GetGoogleSheetLinkUsecase {}

void main() {
  late AdminTeachersScreenBloc sut;
  late GetGoogleSheetLinkUsecase getGoogleSheetLink;
  late SearchTeachersUsecase searchTeachers;

  setUp(() {
    getGoogleSheetLink = MockGetGoogleSheetLinkUsecase();
    searchTeachers = MockSearchTeachersUsecase();
    sut = AdminTeachersScreenBloc(searchTeachers, getGoogleSheetLink);
  });

  const userId = 'id';
  const userEmail = 'email@gmail.com';
  const firstName = 'firstName';
  const lastName = 'lastName';
  const country = 'country';
  const organization = 'Test organization';

  const teacher = TeacherAdminModel(
    id: userId,
    email: userEmail,
    firstName: firstName,
    lastName: lastName,
    country: country,
    organization: organization,
    howDidYouHearAboutUs: 'howDidYouHearAboutUs',
    emailVerified: true,
    createdAt: 0,
    lastModifiedAt: 0,
    subscription: TeacherSubscriptionModel(
      start: 0,
      end: 0,
      plan: SubscriptionPlan.pro,
      canceled: false,
    ),
  );
  const teachers = [teacher];
  const sortingDirection = SortingDirection.asc;
  const sortingType = SortingType.createdAt;
  const searchQuery = '';
  const searchParams = SearchTeachersParams(
    sortingDirection: sortingDirection,
    sortingType: sortingType,
    searchQuery: searchQuery,
  );
  const sheetLink = 'google.com';

  blocTest( // it works
    'emits [] when nothing is added.',
    build: () => sut,
    expect: () => [],
  );

  blocTest( // it doesn't work
    'emits [loading, updated] when init is added.',
    build: () => sut,
    setUp: () {
      // this is default sorting params for view initialization
      when(() => searchTeachers(const SearchTeachersParams(
            sortingDirection: SortingDirection.desc,
            sortingType: SortingType.createdAt,
          ))).thenAnswer((_) async => const Right(teachers));
      when(() => getGoogleSheetLink())
          .thenAnswer((_) async => const Right(sheetLink));
    },
    act: (bloc) => bloc.add(const AdminTeachersScreenEvent.init()),
    expect: () => [
      isA<LoadingAdminTeachersScreenState>(),
      isA<UpdatedAdminTeachersScreenState>(),
    ],
  );

  blocTest( // it doesn't work
    'emits [updated] when search is added.',
    build: () => sut,
    seed: () => const AdminTeachersScreenState.initial(),
    setUp: () {
      when(() => searchTeachers(searchParams))
          .thenAnswer((_) async => const Right(teachers));
      when(() => getGoogleSheetLink())
          .thenAnswer((_) async => const Right(sheetLink));
    },
    act: (bloc) => bloc.add(const AdminTeachersScreenEvent.search(
      sortingDirection: sortingDirection,
      sortingType: sortingType,
      searchQuery: searchQuery,
    )),
    expect: () => [
      isA<LoadingAdminTeachersScreenState>(),
      isA<UpdatedAdminTeachersScreenState>(),
    ],
  );
}

The source code of classes:

import 'dart:async';

import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:imagilabs_edu_flutter/data/models/admin/teacher_admin_model.dart';
import 'package:imagilabs_edu_flutter/domain/usecases/get_google_sheet_link_usecase.dart';
import 'package:imagilabs_edu_flutter/domain/usecases/search_teachers_usecase.dart';
import 'package:imagilabs_edu_flutter/util/bloc/notifiable_bloc.dart';
import 'package:imagilabs_edu_flutter/util/constants.dart';
import 'package:imagilabs_edu_flutter/util/errors/failures.dart';
import 'package:url_launcher/url_launcher_string.dart';

part 'admin_teachers_screen_event.dart';
part 'admin_teachers_screen_state.dart';
part 'admin_teachers_screen_notification.dart';
part 'admin_teachers_screen_bloc.freezed.dart';

class AdminTeachersScreenBloc extends NotifiableBloc<AdminTeachersScreenEvent,
    AdminTeachersScreenState, AdminTeachersScreenNotification> {
  final SearchTeachersUsecase _searchTeachers;
  final GetGoogleSheetLinkUsecase _getGoogleSheetLink;

  AdminTeachersScreenBloc(
    this._searchTeachers,
    this._getGoogleSheetLink,
  ) : super(const AdminTeachersScreenState.initial()) {
    on<_Init>(_onInit);
    on<_Search>(_onSearch);
    on<_OpenGoogleSheetLink>(_onOpenGoogleSheetLink);
  }

  Future<void> _onInit(
    _Init event,
    Emitter<AdminTeachersScreenState> emit,
  ) async {
    emit(const AdminTeachersScreenState.loading());

    await _fetchDataAndUpdate(emit);
  }

  Future<void> _onSearch(
    _Search event,
    Emitter<AdminTeachersScreenState> emit,
  ) async {
    await _fetchDataAndUpdate(
      emit,
      sortingType: event.sortingType,
      sortingDirection: event.sortingDirection,
      searchQuery: event.searchQuery,
    );
  }

  Future<void> _fetchDataAndUpdate(
    Emitter<AdminTeachersScreenState> emit, {
    SortingType sortingType = SortingType.createdAt,
    SortingDirection sortingDirection = SortingDirection.desc,
    String? searchQuery,
  }) async {
    final teachersOrFailureFuture = _searchTeachers(SearchTeachersParams(
      sortingType: sortingType,
      sortingDirection: sortingDirection,
      searchQuery: searchQuery,
    ));
    final googleSheetLinkOrFailureFuture = _getGoogleSheetLink();

    await Future.wait([
      teachersOrFailureFuture,
      googleSheetLinkOrFailureFuture,
    ]);

    final teachersOrFailure = await teachersOrFailureFuture;
    final googleSheetLinkOrFailure = await googleSheetLinkOrFailureFuture;

    ifFailure(failure) => AdminTeachersScreenState.failure(failure);
    emit(teachersOrFailure.fold(
      ifFailure,
      (teachers) => googleSheetLinkOrFailure.fold(
        ifFailure,
        (link) => AdminTeachersScreenState.updated(
          teachers: teachers,
          googleSheetLink: link,
        ),
      ),
    ));
  }

  Future<void> _onOpenGoogleSheetLink(
    _OpenGoogleSheetLink event,
    Emitter<AdminTeachersScreenState> emit,
  ) async {
    try {
      if (await canLaunchUrlString(event.link)) {
        await launchUrlString(event.link);
      } else {
        emitNotification(const AdminTeachersScreenNotification.error());
      }
    } catch (exc) {
      emitNotification(AdminTeachersScreenNotification.error(exc));
    }
  }
}
import 'package:dartz/dartz.dart';
import 'package:imagilabs_edu_flutter/util/errors/failures.dart';
import 'package:imagilabs_edu_flutter/util/usecase.dart';

abstract class GetGoogleSheetLinkUsecase extends Usecase<String, NoParams> {
  @override
  Future<Either<Failure, String>> call([NoParams params]);
}
import 'package:dartz/dartz.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:imagilabs_edu_flutter/data/models/admin/teacher_admin_model.dart';
import 'package:imagilabs_edu_flutter/util/constants.dart';
import 'package:imagilabs_edu_flutter/util/errors/failures.dart';
import 'package:imagilabs_edu_flutter/util/usecase.dart';

part 'search_teachers_usecase.freezed.dart';

abstract class SearchTeachersUsecase
    extends Usecase<List<TeacherAdminModel>, SearchTeachersParams> {
  @override
  Future<Either<Failure, List<TeacherAdminModel>>> call(
    SearchTeachersParams params,
  );
}

@freezed
class SearchTeachersParams with _$SearchTeachersParams {
  const factory SearchTeachersParams({
    String? searchQuery,
    required SortingDirection sortingDirection,
    required SortingType sortingType,
  }) = _SearchTeachersParams;
}

And I get an error:

Bad state: No method stub was called from within `when()`. Was a real method called, or perhaps an extension method?
package:mocktail/src/mocktail.dart 261:7                                                  When._completeWhen
package:mocktail/src/mocktail.dart 256:12                                                 When.thenAnswer
test/presentation/screens/admin/teachers/bloc/admin_teachers_screen_bloc_test.dart 80:12  main.<fn>
package:bloc_test/src/bloc_test.dart 201:20                                               testBloc.<fn>
package:bloc_test/src/bloc_test.dart 254:15                                               _runZonedGuarded.<fn>
dart:async                                                                                runZonedGuarded
package:bloc_test/src/bloc_test.dart 253:3                                                _runZonedGuarded
package:bloc_test/src/bloc_test.dart 200:11                                               testBloc
package:bloc_test/src/bloc_test.dart 156:13                                               blocTest.<fn>
===== asynchronous gap ===========================
dart:async                                                                                _Completer.completeError
package:bloc_test/src/bloc_test.dart 257:43                                               _runZonedGuarded.<fn>
===== asynchronous gap ===========================
dart:async                                                                                _CustomZone.registerBinaryCallback
package:bloc_test/src/bloc_test.dart 254:5                                                _runZonedGuarded.<fn>
dart:async                                                                                runZonedGuarded
package:bloc_test/src/bloc_test.dart 253:3                                                _runZonedGuarded
package:bloc_test/src/bloc_test.dart 200:11                                               testBloc
package:bloc_test/src/bloc_test.dart 156:13                                               blocTest.<fn>

Expected behavior The "when" function should mock methods.

Logs

Doctor summary (to see all details, run flutter doctor -v):
[!] Flutter (Channel stable, 3.10.4, on macOS 13.2.1 22D68 darwin-arm64, locale en-GB)
    ! Warning: `flutter` on your path resolves to /Users/ivan/fvm/versions/3.10.2/bin/flutter, which is not inside your current Flutter SDK checkout at /Users/ivan/fvm/versions/3.10.4. Consider adding /Users/ivan/fvm/versions/3.10.4/bin to the front of your path.
    ! Warning: `dart` on your path resolves to /opt/homebrew/Cellar/dart/2.18.6/libexec/bin/dart, which is not inside your current Flutter SDK checkout at /Users/ivan/fvm/versions/3.10.4. Consider adding /Users/ivan/fvm/versions/3.10.4/bin to the front of your path.
[✓] Android toolchain - develop for Android devices (Android SDK version 32.0.0)
[✓] Xcode - develop for iOS and macOS (Xcode 14.1)
[✓] Chrome - develop for the web
[✓] Android Studio (version 2022.2)
[✓] Android Studio (version 2022.1)
[✓] IntelliJ IDEA Community Edition (version 2022.2.3)
[✓] IntelliJ IDEA Community Edition (version 2023.1.1)
[✓] VS Code (version 1.78.2)
[✓] Connected device (2 available)
[✓] Network resources

! Doctor found issues in 1 category.
ihorkozar commented 10 months ago

Any updates ?

MarlonDSC commented 8 months ago

I'm also facing this error

cromueloliver02 commented 8 months ago

I'm facing this error as well, any updates?

felangel commented 2 months ago

Hi @InAnadea 👋 Can you please share a link to a minimal reproduction sample? It's hard to help without being able to reproduce the issue locally, thanks!

felangel commented 3 weeks ago

Closing for now since this issue is quite old and there isn't a minimal reproduction sample. If this is still a problem please file a new issue with a link to a reproduction sample, thanks!