Flutter Arquitectura Limpia [9] – Fuente de Datos Remota

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!

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.

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!

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.

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

 

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>()));
  },
);

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.

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“.

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

Previous

Flutter Arquitectura Limpia [8] – Fuente de Datos Local

Flutter Arquitectura Limpia [10] – Bloc Scaffolding y Conversión de Entrada

Next

Deja un comentario