I am newbie in Flutter as well as TDD and I do not understand why and when to mark unit test as async in flutter.
Looking through the documentation I found this code snippet:
// Create a MockClient using the Mock class provided by the Mockito package.
// Create new instances of this class in each test.
class MockClient extends Mock implements http.Client {}
main() {
group('fetchPost', () {
test('returns a Post if the http call completes successfully', () async {
final client = MockClient();
// Use Mockito to return a successful response when it calls the
// provided http.Client.
when(client.get('https://jsonplaceholder.typicode.com/posts/1'))
.thenAnswer((_) async => http.Response('{"title": "Test"}', 200));
expect(await fetchPost(client), const TypeMatcher<Post>());
});
test('throws an exception if the http call completes with an error', () {
final client = MockClient();
// Use Mockito to return an unsuccessful response when it calls the
// provided http.Client.
when(client.get('https://jsonplaceholder.typicode.com/posts/1'))
.thenAnswer((_) async => http.Response('Not Found', 404));
expect(fetchPost(client), throwsException);
});
});
}
If you look carefully you will noticed that first test is marked as async and the second is not. Why is that? What is different between these two test(except the cases) so that the first one has to be async?
Thanks :)
When you want to use await, you have to mark a callback or function in general as async.
In your case:
expect(await fetchPost(client), const TypeMatcher<Post>());
The await is needed because the result of the function execution matters. They are expecting exactly a Post type to be returned, hence, they need the await.
In the other case:
expect(fetchPost(client), throwsException);
It only matters that an exception is thrown, but the result is irrelevant.
When to mark callback with async when testing
Whenever you need await, you mark your callbacks with async. In general, I would advise always awaiting functions in tests because the tests will otherwise run in parallel, which could show undesired behavior.
Related
I am trying to process a method of type Stream from a Bloc implementation, but it seems I am not properly handling concurrency and I would need some assistance.
I have the following interface and implementations to wrap data
abstract class ResultWrapper {}
class ResultStart extends ResultWrapper {}
class ResultFetching extends ResultWrapper {}
class ResultDone extends ResultWrapper {}
then I have the following method to emit above classes in a stream fashion way
class FetchCountries {
Stream<ResultWrapper> fetch() async* {
yield ResultStart();
print('starting request');
yield ResultFetching();
print('fetching');
yield ResultDone();
print('done');
}
}
and finally, this is my Bloc implementation where I try to orchestrate my Event with States
CountryBloc(this.fetchCountries): super(InitialState()) {
on<FetchCountriesEvent>((event, emit) async {
fetchCountries.fetch().listen((result) {
if (result is ResultStart) {
emit(LoadingState());
} else if (result is ResultFetching) {
emit(AllFetchedState());
} else if (result is ResultDone) {
emit(DoneState());
}
});
});
}
but it seems I am either not listening the stream method properly or emiting States properly because I get following exception
Exception has occurred. _AssertionError ('package:bloc/src/bloc.dart':
Failed assertion: line 137 pos 7: '!_isCompleted': emit was called
after an event handler completed normally. This is usually due to an
unawaited future in an event handler. Please make sure to await all
asynchronous operations with event handlers and use emit.isDone after
asynchronous operations before calling emit() to ensure the event
handler has not completed.
I searched about that stacktrace but almost all suggestions I found are related to Future methods instead Stream methods and I was really not able to find a solution for my use case.
Can someone please tell me how to properly consume this Stream method from Bloc to emit states?
Thank you so much in advance.
You should use await emit.forEach for stream.
It will be like
await emit.forEach(
yourStream,
onData: (data) {
return yourState;
},
)
I am trying to test a class with a function like that
class A {
void doStuff() {
// Do stuff...
notifyListeners();
}
}
I am currently using the flutter-test package to automate my tests.
My test looks like this:
void main() {
final myInstance = A();
group("Class A", () {
test("should correctly do stuff", () async {
myInstance.doStuff();
expect(...);
});
});
}
The test is working and expect yields the correct result.
However, console shows an error message:
The following StateError was thrown while dispatching notifications for Class A:
Bad state: Future already completed
What causes this error and how can I prevent it from happening?
Turns out, notifyListeners() was the root-cause here.
It threw an error, presumably because it was not yet initialized.
By waiting for its initialization, the error has stopped occuring.
See TestWidgetsFlutterBinding.ensureInitialized for reference
setUpAll(() {
TestWidgetsFlutterBinding.ensureInitialized();
});
I was using mocktail for my unit testing and I could not find a way to throw an Exception on the first call but later, on the second call, answer with the right output.
I could find this solution for two different answers but this was not enough for testing exceptions thrown. So I needed to come up with a modified solution.
This is what I wanted to achieve. By the way, I was testing a connection retry with http get.
import 'package:http/http.dart' as http;
import 'package:mocktail/mocktail.dart';
class MockClient extends Mock implements http.Client {}
final mockClient = MockClient();
//First time fails, second one retrieves a result. This doesn't work on Mocktail
when(() => mockClient.get(Uri.parse(url)))
.thenThrow(SocketException()) // Call 1
.thenAnswer((_) => Future.value(http.Response("page content", 200)) // Call 2
);
After trying different ideas, a possible solution was to store each answer inside of a List
import 'package:http/http.dart' as http;
import 'package:mocktail/mocktail.dart';
class MockClient extends Mock implements http.Client {}
final mockClient = MockClient();
final List<Future<http.Response> Function(Invocation)> answers = [
(_) => throw SocketException(),
(_) => Future.value(http.Response("page content", 200)),
];
when(() => mockClient.get(Uri.parse(url)))
.thenAnswer((invocation) => answers.removeAt(0)(invocation));
// Calling answers.removeAt(0) without the lambda method returns the same answer on all of them
In this example Invocation is not used and checks only 2 consecutive calls. But that behavior can be extended from what you can see here.
I haven't tried this with Mockito but it should work in a similar way adapting the syntax.
I tried to mock database to test my local api, i search in official document finding mockito which can work with remote api fine, but also can not work with local database out of box, is there any way to work around of it?
In these cases, you have two options (among many others). Even if my examples assume you're making HTTP calls, it doesn't matter. You can use these strategies regardless the specific use case I'm exposing!
The first one is using the "Strategy pattern" to create an interface for the API and then switch between a test and a production API. Here's a simple example:
abstract class HttpRepository {
const HttpRepository();
Future<Something> sendRequest();
}
You can now create 2 concrete classes: one is for the actual API call and the other is just a mock for tests.
/// Use this in your tests
class MockHttpRepository extends HttpRepository {
const MockHttpRepository();
#override
Future<Something> sendRequest() async {
// Simulating the HTTP call
await Future.delayed(const Duration(seconds: 2));
return Something();
}
}
/// Use this in your Flutter code to make the actual HTTP call or whatever else
class ApiHttpRepository extends HttpRepository {
const ApiHttpRepository();
#override
Future<Something> sendRequest() async {
// Doing a real HTTP call
final response = await makeGetOrPost();
return Something.withData(response);
}
}
In this way, you'll use ApiHttpRepository in your Flutter app and MockHttpRepository in tests. Use const constructors whenever possible.
The other way is using mocks to simulate fake HTTP calls or anything else. Basically, you're using when to "trap" a method call and return a fake response you can control.
// 1. "Enable" mocking on your type
class MockRepo extends Mock implements ApiHttpRepository {}
// 2. Mock methods
const ApiHttpRepository repo = MockRepo();
when(repo.sendRequest()).thenAnswer((_) async => Something());
In this case, we're using thenAnswer because the return type of sendRequest() is of type Future<T>. In your case, if you are reading data from a database you just need to:
Make your class "mockable" using extends Mock implements YourClass
Use when on the mockable instance and control the output
Make sure to use thenAnswer if the method returns a Future<T> and thenReturn in all the other cases.
I am developing an android / ios application in flutter, and I have chosen to use redux for my state management.
I am writing unit tests for my redux actions, which have been implemented using the async_redux package.
I am following the excellent guidelines set out for testing by the author of the package, but I am not sure how to mock the dispatch of further actions from my action under test.
For example, the below LogoutAction dispatchs a DeleteDatabaseAction and waits for it to complete:
class LogoutAction extends ReduxAction<AppState> {
#override
Future<AppState> reduce() async {
await dispatchFuture(DeleteDatabaseAction());
return AppState.initialState();
}
}
class DeleteDatabaseAction extends ReduxAction<AppState> {
#override
FutureOr<AppState> reduce() {
throw StateError(
'Unwanted call to runtime implementation of DeleteDatabaseAction',
);
}
}
void main() {
final store = Store<AppState>(initialState: AppState(loggedIn: true));
final storeTester = StoreTester.from(store);
test('Logout action should return correct state and not throw StateError', () async {
storeTester.dispatch(LogoutAction());
TestInfo<AppState> info = await storeTester.wait(LogoutAction);
expect(info.state.loggedIn, false);
});
}
I want to test only the action under test, and stub out all further action calls.
i.e. How can I mock / stub the dispatch and dispatchFuture methods on ReduxAction, so that the runtime DeleteDatabaseAction implementation is not run?
So far I have attempted:
Inject DeleteDatabaseAction using get_it and inject a mock during test
I have 100+ actions that will now need to be added to my context
Some actions have parameters that change based on where they are called from, so cannot be registered at app startup
Subclass Store, override the above methods and use the subclass in my test here final store = Store<AppState>(initialState: AppState(loggedIn: true))
I will not be able to dispatch my action under test, as it uses the same store in the async_redux test implementation
Here: storeTester.dispatch(LogoutAction());
Create a separate Dispatcher implementation, inject this and override with a mock during tests
This will work, but it is new framework, I can go this route but now I am deviating from the well documented framework provided by asyn_redux
This wasn't available when you asked this question. But now the answer is here: https://pub.dev/packages/async_redux#mocking-actions-and-reducers
To mock an action and its reducer, start by creating a MockStore in your tests, instead of a regular Store. The MockStore has a mocks parameter which is a map where the keys are action types, and the values are the mocks. For example:
var store = MockStore<AppState>(
initialState: initialState,
mocks: {
MyAction1 : ...
MyAction2 : ...
...
},
);
However, there are other ways:
As you said, use get_it or some other dependency injection code to inject the http client into the action reducer. This works well.
Use a DAO. For example:
class DeleteDatabaseAction extends ReduxAction<AppState> {
#override
Future<AppState> reduce() {
await dao.deleteDatabase();
return null;
}
Then you can mock the DAO itself, or inject the DAO via get_it. You can also make the dao a getter to some BaseAction (that extends ReduxAction) and inject it there.