Skip to content

Commit

Permalink
Added some tests and got all tests passing again - DC
Browse files Browse the repository at this point in the history
  • Loading branch information
ChopinDavid committed Jan 5, 2025
1 parent 2d53b16 commit 70f4af3
Show file tree
Hide file tree
Showing 10 changed files with 738 additions and 313 deletions.
3 changes: 1 addition & 2 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@ void main() {
GetIt.instance.registerSingleton<DbHelper>(DbHelper());
GetIt.instance.registerSingleton<ExplanationHelper>(ExplanationHelper());
GetIt.instance.registerSingleton<UrlHelper>(const UrlHelper());
GetIt.instance
.registerSingleton<TranslationService>(const TranslationService());
GetIt.instance.registerSingleton<TranslationService>(TranslationService());

runApp(const MyApp());
}
Expand Down
10 changes: 7 additions & 3 deletions lib/services/translation_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@ import 'dart:convert';
import 'dart:io';

import 'package:collection/collection.dart';
import 'package:http/http.dart' as http;
import 'package:flutter/foundation.dart';
import 'package:http/http.dart';

class TranslationService {
const TranslationService();
TranslationService({@visibleForTesting Client? client})
: _client = client ?? Client();

final Client _client;

Future<String> getSentenceFrom({required int tatoebaKey}) async {
final response = await http
final response = await _client
.get(Uri.parse('https://tatoeba.org/en/api_v0/sentence/$tatoebaKey'));

if (response.statusCode < 200 || response.statusCode >= 300) {
Expand Down
106 changes: 53 additions & 53 deletions lib/utilities/explanation_helper.dart

Large diffs are not rendered by default.

69 changes: 38 additions & 31 deletions lib/widgets/translation_widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:uchu/blocs/translation/translation_bloc.dart';

class TranslationWidget extends StatelessWidget {
const TranslationWidget({super.key});
const TranslationWidget({
super.key,
@visibleForTesting this.mockNavigatorState,
});

final NavigatorState? mockNavigatorState;

@override
Widget build(BuildContext context) {
Expand All @@ -12,45 +17,47 @@ class TranslationWidget extends StatelessWidget {
BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.8),
child: Card(
clipBehavior: Clip.hardEdge,
child: BlocBuilder<TranslationBloc, TranslationState>(
builder: (context, state) {
if (state is TranslationLoaded) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
const Spacer(),
IconButton(
onPressed: Navigator.of(context).pop,
icon: const Icon(Icons.close))
],
),
Flexible(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
const Spacer(),
IconButton(
onPressed:
(mockNavigatorState ?? Navigator.of(context)).pop,
icon: const Icon(Icons.close))
],
),
BlocBuilder<TranslationBloc, TranslationState>(
builder: (context, state) {
if (state is TranslationLoaded) {
return Flexible(
child: Padding(
padding: const EdgeInsets.only(
left: 24.0,
right: 24.0,
bottom: 24.0,
),
child: Text(state.translation),
)),
],
);
}
));
}

if (state is TranslationError) {
return const Text('There was an issue loading translation...');
}
if (state is TranslationError) {
return const Text(
'There was an issue loading translation...');
}

return const Padding(
padding: EdgeInsets.all(24.0),
child: SizedBox(
height: 24.0,
width: 24.0,
child: CircularProgressIndicator()),
);
},
return const Padding(
padding: EdgeInsets.all(24.0),
child: SizedBox(
height: 24.0,
width: 24.0,
child: CircularProgressIndicator()),
);
},
),
],
),
),
);
Expand Down
60 changes: 60 additions & 0 deletions test/unit_tests/blocs/translation/translation_bloc_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:get_it/get_it.dart';
import 'package:mocktail/mocktail.dart';
import 'package:uchu/blocs/translation/translation_bloc.dart';
import 'package:uchu/services/translation_service.dart';

import '../../mocks.dart';

main() {
late TranslationService mockTranslationService;
late TranslationBloc testObject;
const expectedTatoebaKey = 123;
const expectedTranslation = 'the quick brown fox jumps over the lazy dog';
const expectedErrorMessage = 'something went wrong!';

setUp(() async {
await GetIt.instance.reset();
mockTranslationService = MockTranslationService();
GetIt.instance
.registerSingleton<TranslationService>(mockTranslationService);

testObject = TranslationBloc();
});

blocTest(
'emits TranslationLoading and TranslationLoaded when TranslationService.getSentenceFrom does not throw',
setUp: () {
when(() => mockTranslationService.getSentenceFrom(
tatoebaKey: expectedTatoebaKey))
.thenAnswer((_) async => expectedTranslation);
},
build: () => testObject,
act: (bloc) => bloc.add(
const TranslationFetchTranslationEvent(
tatoebaKey: expectedTatoebaKey,
),
),
expect: () => [
const TranslationLoading(),
const TranslationLoaded(translation: expectedTranslation),
]);

blocTest(
'emits TranslationLoading and TranslationError when TranslationService.getSentenceFrom does throw',
setUp: () {
when(() => mockTranslationService.getSentenceFrom(
tatoebaKey: expectedTatoebaKey)).thenThrow(expectedErrorMessage);
},
build: () => testObject,
act: (bloc) => bloc.add(
const TranslationFetchTranslationEvent(
tatoebaKey: expectedTatoebaKey,
),
),
expect: () => [
const TranslationLoading(),
const TranslationError(errorString: expectedErrorMessage),
]);
}
17 changes: 17 additions & 0 deletions test/unit_tests/mocks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@ import 'dart:math';

import 'package:bloc_test/bloc_test.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart';
import 'package:mocktail/mocktail.dart';
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
import 'package:sqflite/sqflite.dart';
import 'package:uchu/blocs/exercise/exercise_bloc.dart';
import 'package:uchu/blocs/translation/translation_bloc.dart';
import 'package:uchu/models/noun.dart';
import 'package:uchu/services/translation_service.dart';
import 'package:uchu/utilities/db_helper.dart';
import 'package:uchu/utilities/exercise_helper.dart';
import 'package:uchu/utilities/explanation_helper.dart';
Expand Down Expand Up @@ -45,3 +48,17 @@ class MockUrlLauncher extends Mock
implements UrlLauncherPlatform {}

class MockUrlHelper extends Mock implements UrlHelper {}

class MockTranslationService extends Mock implements TranslationService {}

class MockClient extends Mock implements Client {}

class MockTranslationBloc extends MockBloc<TranslationEvent, TranslationState>
implements TranslationBloc {}

class MockNavigatorState extends Mock implements NavigatorState {
@override
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
return super.toString();
}
}
95 changes: 95 additions & 0 deletions test/unit_tests/services/translation_service_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import 'dart:io';

import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart';
import 'package:mocktail/mocktail.dart';
import 'package:uchu/services/translation_service.dart';

import '../mocks.dart';

main() {
late Client mockClient;
late TranslationService testObject;

setUp(() {
mockClient = MockClient();
testObject = TranslationService(client: mockClient);
});

group(
'getSentenceFrom',
() {
const expectedTatoebaKey = 1;
const expectedTranslation = 'translation';
const goodResponseBody =
'{"translations":[[{"lang":"eng","text":"$expectedTranslation"}]]}';
const noEngTranslationResponseBody =
'{"translations":[[{"lang":"ger","text":"Übersetzung"}]]}';

test(
'throws HttpException if tatoeba response is < 200',
() async {
when(() => mockClient.get(Uri.parse(
'https://tatoeba.org/en/api_v0/sentence/$expectedTatoebaKey')))
.thenAnswer(
(_) async => Response('error', 199),
);

expect(
() async => await testObject.getSentenceFrom(tatoebaKey: 1),
throwsA(isA<HttpException>()),
);
},
);

test(
'throws HttpException if tatoeba response is >299',
() async {
when(() => mockClient.get(Uri.parse(
'https://tatoeba.org/en/api_v0/sentence/$expectedTatoebaKey')))
.thenAnswer(
(_) async => Response('error', 300),
);

expect(
() async => await testObject.getSentenceFrom(tatoebaKey: 1),
throwsA(isA<HttpException>()),
);
},
);
test(
'does not throw an exception if tatoeba response is 200 and there is an english translation',
() async {
when(() => mockClient.get(Uri.parse(
'https://tatoeba.org/en/api_v0/sentence/$expectedTatoebaKey')))
.thenAnswer(
(_) async => Response(goodResponseBody, 200),
);

expect(await testObject.getSentenceFrom(tatoebaKey: 1),
expectedTranslation);

verify(() => mockClient.get(Uri.parse(
'https://tatoeba.org/en/api_v0/sentence/$expectedTatoebaKey')))
.called(1);
},
);

test(
'does throw an exception if tatoeba response is 200 but there is not an english translation',
() async {
when(() => mockClient.get(Uri.parse(
'https://tatoeba.org/en/api_v0/sentence/$expectedTatoebaKey')))
.thenAnswer(
(_) async => Response(noEngTranslationResponseBody, 200),
);

expect(
() async => await testObject.getSentenceFrom(tatoebaKey: 1),
throwsA(isA<Exception>()),
);
},
);
},
);
}
4 changes: 2 additions & 2 deletions test/unit_tests/utilities/exercise_helper_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ main() {
);

group(
'getAnswerGroupsForSentence',
'getAnswerGroupsForSentenceExercise',
() {
test(
'creates new list when bare is not yet registered in answerGroups',
Expand Down Expand Up @@ -88,7 +88,7 @@ main() {
);

group(
'getTextSpansFromSentence',
'getSpansFromSentence',
() {
test('number of spans returned is correct', () {
const sentence = "Всему' своё вре'мя.";
Expand Down
Loading

0 comments on commit 70f4af3

Please sign in to comment.