Saltar al contenido principal

Probar cada capa

Cómo probar una aplicación que implementa la arquitectura MVVM.

Probar la capa de UI

#

Una forma de determinar si tu arquitectura es sólida es considerar qué tan fácil (o difícil) es probar la aplicación. Debido a que los view models y las views tienen entradas bien definidas, sus dependencias se pueden simular con mocks o fakes de forma sencilla, y las pruebas unitarias se escriben con facilidad.

Pruebas unitarias del ViewModel

#

Para probar la lógica de la UI del view model, deberías escribir pruebas unitarias que no dependan de las librerías de Flutter o de frameworks de pruebas.

Los repositorios son las únicas dependencias de un view model (a menos que estés implementando casos de uso), y escribir mocks o fakes del repositorio es la única configuración que necesitas hacer. En este test de ejemplo, se utiliza un fake llamado FakeBookingRepository.

home_screen_test.dart
dart
void main() {
  group('HomeViewModel tests', () {
    test('Load bookings', () {
      // HomeViewModel._load is called in the constructor of HomeViewModel.
      final viewModel = HomeViewModel(
        bookingRepository: FakeBookingRepository()
          ..createBooking(kBooking),
        userRepository: FakeUserRepository(),
      );

      expect(viewModel.bookings.isNotEmpty, true);
    });
  });
}

La clase FakeBookingRepository implementa BookingRepository. En la sección de la capa de datos de este caso de estudio, se explica detalladamente la clase BookingRepository.

fake_booking_repository.dart
dart
class FakeBookingRepository implements BookingRepository {
  List<Booking> bookings = List.empty(growable: true);

  @override
  Future<Result<void>> createBooking(Booking booking) async {
    bookings.add(booking);
    return Result.ok(null);
  }
  // ...
}

Pruebas de widgets de la View

#

Una vez que hayas escrito pruebas para tu view model, ya habrás creado los fakes que necesitas para escribir también pruebas de widgets. El siguiente ejemplo muestra cómo se configuran las pruebas de widgets de HomeScreen utilizando el HomeViewModel y los repositorios necesarios:

home_screen_test.dart
dart
void main() {
  group('HomeScreen tests', () {
    late HomeViewModel viewModel;
    late MockGoRouter goRouter;
    late FakeBookingRepository bookingRepository;

    setUp(() {
      bookingRepository = FakeBookingRepository()
        ..createBooking(kBooking);
      viewModel = HomeViewModel(
        bookingRepository: bookingRepository,
        userRepository: FakeUserRepository(),
      );
      goRouter = MockGoRouter();
      when(() => goRouter.push(any())).thenAnswer((_) => Future.value(null));
    });

    // ...
  });
}

Esta configuración crea los dos repositorios fake necesarios y los pasa a un objeto HomeViewModel. Esta clase no necesita ser imitada.

Una vez definidos el view model y sus dependencias, se debe crear el árbol de widgets que se probará. En las pruebas de HomeScreen, se define un método loadWidget.

home_screen_test.dart
dart
void main() {
  group('HomeScreen tests', () {
    late HomeViewModel viewModel;
    late MockGoRouter goRouter;
    late FakeBookingRepository bookingRepository;

    setUp(
      // ...
    );

    void loadWidget(WidgetTester tester) async {
      await testApp(
        tester,
        ChangeNotifierProvider.value(
          value: FakeAuthRepository() as AuthRepository,
          child: Provider.value(
            value: FakeItineraryConfigRepository() as ItineraryConfigRepository,
            child: HomeScreen(viewModel: viewModel),
          ),
        ),
        goRouter: goRouter,
      );
    }

    // ...
  });
}

Este método a su vez llama a testApp, un método generalizado utilizado para todas las pruebas de widgets en la aplicación compass. Tiene el siguiente aspecto:

testing/app.dart
dart
void testApp(
  WidgetTester tester,
  Widget body, {
  GoRouter? goRouter,
}) async {
  tester.view.devicePixelRatio = 1.0;
  await tester.binding.setSurfaceSize(const Size(1200, 800));
  await mockNetworkImages(() async {
    await tester.pumpWidget(
      MaterialApp(
        localizationsDelegates: [
          GlobalWidgetsLocalizations.delegate,
          GlobalMaterialLocalizations.delegate,
          AppLocalizationDelegate(),
        ],
        theme: AppTheme.lightTheme,
        home: InheritedGoRouter(
          goRouter: goRouter ?? MockGoRouter(),
          child: Scaffold(
            body: body,
          ),
        ),
      ),
    );
  });
}

El único trabajo de esta función es crear un árbol de widgets que pueda ser probado.

El método loadWidget introduce las partes únicas de un árbol de widgets para realizar pruebas. En este caso, eso incluye la HomeScreen y su view model, así como algunos repositorios fake adicionales que se encuentran más arriba en el árbol de widgets.

Lo más importante a destacar es que las pruebas de views y view models solo requieren simular repositorios si tu arquitectura es sólida.

Probar la capa de datos

#

De manera similar a la capa de UI, los componentes de la capa de datos tienen entradas y salidas bien definidas, lo que permite simular ambos lados. Para escribir pruebas unitarias para cualquier repositorio dado, simula los servicios de los que depende. El siguiente ejemplo muestra una prueba unitaria para el BookingRepository.

booking_repository_remote_test.dart
dart
void main() {
  group('BookingRepositoryRemote tests', () {
    late BookingRepository bookingRepository;
    late FakeApiClient fakeApiClient;

    setUp(() {
      fakeApiClient = FakeApiClient();
      bookingRepository = BookingRepositoryRemote(
        apiClient: fakeApiClient,
      );
    });

    test('should get booking', () async {
      final result = await bookingRepository.getBooking(0);
      final booking = result.asOk.value;
      expect(booking, kBooking);
    });
  });
}

Para obtener más información sobre cómo escribir mocks y fakes, consulta los ejemplos en el directorio de testing de la aplicación Compass o lee la documentación de pruebas de Flutter.

Comentarios

#

Dado que esta sección del sitio web está evolucionando, ¡agradecemos tus comentarios!