Saltar al contenido principal

Simular dependencias usando Mockito

Utiliza el paquete Mockito para imitar el comportamiento de los servicios para realizar pruebas.

A veces, las pruebas unitarias pueden depender de clases que obtienen datos de servicios web o bases de datos en vivo. Esto resulta inconveniente por varias razones:

  • Llamar a servicios o bases de datos en vivo ralentiza la ejecución de las pruebas.
  • Una prueba que pasa podría empezar a fallar si un servicio web o una base de datos devuelve resultados inesperados. Esto se conoce como una "prueba inestable" (flaky test).
  • Es difícil probar todos los escenarios posibles de éxito y fallo utilizando un servicio web o una base de datos en vivo.

Por lo tanto, en lugar de depender de un servicio web o una base de datos en vivo, puedes "simular" (mock) estas dependencias. Los simuladores (mocks) permiten emular un servicio web o una base de datos en vivo y devolver resultados específicos según la situación.

En términos generales, puedes simular dependencias creando una implementación alternativa de una clase. Puedes escribir estas implementaciones alternativas a mano o utilizar el paquete Mockito como atajo.

Esta receta demuestra los conceptos básicos de la simulación con el paquete Mockito mediante los siguientes pasos:

  1. Añade las dependencias del paquete.
  2. Crea una función para probar.
  3. Crea un archivo de pruebas con un http.Client simulado.
  4. Escribe una prueba para cada condición.
  5. Ejecuta las pruebas.

Para más información, consulta la documentación del paquete Mockito.

1. Añade las dependencias del paquete

#

Para usar el paquete mockito, añádelo al archivo pubspec.yaml junto con la dependencia flutter_test en la sección dev_dependencies.

Este ejemplo también utiliza el paquete http, así que define esa dependencia en la sección dependencies.

mockito: 5.0.0 es compatible con la seguridad nula (null safety) de Dart gracias a la generación de código. Para ejecutar la generación de código requerida, añade la dependencia build_runner en la sección dev_dependencies.

Para añadir las dependencias, ejecuta flutter pub add:

flutter pub add http dev:mockito dev:build_runner

2. Crea una función para probar

#

En este ejemplo, realizarás una prueba unitaria de la función fetchAlbum de la receta Obtener datos de internet. Para probar esta función, realiza dos cambios:

  1. Proporciona un http.Client a la función. Esto permite proporcionar el correcto http.Client según la situación. Para Flutter y proyectos del lado del servidor, proporciona un http.IOClient. Para aplicaciones de navegador, proporciona un http.BrowserClient. Para las pruebas, proporciona un http.Client simulado.
  2. Usa el client proporcionado para obtener datos de internet, en lugar del método estático http.get(), que es difícil de simular.

La función ahora debería verse así:

dart
Future<Album> fetchAlbum(http.Client client) async {
  final response = await client.get(
    Uri.parse('https://jsonplaceholder.typicode.com/albums/1'),
  );

  if (response.statusCode == 200) {
    // If the server did return a 200 OK response,
    // then parse the JSON.
    return Album.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
  } else {
    // If the server did not return a 200 OK response,
    // then throw an exception.
    throw Exception('Failed to load album');
  }
}

En el código de tu aplicación, puedes proporcionar un http.Client al método fetchAlbum directamente con fetchAlbum(http.Client()). http.Client() crea un http.Client por defecto.

3. Crea un archivo de pruebas con un http.Client simulado

#

A continuación, crea un archivo de pruebas.

Siguiendo los consejos de la receta Introducción a las pruebas unitarias, crea un archivo llamado fetch_album_test.dart en la carpeta raíz test.

Añade la anotación @GenerateMocks([], customMocks: [MockSpec<http.Client>(as: #MockHttpClient)]) a la función principal para generar una clase MockHttpClient con mockito.

La clase MockHttpClient generada implementa la clase http.Client. Esto te permite pasar el MockHttpClient a la función fetchAlbum, y devolver diferentes respuestas http en cada prueba.

Los mocks generados se ubicarán en fetch_album_test.mocks.dart. Importa este archivo para usarlos.

dart
import 'package:http/http.dart' as http;
import 'package:mocking/main.dart';
import 'package:mockito/annotations.dart';

// Generate a MockClient using the Mockito package.
// Create new instances of this class in each test.
// Note: Naming the generated mock `MockHttpClient` to avoid confusion with
// `MockClient` from `package:http/testing.dart`.
@GenerateMocks([], customMocks: [MockSpec<http.Client>(as: #MockHttpClient)])
void main() {
}

A continuación, genera los mocks ejecutando el siguiente comando:

dart run build_runner build

4. Escribe una prueba para cada condición

#

La función fetchAlbum() hace una de dos cosas:

  1. Devuelve un Album si la llamada http tiene éxito
  2. Lanza una Exception si la llamada http falla

Por lo tanto, quieres probar estas dos condiciones. Usa la clase MockHttpClient para devolver una respuesta "Ok" para la prueba de éxito, y una respuesta de error para la prueba fallida. Prueba estas condiciones utilizando la función when() proporcionada por Mockito:

dart
import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart' as http;
import 'package:mocking/main.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';

import 'fetch_album_test.mocks.dart';

// Generate a MockClient using the Mockito package.
// Create new instances of this class in each test.
// Note: Naming the generated mock `MockHttpClient` to avoid confusion with
// `MockClient` from `package:http/testing.dart`.
@GenerateMocks([], customMocks: [MockSpec<http.Client>(as: #MockHttpClient)])
void main() {
  group('fetchAlbum', () {
    test('returns an Album if the http call completes successfully', () async {
      final client = MockHttpClient();

      // Use Mockito to return a successful response when it calls the
      // provided http.Client.
      when(
        client.get(Uri.parse('https://jsonplaceholder.typicode.com/albums/1')),
      ).thenAnswer(
        (_) async =>
            http.Response('{"userId": 1, "id": 2, "title": "mock"}', 200),
      );

      expect(await fetchAlbum(client), isA<Album>());
    });

    test('throws an exception if the http call completes with an error', () {
      final client = MockHttpClient();

      // Use Mockito to return an unsuccessful response when it calls the
      // provided http.Client.
      when(
        client.get(Uri.parse('https://jsonplaceholder.typicode.com/albums/1')),
      ).thenAnswer((_) async => http.Response('Not Found', 404));

      expect(fetchAlbum(client), throwsException);
    });
  });
}

5. Ejecuta las pruebas

#

Ahora que tienes una función fetchAlbum() con pruebas en su lugar, ejecuta las pruebas.

flutter test test/fetch_album_test.dart

También puedes ejecutar pruebas dentro de tu editor favorito siguiendo las instrucciones de la receta Introducción a las pruebas unitarias.

Ejemplo completo

#
lib/main.dart
#
dart
import 'dart:async';
import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

Future<Album> fetchAlbum(http.Client client) async {
  final response = await client.get(
    Uri.parse('https://jsonplaceholder.typicode.com/albums/1'),
  );

  if (response.statusCode == 200) {
    // If the server did return a 200 OK response,
    // then parse the JSON.
    return Album.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
  } else {
    // If the server did not return a 200 OK response,
    // then throw an exception.
    throw Exception('Failed to load album');
  }
}

class Album {
  final int userId;
  final int id;
  final String title;

  const Album({required this.userId, required this.id, required this.title});

  factory Album.fromJson(Map<String, dynamic> json) {
    return Album(
      userId: json['userId'] as int,
      id: json['id'] as int,
      title: json['title'] as String,
    );
  }
}

void main() => runApp(const MyApp());

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  late final Future<Album> futureAlbum;

  @override
  void initState() {
    super.initState();
    futureAlbum = fetchAlbum(http.Client());
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Fetch Data Example',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
      ),
      home: Scaffold(
        appBar: AppBar(title: const Text('Fetch Data Example')),
        body: Center(
          child: FutureBuilder<Album>(
            future: futureAlbum,
            builder: (context, snapshot) {
              if (snapshot.hasData) {
                return Text(snapshot.data!.title);
              } else if (snapshot.hasError) {
                return Text('${snapshot.error}');
              }

              // By default, show a loading spinner.
              return const CircularProgressIndicator();
            },
          ),
        ),
      ),
    );
  }
}
test/fetch_album_test.dart
#
dart
import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart' as http;
import 'package:mocking/main.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';

import 'fetch_album_test.mocks.dart';

// Generate a MockClient using the Mockito package.
// Create new instances of this class in each test.
// Note: Naming the generated mock `MockHttpClient` to avoid confusion with
// `MockClient` from `package:http/testing.dart`.
@GenerateMocks([], customMocks: [MockSpec<http.Client>(as: #MockHttpClient)])
void main() {
  group('fetchAlbum', () {
    test('returns an Album if the http call completes successfully', () async {
      final client = MockHttpClient();

      // Use Mockito to return a successful response when it calls the
      // provided http.Client.
      when(
        client.get(Uri.parse('https://jsonplaceholder.typicode.com/albums/1')),
      ).thenAnswer(
        (_) async =>
            http.Response('{"userId": 1, "id": 2, "title": "mock"}', 200),
      );

      expect(await fetchAlbum(client), isA<Album>());
    });

    test('throws an exception if the http call completes with an error', () {
      final client = MockHttpClient();

      // Use Mockito to return an unsuccessful response when it calls the
      // provided http.Client.
      when(
        client.get(Uri.parse('https://jsonplaceholder.typicode.com/albums/1')),
      ).thenAnswer((_) async => http.Response('Not Found', 404));

      expect(fetchAlbum(client), throwsException);
    });
  });
}

Resumen

#

En este ejemplo, has aprendido a utilizar Mockito para probar funciones o clases que dependen de servicios web o bases de datos. Esto es solo una breve introducción a la biblioteca Mockito y al concepto de simulación (mocking). Para obtener más información, consulta la documentación proporcionada por el paquete Mockito.