Flutter Arquitectura Limpia [9] – Fuente de Datos Remota

[wp_ad_camp_1]

Flutter Arquitectura Limpia [9] – Fuente de Datos Remota

La última parte restante de la capa de datos para la que actualmente solo tenemos un contrato es la Fuente de datos remota. Aquí es donde se realizará toda la comunicación con la API de Numbers, para lo cual vamos a utilizar el paquete http. Todo esto se hará haciendo un desarrollo basado en pruebas, por supuesto.

Debo aclarar que el contenido original es de Resocoder,  lo que he hecho  es una traducción al español del contenido. Al final de este artículo está el video en inglés para que vayan ilustrándose mejor y ya tengan una claridad de lo expuesto

Curso TDD Arquitectura Limpia

001 draw

Esta publicación es solo una parte de una serie de tutoriales. ¡Todas las demás partes las puede ver aquí y aprenda a diseñar sus aplicaciones Flutter!

Índice()

    Configurando el Test

    002 construction and tools

    El archivo number_trivia_remote_data_source.dart actualmente solo contiene el contrato de la fuente de datos. Pondremos la implementación en el mismo archivo, debajo de la clase abstracta. Como de costumbre, creemos un archivo de prueba que refleje la ubicación del archivo mencionado anteriormente: test / features / number_trivia / data / datasources.

    Para configurar el archivo de prueba, crearemos un Cliente simulado a partir del paquete http y lo pasaremos a la implementación remota de fuente de datos actualmente inexistente.

     

    number_trivia_remote_data_source_test.dart

    import 'package:http/http.dart' as http;
    import 'package:flutter_test/flutter_test.dart';
    import 'package:mockito/mockito.dart';
    import 'package:matcher/matcher.dart';
    
    import '../../../../fixtures/fixture_reader.dart';
    
    class MockHttpClient extends Mock implements http.Client{}
    
    void main() {
      NumberTriviaRemoteDataSourceImpl dataSource;
      MockHttpClient mockHttpClient;
    
      setUp(() {
        mockHttpClient = MockHttpClient();
        dataSource = NumberTriviaRemoteDataSourceImpl(client: mockHttpClient);
      });
    }

    Para hacer feliz el archivo de prueba, crearemos una implementación básica del origen de datos remoto.

    [wp_ad_camp_2]

    number_trivia_remote_data_source.dart

    ...
    class NumberTriviaRemoteDataSourceImpl implements NumberTriviaRemoteDataSource {
      final http.Client client;
    
      NumberTriviaRemoteDataSourceImpl({@required this.client});
    
      @override
      Future<NumberTriviaModel> getConcreteNumberTrivia(int number) {
        // TODO: implement getConcreteNumberTrivia
        return null;
      }
    
      @override
      Future<NumberTriviaModel> getRandomNumberTrivia() {
        // TODO: implement getRandomNumberTrivia
        return null;
      }
    }

    Similar a cómo hubo una gran superposición entre los métodos concretos y aleatorios del Repositorio implementado en la sexta parte, lo mismo será cierto para la Fuente de Datos remota. Comencemos con el método getConcreteNumberTrivia, escribiendo pruebas e implementándolo poco a poco.

     

    getConcreteNumberTrivia

    003 construction and tools 1

    Solo para recapitular, estamos trabajando con la API de Numbers, en este caso el punto final concreto. Para el número 42, se ve así: http://numbersapi.com/42. El problema es que realizar una solicitud GET en ese tipo de URL nos da una respuesta de texto sin formato que contiene solo la cadena de preguntas y respuestas. ¡Pero queremos obtener una respuesta JSON!

    Te puede interesar:  Flutter Arquitectura Limpia [12] – Implementación de Bloc 2/2

    Hacer que la API responda con un encabezado de aplicación / json es posible de dos maneras:

    1. Agregue un parámetro de consulta a la URL, haciendo que se vea así: http://numbersapi.com/42?json
    2. Enviar un tipo de contenido: encabezado de aplicación / json junto con la solicitud GET

    Vamos a elegir la segunda opción. Dado que es muy importante obtener la URL y el encabezado correctamente, crearemos una prueba específicamente para ellos.

     

    test.dart

    group('getConcreteNumberTrivia', () {
      final tNumber = 1;
    
      test(
        'should preform a GET request on a URL with number being the endpoint and with application/json header',
        () {
          //arrange
          when(mockHttpClient.get(any, headers: anyNamed('headers'))).thenAnswer(
            (_) async => http.Response(fixture('trivia.json'), 200),
          );
          // act
          dataSource.getConcreteNumberTrivia(tNumber);
          // assert
          verify(mockHttpClient.get(
            'http://numbersapi.com/$tNumber',
            headers: {'Content-Type': 'application/json'},
          ));
        },
      );
    }

    Con el fallo de la prueba, escribiremos solo el código de producción que sea suficiente para que la prueba pase.

    [wp_ad_camp_3]

    implementation.dart

    @override
    Future<NumberTriviaModel> getConcreteNumberTrivia(int number) {
      client.get(
        'http://numbersapi.com/$number',
        headers: {'Content-Type': 'application/json'},
      );
    }

    Puede parecer que la parte de organizar esta prueba es innecesaria. No estamos haciendo nada con la Respuesta devuelta, después de todo.

    Si bien este razonamiento es cierto por ahora, a medida que agregamos funcionalidad a la implementación del método, no organizar el mockHttpClient para que devuelva un objeto de respuesta válido provocaría todo tipo de errores inesperados. Esto se debe a que un método en un simulacro que no está configurado previamente devuelve nulo.

    También debemos probar si el modelo devuelto contendrá los datos adecuados. Cuando todo transcurre sin problemas y el código de respuesta es 200 SUCCESS, el NumberTriviaModel devuelto debe contener datos convertidos de la respuesta JSON.

    test.dart

    final tNumberTriviaModel =
        NumberTriviaModel.fromJson(json.decode(fixture('trivia.json')));
    ...
    test(
      'should return NumberTrivia when the response code is 200 (success)',
      () async {
        // arrange
        when(mockHttpClient.get(any, headers: anyNamed('headers'))).thenAnswer(
          (_) async => http.Response(fixture('trivia.json'), 200),
        );
        // act
        final result = await dataSource.getConcreteNumberTrivia(tNumber);
        // assert
        expect(result, equals(tNumberTriviaModel));
      },
    );

    Probamos si los datos en el resultado de la llamada al método son los que esperamos comparándolo con un tNumberTriviaModel, que es una instancia construida a partir del mismo dispositivo JSON que devuelve el mockHttpClient.

     

    implementation.dart

    @override
    Future<NumberTriviaModel> getConcreteNumberTrivia(int number) async {
      final response = await client.get(
        'http://numbersapi.com/$number',
        headers: {'Content-Type': 'application/json'},
      );
    
      return NumberTriviaModel.fromJson(json.decode(response.body));
    }
    004 page not found

     

    Te puede interesar:  Flutter Arquitectura Limpia [3] - Refactorización de capa de dominio

    Finalmente, no debemos olvidar dar cuenta de un caso cuando algo sale mal. Si el código de respuesta es 404 NO ENCONTRADO o cualquier otro código de error (básicamente cualquier otro que no sea 200), se debe lanzar una ServerException.

    Similar a cómo probamos si el método arroja una excepción en la parte anterior, ahora hacemos lo mismo. Para mantener el código limpio, almacenamos el método dentro de una variable de llamada y lo invocamos desde una función de orden superior que se pasa al método de espera.

    test.dart

    test(
      'should throw a ServerException when the response code is 404 or other',
      () async {
        // arrange
        when(mockHttpClient.get(any, headers: anyNamed('headers'))).thenAnswer(
          (_) async => http.Response('Something went wrong', 404),
        );
        // act
        final call = dataSource.getConcreteNumberTrivia;
        // assert
        expect(() => call(tNumber), throwsA(TypeMatcher<ServerException>()));
      },
    );
    [wp_ad_camp_4]

    implementation.dart

    @override
    Future<NumberTriviaModel> getConcreteNumberTrivia(int number) async {
      final response = await client.get(
        'http://numbersapi.com/$number',
        headers: {'Content-Type': 'application/json'},
      );
    
      if (response.statusCode == 200) {
        return NumberTriviaModel.fromJson(json.decode(response.body));
      } else {
        throw ServerException();
      }
    }

    Esta excepción se maneja en el Repositorio, por supuesto. Ahora tenemos el método getConcreteNumberTrivia totalmente implementado.

     

    DRY Dentro de las pruebas

    005 hair dryer

    El principio DRY (Don't Repeat Yourself) es el núcleo de cualquier tipo de programación, ya sea que esté haciendo TDD o no. La duplicación de código en las pruebas es tan mala como la duplicación en el código de producción. Como puede ver, la parte de arreglo de las dos primeras pruebas es completamente idéntica, devolviendo 200 SUCCESS. No podemos simplemente mover la configuración del simulacro al método setUp para todo el grupo de prueba, porque la parte de arreglo de la última prueba devuelve 404 NOT FOUND.

    Si no podemos utilizar formas específicas de prueba para luchar contra la duplicación, tendremos que seguir el camino de la vieja escuela: crear métodos. Crearemos uno incluso para el error 404 para el uso en pruebas posteriores.

     

    test.dart

    void main() {
      ...
    
      void setUpMockHttpClientSuccess200() {
        when(mockHttpClient.get(any, headers: anyNamed('headers'))).thenAnswer(
          (_) async => http.Response(fixture('trivia.json'), 200),
        );
      }
    
      void setUpMockHttpClientFailure404() {
        when(mockHttpClient.get(any, headers: anyNamed('headers'))).thenAnswer(
          (_) async => http.Response('Something went wrong', 404),
        );
      }
      
      // Change the code below to use these methods...
    }

    getRandomNumberTrivia

    006 dice

    Al igual que cuando implementamos el repositorio, la diferencia entre los métodos concretos y aleatorios será mínima. De hecho, todo lo que se necesita para obtener un número aleatorio es cambiar la url de http://numbersapi.com/some_number a http://numbersapi.com/random.

    Te puede interesar:  Flutter Arquitectura Limpia [1] - Explicación y Estructura

    Debido a esta inmensa similitud, espero que me perdones una vez más por no seguir completamente el canon de TDD. ¡Estamos a punto de copiar, pegar y modificar algunos códigos! Las modificaciones son muy sencillas: simplemente cambie el nombre de cualquier referencia de "concreto" a "aleatorio" y estará listo.

     

    test.dart

    group('getRandomNumberTrivia', () {
      final tNumberTriviaModel =
          NumberTriviaModel.fromJson(json.decode(fixture('trivia.json')));
    
      test(
        'should preform a GET request on a URL with *random* endpoint with application/json header',
        () {
          //arrange
          setUpMockHttpClientSuccess200();
          // act
          dataSource.getRandomNumberTrivia();
          // assert
          verify(mockHttpClient.get(
            'http://numbersapi.com/random',
            headers: {'Content-Type': 'application/json'},
          ));
        },
      );
    
      test(
        'should return NumberTrivia when the response code is 200 (success)',
        () async {
          // arrange
          setUpMockHttpClientSuccess200();
          // act
          final result = await dataSource.getRandomNumberTrivia();
          // assert
          expect(result, equals(tNumberTriviaModel));
        },
      );
    
      test(
        'should throw a ServerException when the response code is 404 or other',
        () async {
          // arrange
          setUpMockHttpClientFailure404();
          // act
          final call = dataSource.getRandomNumberTrivia;
          // assert
          expect(() => call(), throwsA(TypeMatcher<ServerException>()));
        },
      );
    });

    En cuanto al código de producción, literalmente vamos a copiarlo y pegarlo del método getConcreteNumberTriviaMethod y simplemente cambiaremos el "$number" interpolado en la URL a "aleatorio".

    [wp_ad_camp_5]

    implementation.dart

    @override
    Future<NumberTriviaModel> getRandomNumberTrivia() async {
      final response = await client.get(
        'http://numbersapi.com/random',
        headers: {'Content-Type': 'application/json'},
      );
    
      if (response.statusCode == 200) {
        return NumberTriviaModel.fromJson(json.decode(response.body));
      } else {
        throw ServerException();
      }
    }

    ¡Todas las pruebas están pasando! Por supuesto, este tipo de duplicación es completamente inexcusable. ¡Creemos un método auxiliar entonces! Recibirá la URL para realizar la solicitud GET como argumento.

    implementation.dart

    @override
    Future<NumberTriviaModel> getConcreteNumberTrivia(int number) =>
        _getTriviaFromUrl('http://numbersapi.com/$number');
    
    @override
    Future<NumberTriviaModel> getRandomNumberTrivia() =>
        _getTriviaFromUrl('http://numbersapi.com/random');
    
    Future<NumberTriviaModel> _getTriviaFromUrl(String url) async {
      final response = await client.get(
        url,
        headers: {'Content-Type': 'application/json'},
      );
    
      if (response.statusCode == 200) {
        return NumberTriviaModel.fromJson(json.decode(response.body));
      } else {
        throw ServerException();
      }
    }

    Aunque es altamente improbable, esta refactorización aún podría haber estropeado algo. Solo después de volver a probar el código y verlo pasar, podemos dormir bien por la noche.

    ¿Qué sigue?

    Nos estamos moviendo bastante rápido. Acabamos de terminar toda la capa de datos y con el dominio ya implementado, solo queda la capa de presentación. Nuevamente mantendremos la tradición y comenzaremos desde el centro de la capa, que es el titular de la lógica de presentación. En el caso de la aplicación Number Trivia, vamos a utilizar Bloc.

     

    Aquí les comparto el video

    Deja una respuesta

    Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

    Subir