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
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
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
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:
- Agregue un parámetro de consulta a la URL, haciendo que se vea así: http://numbersapi.com/42?json
- 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'},
);
}
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));
}
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
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
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".
[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
Lo siento, debes estar conectado para publicar un comentario.