This repository is based on Reso Coder's awesome TDD Clean Architecture Course, which you can see by clicking here or clicking here to go to the original repo.
Since the tutorial was made in 2019, a lot of things changed. Now the new versions of flutter are null safety, what made some of the old versions' code deprecated. Additionally some of the libraries used on the original tutorial were discontinued, like the Data Connection Checker.
While going through the tutorial, I've updated some of the old code to make it work on the new versions of flutter. Some library choices were made, like the Mockito in favor of Mocktail due to it making testing with null safety work closer to what's in the tutorial without the need to generate more complicated code.
The libraries were updated to the latest version, except for the flutter_bloc, which was updated to v7.2.0, the last null safety version before the changes on how to Handle events, because I wanted to keep the code as close as possible to the old version to make it easier to follow the tutorial. Additionally, some libraries were removed in favor of others. which is the case for using mocktail instead of mockito. More details below.
get_it: ^7.2.0
flutter_bloc: ^7.2.0
equatable: ^2.0.3
dartz: ^0.10.1
http: ^0.13.4
shared_preferences: ^2.0.15
mocktail: ^0.3.0
internet_connection_checker: ^0.0.1+4
Mockito's any
argument doesn't work very well with null safety due to being illegal to pass null
where a non-nullable variable is expected. Mockito's workaround for this problem can be seen by going to its readme clicking here. Since it would deviate a lot from the original tutorial, I decided to go with Mocktail instead. Check the example below from the number_trivia_repository_impl_test.dart
:
A test on the old version original code (with mockito):
test(
'should return server failure when the call to remote data source is unsuccessful',
() async {
// arrange
when(mockRemoteDataSource.getConcreteNumberTrivia(any))
.thenThrow(ServerException());
// act
final result = await repository.getConcreteNumberTrivia(tNumber);
// assert
verify(mockRemoteDataSource.getConcreteNumberTrivia(tNumber));
verifyZeroInteractions(mockLocalDataSource);
expect(result, equals(Left(ServerFailure())));
},
);
test(
'should return server failure when the call to remote data source is unsuccessful',
() async {
// arrange
when(() => mockRemoteDataSource.getConcreteNumberTrivia(any()))
.thenThrow(ServerException());
// act
final result = await repository.getConcreteNumberTrivia(tNumber);
// assert
verify(() => mockRemoteDataSource.getConcreteNumberTrivia(tNumber));
verifyZeroInteractions(mockLocalDataSource);
expect(result, equals(Left(ServerFailure())));
},
);
The only changes where the when
, the any
and the verify
.
Since the data_connection_checker was discontinued and no longer worked, I decided to go with the internet_connection_checker library. Everything that has the type DataConnectionChecker
becomes InternetConnectionChecker
and we are done.
To avoid certain runtime errors, null safety requires you to declare non-nullable uninitialized variable with the late
keyword to tell the program that the variable will be initialized later on. This change is present in all the test files, like this example at the beginning of get_concrete_number_trivia_test.dart
:
late GetConcreteNumberTrivia usecase;
late MockNumberTriviaRepository mockNumberTriviaRepository;
setUp(() {
mockNumberTriviaRepository = MockNumberTriviaRepository();
usecase = GetConcreteNumberTrivia(mockNumberTriviaRepository);
});
When testing for bloc states (number_trivia_bloc_test.dart
), you should send a bloc.stream
instead of the actual bloc
as the first argument when calling expectLater
:
test(
'should emit [Error] when the input is invalid',
() async {
// arrange
when(() => mockInputConverter.stringToUnsignedInteger(any()))
.thenReturn(Left(InvalidInputFailure()));
// assert later
final expected = [
Error(message: INVALID_INPUT_FAILURE_MESSAGE),
];
expectLater(bloc.stream, emitsInOrder(expected));
// act
bloc.add(const GetTriviaForConcreteNumber(tNumberString));
},
);
Instead of what's in the original code. Additionally, you're not gonna need to provide the Empty()
state as part of the expected states since it's already being tested right before the other tests.
test('initialState should be Empty', () {
// assert
expect(bloc.initialState, equals(Empty()));
});
Now the app theme is gonna be defined like this in the main.dart
:
Widget build(BuildContext context) {
return MaterialApp(
title: 'Number Trivia',
theme: theme.copyWith(
colorScheme: theme.colorScheme.copyWith(
primary: Colors.green.shade800,
secondary: Colors.green.shade600,
),
),
home: const NumberTriviaPage(),
);
}
I believe that the main changes were covered here. If there's anything left, feel free to contact me.