Flutter: how to mock a stream - testing

I´m using the bloc pattern and now I came to test the UI. My question is: how to mock streams?
This is the code that I have:
I give to the RootPage class an optional mockBloc value and I will set it to the actual _bloc if this mockBloc is not null
class RootPage extends StatefulWidget {
final loggedOut;
final mockBlock;
RootPage(this.loggedOut, {this.mockBlock});
#override
_RootPageState createState() => _RootPageState();
}
class _RootPageState extends State<RootPage> {
LoginBloc _bloc;
#override
void initState() {
super.initState();
if (widget.mockBlock != null)
_bloc = widget.mockBlock;
else
_bloc = new LoginBloc();
if (widget.loggedOut == false)
_bloc.startLoad.add(null);
}
...
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: _bloc.load,
builder: (context, snapshot) {
if (snapshot.hasData) {
return Column(
children: <Widget>
...
This is what I´ve tried:
testWidgets('MyWidget', (WidgetTester tester) async {
MockBloc mockBloc = new MockBloc();
MockTokenApi mockTokenApi = new MockTokenApi();
await tester.pumpWidget(new MaterialApp(
home: RootPage(false, mockBlock: mockBloc)));
when(mockBloc.startLoad.add(null)).thenReturn(mockBloc.insertLoadStatus.add(SettingsStatus.set)); //this will give in output the stream that the StreamBuilder is listening to (_bloc.load)
});
await tester.pump();
expect(find.text("Root"), findsOneWidget);
});
The result that I achieve is always to get:
The method 'add' was called on null
when _bloc.startLoad.add(null) is called

You can create a mock Stream by creating a mock class using mockito. Here's a sample on how one can be implemented.
import 'package:mockito/mockito.dart';
class MockStream extends Mock implements Stream<int> {}
void main() async {
var stream = MockStream();
when(stream.first).thenAnswer((_) => Future.value(7));
print(await stream.first);
when(stream.listen(any)).thenAnswer((Invocation invocation) {
var callback = invocation.positionalArguments.single;
callback(1);
callback(2);
callback(3);
});
stream.listen((e) async => print(await e));
}

Since the advent of null safety, you can do this as follows:
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:test/test.dart';
import 'test_test.mocks.dart';
#GenerateMocks([Stream])
void main() {
test('foo', () async {
var stream = MockStream();
Stream<int> streamFunc() async* {
yield 1;
yield 2;
yield 3;
}
when(stream.listen(
any,
onError: anyNamed('onError'),
onDone: anyNamed('onDone'),
cancelOnError: anyNamed('cancelOnError'),
)).thenAnswer((inv) {
var onData = inv.positionalArguments.single;
var onError = inv.namedArguments[#onError];
var onDone = inv.namedArguments[#onDone];
var cancelOnError = inv.namedArguments[#cancelOnError];
return streamFunc().listen(onData, onError: onError, onDone: onDone, cancelOnError: cancelOnError);
});
stream.listen((e) async => print(await e));
});
}
source

Related

Flutter API calls with Future Builder returns error when future method called inside initState()

I was trying to call APIs that need to be loaded the first time the page is built. So I used FutureBuilder and written an initState call. Every page works fine but only on one page, I faced an issue with context. Then I understood about didChangeDependencies. Which is the correct way to call APIs from another class (that need to access the current widget context too).
Where should I call calculatorFuture = getParkingAreaList(); in initState() or didChangeDependencies().
String TAG = "CalculatorPage";
class CalculatorPage extends StatefulWidget {
const CalculatorPage({
Key key,
}) : super(key: key);
#override
_CalculatorPageState createState() => _CalculatorPageState();
}
class _CalculatorPageState extends State<CalculatorPage> {
Future calculatorFuture;
#override
void initState() {
super.initState();
}
#override
void didChangeDependencies() {
super.didChangeDependencies();
calculatorFuture = getParkingAreaList();
}
Future<bool> apiCallHere() async {
print('apiCallHere');
String lang = getCurrentLanguage(context);
print('going to call res');
var res = await HttpHandler.apiCallFromHttpHanlderClass(context);
if (res == null) {
print('res is null');
showToast(LanguageLocalization.of(context).getTranslatedValue('generic_failure_message'));
return false;
}
print(res);
if(res["STATUS_CODE"] == 1) {
print('apiCallHere status code = 1');
apiData = res['someData'];
return true;
}
else {
print('apiCallHere status code !=1');
return false;
}
}
#override
Widget build(BuildContext context) {
print(TAG);
return Scaffold(
appBar: commonAppBar(context: context, title: 'calculator'),
body: FutureBuilder(
future: calculatorFuture,
builder: (context, snapshot) {
print(snapshot);
if (snapshot.hasError || (snapshot.hasData && !snapshot.data)) {
print('has error');
print(snapshot.hasError ? snapshot.error : "unable to load data");
return unableToLoadView(context);
} else if (snapshot.hasData && snapshot.data) {
print('completed future');
return Container(
margin: commonPagePadding,
child: ListView(
children: <Widget>[
//some widget that deals with apiData
],
)
);
} else {
print('loading');
return showLoaderWidget(context);
}
},
)
);
}
}
you can call future function in didchangedependenci changing like that
#override
Future <void> didChangeDependencies() async{
super.didChangeDependencies();
calculatorFuture = await getParkingAreaList();
}
If you want to call the API once when a page loads up just place the future inside initState like the example below.
#override
void initState() {
super.initState();
calculatorFuture = getParkingAreaList();
}

Unhandled Exception: type 'String' is not a subtype of type 'Future<String>' in type cast

I am gettting this error ,can anyone help me to sort out this error
static Future<String> get_video_lecture_subject(int schoolId,int classroom) async {
var body;
body = jsonEncode({
"school_id": schoolId,
"classroom": classroom,
});
final response = await http.post(
'https://b5c4tdo0hd.execute-api.ap-south-1.amazonaws.com/testing/get-video-lecture-subjects',
headers: {"Content-Type": "application/json"},
body: body,
);
print(response.body.toString());
return response.body.toString();
}
i have used above function in getpref() function
getpref() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
int classNo = int.parse(prefs.getString("class")[0]);
int schoolId = prefs.getInt("school_id");
print("hello");
response=(await ApiService.get_video_lecture_subject(schoolId, classNo)) as Future<String> ;
print(response);
}
The expression:
(await ApiService.get_video_lecture_subject(schoolId, classNo)) as Future<String>
calls your method get_video_lecture_subject. That returns a Future<String>.
You then await that future, which results in a String.
Then you try to cast that String to Future<String>. That fails because a string is not a future.
Try removing the as Future<String>.
Check out this sample example and let me know if it works
import 'dart:async';
import 'package:flutter/material.dart';
void main() {
getPrefs();
runApp(MyApp());
}
void getPrefs() async {
String value = await yourfunction();
print(value);
}
Future<String> yourfunction() {
final c = new Completer<String>();
// Below is the sample example you can do your api calling here
Timer(Duration(seconds: 1), () {
c.complete("you should see me second");
});
// Process your thigs above and send the string via the complete method.
// in you case it will be c.complete(response.body);
return c.future;
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: SafeArea(
child:
/* FutureBuilder<String>(
future: yourfunction(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text(snapshot.data);
}
return CircularProgressIndicator();
}), */
Text('')),
));
}
}
Replace the line with this
final res = await ApiService.get_video_lecture_subject(schoolId, classNo);
Update: Change the variable name (maybe it's type was initialized as Future)

Flutter: how to mock Bloc

I would like to mock my Bloc in order to test my view.
For example, this is my Bloc:
class SearchBloc extends Bloc<SearchEvent, SearchState> {
#override
// TODO: implement initialState
SearchState get initialState => SearchStateUninitialized();
#override
Stream<SearchState> mapEventToState(SearchState currentState, SearchEvent event) async* {
if (event is UserWrites) {
yield (SearchStateInitialized.success(objects);
}
}
}
And this is the view:
class _SearchViewState extends State<SearchView> {
final TextEditingController _filterController = new TextEditingController();
#override
void initState() {
_filterController.addListener(() {
widget._searchBloc.dispatch(FetchByName(_filterController.text));
}
}
TextField buildAppBarTitle(BuildContext context) {
return new TextField(
key: Key("AppBarTextField"),
controller: _filterController,
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: buildAppBarTitle(context),),
body: buildBlocBuilder(),
);
}
BlocBuilder<SearchEvent, SearchState> buildBlocBuilder() {
return BlocBuilder(
bloc: widget._searchBloc,
builder: (context, state) {
if (state is SearchStateUninitialized) {
return Container(
key: Key("EmptyContainer"),
);
}
return buildInitializedView(state, context);
}
});
buildInitializedView(SearchStateInitialized state, BuildContext context) {
if (state.objects.isEmpty) {
return Center(child: Text("Nothing found"),);
} else {
return buildListOfCards();
}
}
}
Now, this is my test:
testWidgets('Should find a card when the user searches for something', (WidgetTester tester) async {
_searchView = new SearchView(_searchBloc);
when(mockService.find( name: "a")).thenAnswer((_) =>
[objects]);
await tester.pumpWidget(generateApp(_searchView));
await tester.enterText(find.byKey(Key("searchBar")), "a");
await tester.pump();
expect(find.byType(Card), findsOneWidget);
});
}
As you can see, I just want to test that, when the user writes something in the search, and the object he's looking for exists, a card should be shown.
If I understood correctly, you are mocking some service that is used by the searchBloc. I personally try to design the app in a way that the app only depends on a bloc and the bloc may depend on some other services. Then when I would like to make a widget test, I only need to mock the bloc. You can use bloc_test package for that.
There is this example on the bloc_test page for stubbing a counterBloc:
// Create a mock instance
final counterBloc = MockCounterBloc();
// Stub the bloc `Stream`
whenListen(counterBloc, Stream.fromIterable([0, 1, 2, 3]));
however, I often do not need to stub the bloc stream and it is enough to emit the state, like this
when(counterBloc.state).thenAnswer((_) => CounterState(456));
Hope this helps.
Have a look at a post from David Anaya which deal with Unit Testing with “Bloc” and mockito.
The last version of his example is here
Sometimes widgets require a little time to build. Try with:
await tester.pumpWidget(generateApp(_searchView));
await tester.enterText(find.byKey(Key("searchBar")), "a");
await tester.pump(Duration(seconds: 1));
expect(find.byType(Card), findsOneWidget);
To mock the bloc, you can use the bloc_test package
Also, you may watch this tutorial which covers bloc testing include mock bloc very nice.

Why is data not being refreshed in StreamBuilder?

I am making a movie list application in Flutter and when I open the app I am displaying most-viewed movies list. When I click a button, I fetch new data and add it to the sink which is then sent to the StreamBuilder and should refresh the screen with new data.
But that doesn't happen. I cannot fathom why!
Here is my code for the Repository:
class MoviesRepository {
final _movieApiProvider = MovieApiProvider();
fetchAllMovies() async => _movieApiProvider.fetchMovieList();
fetchAllSimilarMovies(genreIdeas) async => await _movieApiProvider.fetchMoviesLikeThis(genreIdeas);
fetchTopRatedMovies() async => await _movieApiProvider.fetchTopRatedMovieList();
}
Here is my code for bloc:
class MoviesBloc {
final _moviesRepository = MoviesRepository();
final _moviesFetcher = BehaviorSubject<Result>();
Sink<Result> get allMovies => _fetcherController.sink;
final _fetcherController = StreamController<Result>();
Observable<Result> get newResults => _moviesFetcher.stream;
fetchAllMovies() async {
Result model = await _moviesRepository.fetchAllMovies();
allMovies.add(model);
}
fetchTopRatedMovies() async{
Result model = await _moviesRepository.fetchTopRatedMovies();
allMovies.add(model);
}
dispose() {
_moviesFetcher.close();
_fetcherController.close();
}
MoviesBloc() {
_fetcherController.stream.listen(_handle);
}
void _handle(Result event) {
_moviesFetcher.add(event);
}
}
final bloc = MoviesBloc();
UPDATE
And here is my code for the StreamBuilder:
class HomeScreenListState extends State<HomeScreenList> {
#override
void initState() {
super.initState();
print("init");
bloc.fetchAllMovies();
}
#override
void dispose() {
super.dispose();
bloc.dispose();
}
#override
Widget build(BuildContext context) {
return StreamBuilder<Result>(
builder: (context, snapshot) {
if (snapshot.hasData) {
print("new data is here");
return buildList(snapshot);
} else if (snapshot.hasError) {
return Text('Error!!');
}
return Center(
child: CircularProgressIndicator(),
);
},
stream: bloc.newResults,
);
}
}
And this is the button that triggers to get new data and add it to the sink:
child: RaisedButton(onPressed: () {
bloc.fetchTopRatedMovies();
},
This button fetches top-rated movies and add to the same sink. Now the StreamBuilder should pick up the new data as I think.
Where is the problem??
You can use Equatable alongside your BLoC implementation to manage state changes on your screen. Here's a guide that I suggest trying out.

Unable to convert json response to list. Casting error

I am trying to convert my json response to list but getting the below error.
Json is fetched inside map ,but need to convert map to list and display in listview.
"_InternalLinkedHashMap<String, dynamic>' is not a subtype of type 'List<dynamic>'"
Json Response
{"78":{"id":118,"first_name":"test","last_name":null,"email":"jignesh.test#gmail.com","phone":"","city":"null","state":"null","countrie_id":1,"location":"null","lat":"37.421998333333335","lng":"-122.08400000000002","image":"","role_id":3,"client_id":3,"coordinator_id":1,"sst_id":2,"created_at":"2018-10-08 10:59:18","updated_at":"2018-10-08 10:59:18","deleted_at":null,"status":0},
"79":{"id":119,"first_name":"Rahul test","last_name":null,"email":"rahul.test#gmail.com","phone":"","city":"null","state":"null","countrie_id":1,"location":"null","lat":"19.2284","lng":"72.85813","image":"","role_id":3,"client_id":3,"coordinator_id":1,"sst_id":2,"created_at":"2018-10-08 11:19:14","updated_at":"2018-10-08 11:19:14","deleted_at":null,"status":0},
"80":{"id":120,"first_name":"Customer Name","last_name":null,"email":"jjkkk#gmail.com","phone":"","city":"Mumbai","state":"Maharastra","countrie_id":1,"location":"virar","lat":"123","lng":"456","image":"images\/customer_image\/0hUSFUSqYAQTt57bVnnHjuQUOACECWzBOfJLWWa6.png","role_id":3,"client_id":1,"coordinator_id":1,"sst_id":2,"created_at":"2018-10-09 12:24:08","updated_at":"2018-10-09 14:03:07","deleted_at":null,"status":0},"status":"success","message":"List Fetched Successfully."}
Below is my Future method for calling post api method.
Future<PosModelData> posList(){
print('pos list api called');
return networkUtil.post("http://192.168.0.46/api/v1/poslist",body:{
"sstId":"2"
}).then((response){
if(response["status"]=="success"){
print("List fetched");
posLists=((response) as List).map((data)=>new PosModelData.fromJson(data)).toList();
// print(response.toString());
// print(posLists);
}
});
}
PosModel.dart
class PosModelData {
final String first_name;
final String last_name;
final String email;
PosModelData({this.first_name, this.last_name, this.email});
factory PosModelData.fromJson(Map json) {
return new PosModelData(
first_name: json['first_name'],
last_name: json['last_name'],
email: json['email'],
);
}
}
NetworkUtil.dart
Future<dynamic> post(String url, {Map header, body, encoding}) {
return http
.post(url, body: body, headers: header, encoding: encoding)
.then((http.Response response) {
final String resBody = response.body;
return jsonDecoder.convert(resBody);
});
}
I'm afraid your json string/response is not properly created.
In the same Map you have the status and then each element of your List so it's not possible to extract the List. You can reformat the server response and then get as shown in this code:
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
String responseStr = '''
{
"status":"success",
"message":"List Fetched Successfully.",
"posts":
[
{"id":118,"first_name":"test","last_name":null,"email":"jignesh.test#gmail.com","phone":"","city":"null","state":"null","countrie_id":1,"location":"null","lat":"37.421998333333335","lng":"-122.08400000000002","image":"","role_id":3,"client_id":3,"coordinator_id":1,"sst_id":2,"created_at":"2018-10-08 10:59:18","updated_at":"2018-10-08 10:59:18","deleted_at":null,"status":0},
{"id":119,"first_name":"Rahul test","last_name":null,"email":"rahul.test#gmail.com","phone":"","city":"null","state":"null","countrie_id":1,"location":"null","lat":"19.2284","lng":"72.85813","image":"","role_id":3,"client_id":3,"coordinator_id":1,"sst_id":2,"created_at":"2018-10-08 11:19:14","updated_at":"2018-10-08 11:19:14","deleted_at":null,"status":0},
{"id":120,"first_name":"Customer Name","last_name":null,"email":"jjkkk#gmail.com","phone":"","city":"Mumbai","state":"Maharastra","countrie_id":1,"location":"virar","lat":"123","lng":"456","image":"images\/customer_image\/0hUSFUSqYAQTt57bVnnHjuQUOACECWzBOfJLWWa6.png","role_id":3,"client_id":1,"coordinator_id":1,"sst_id":2,"created_at":"2018-10-09 12:24:08","updated_at":"2018-10-09 14:03:07","deleted_at":null,"status":0}
]
}
''';
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
home: new MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
List<PosModelData> postList;
#override
void initState() {
super.initState();
getPosts();
}
Future<Null> getPosts() async {
// Your http logic
Map<String, dynamic> response = json.decode(responseStr);
if (response["status"] == "success") {
postList = ((response["posts"]) as List)
.map((data) => new PosModelData.fromJson(data))
.toList();
setState(() {});
}
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(),
body: Center(
child: postList != null ?
Column(
children: <Widget>[
new Text("${postList[0].first_name} ${postList[0].email}"),
new Text("${postList[1].first_name} ${postList[1].email}"),
new Text("${postList[2].first_name} ${postList[2].email}"),
],
) : CircularProgressIndicator(),
),
);
}
}
class PosModelData {
final String first_name;
final String last_name;
final String email;
PosModelData({this.first_name, this.last_name, this.email});
factory PosModelData.fromJson(Map json) {
return new PosModelData(
first_name: json['first_name'],
last_name: json['last_name'],
email: json['email'],
);
}
}
You assume that your JSON response is list of objects, while in fact it is a single object with keys that maps to PosModelData.
You should either change your server response to something like:
[
{
"id":118,
"first_name":"test",
"last_name":null,
...
},
{
"id":119,
"first_name":"Rahul test",
"last_name":null,
...
}
]
or change the way you parse JSON response.