Vitor
Vitor

Reputation: 336

FutureBuilder Snapshot Data is null with Mockito - Flutter

I am building some tests for my flutter application and I have figured out that it's getting on future builder before the method that calls the API return the data, so the snapshot always will be empty.

How can I force the FutureBuilder wait to the method return the data?

The test code:

@GenerateMocks([http.Client])
void main(){

  group('Landing Screen', (){
    testWidgets('When get to server endpoint, then LandingScreen should shows ProductBox', (WidgetTester tester) async {

      await tester.runAsync(() async {
        final client = MockClient();

        final product =  Product(id: "a1838sn18f", name: "Sabão em pó", description:"Tests",supplierItemCode: "35", unityMeasurement: "kg", unityMeasurementQuantity: 5, image:"https://a-static.mlcdn.com.br/1500x1500/sabao-em-pedra-ype-5-unidades-ype/costaatacado/90131/fd9f6b24567bc11702c189709a9f8dd3.jpg" );
        final supplier = Supplier(fantasyName: "aisdiaj", companyName: "TEste s1", CNPJ: "5575554");
        final findProductBox = find.byWidget(ProductBox(productBoxModel: ProductBoxModel(product: product , supplier: supplier),));


        when(client
            .get(Uri.http(DOMAIN, LAST_SEARCH_URL, queryParameters)))
            .thenAnswer((_) async =>
            http.Response('[{"product":{"id":"a1838sn18f","createdByUserId":"fake","createdDate":null,"lastModifiedByUserId":null,"lastModifiedDate":null,"name":"Sabão em pó","description":"Tests","supplierItemCode":"35","unityMeasurement":"kg","unityMeasurementQuantity":5,"image":"https://a-static.mlcdn.com.br/1500x1500/sabao-em-pedra-ype-5-unidades-ype/costaatacado/90131/fd9f6b24567bc11702c189709a9f8dd3.jpg"}'
                ',"supplier":{"id":"asdle2","createdByUserId":null,"createdDate":null,"lastModifiedByUserId":null,"lastModifiedDate":null,"fantasyName":"aisdiaj","companyName":"TEste s1","cnpj":"5575554"}}]'
                , 200));

        when(client
            .get(Uri.http(DOMAIN, MOST_SEARCH_URL, queryParameters)))
            .thenAnswer((_) async =>
            http.Response('[{"product":{"id":"a1838sn18f","createdByUserId":"fake","createdDate":null,"lastModifiedByUserId":null,"lastModifiedDate":null,"name":"Sabão em pó","description":"Tests","supplierItemCode":"35","unityMeasurement":"kg","unityMeasurementQuantity":5,"image":"https://a-static.mlcdn.com.br/1500x1500/sabao-em-pedra-ype-5-unidades-ype/costaatacado/90131/fd9f6b24567bc11702c189709a9f8dd3.jpg"}'
                ',"supplier":{"id":"asdle2","createdByUserId":null,"createdDate":null,"lastModifiedByUserId":null,"lastModifiedDate":null,"fantasyName":"aisdiaj","companyName":"TEste s1","cnpj":"5575554"}}]'
                , 200));

        await tester.pumpWidget(MaterialApp(home: LandingScreen(client),));

        expect(await findProductBox, findsWidgets);

      });

    });
  });

}

This is the FutureBuilder:

class LandingScreen extends StatelessWidget{
  http.Client client;
  
  LandingScreen(this.client);
  var lastSearch = <ProductBoxModel> [];
  var mostSearch = <ProductBoxModel> [];
  Color color = Colors.grey.withOpacity(0.5);

  @override
  Widget build(BuildContext context) {
    LandingScreenRoutes landingScreenRoutes = LandingScreenRoutes(client);
    var lastSearchFuture =  landingScreenRoutes.getLastSearch('5'); //TODO this idUser should come from logged user
    var mostSearchFuture = landingScreenRoutes.getMostSearch('5'); //TODO this idUser should come from logged user


****HIDED CONTENT****
Expanded(

                    child: FutureBuilder(
                      future: lastSearchFuture,
                      builder: (context, snapshot){
                        if(snapshot.hasData){
                          lastSearchFuture.asStream().forEach((element) { lastSearch.addAll(element);});
                          return Padding(
                            padding: sidePadding,
                            child: Container(
                                    width: size.width,
                                    height: size.height,
                                    child: ListView.builder(
                                      itemCount: lastSearch.length ,
                                      itemBuilder: (context, index){
                                      return ProductBox(
                                       productBoxModel: lastSearch[index],
                                      );
                                    },
                                    ),
                                )
                            );
                      }else
                          return Container(
                            alignment: Alignment.center,
                            width: size.width,
                            height: size.height,
                            child: CircularProgressIndicator(),
                          );
    }               )
                  ),


Here is the LandingScreenRoutes class:

const DOMAIN = "10.0.2.2:9090";
const LAST_SEARCH_URL = "/news/lastSearch";
const MOST_SEARCH_URL = "/news/mostSearch";

class LandingScreenRoutes{
  http.Client client;

  LandingScreenRoutes(this.client);

  Future<List<ProductBoxModel>> getLastSearch(String userId) async {
    final queryParameters = {'userId':userId};
    var response = await client.get(Uri.http(DOMAIN, LAST_SEARCH_URL, queryParameters));
    List<ProductBoxModel> productList = [];
    for(Map<String, dynamic> p in jsonDecode(response.body)){
      productList.add(ProductBoxModel.fromJson(p));
    }
    return productList;
  }

  Future<List<ProductBoxModel>> getMostSearch(String userId) async {
    final queryParameters = {'userId':userId};
    var response = await client.get(Uri.http(DOMAIN, MOST_SEARCH_URL, queryParameters));
    List<ProductBoxModel> productList = [];
    for(Map<String, dynamic> p in jsonDecode(response.body)){
      productList.add(ProductBoxModel.fromJson(p));
    }
    return productList;
  }
}

The application first checks if the snapshot has data:

enter image description here

And after it goes to the methods that fetch data from backend:

enter image description here enter image description here

This should not happen. Because of this, the snapshot.hasdata ALWAYS will be false, and my test wont will pass.

How can I force the application wait until has data or other thing?

Upvotes: 0

Views: 548

Answers (1)

francisco gomes
francisco gomes

Reputation: 381

Have you tried the expectLater?

Before:

expect(await findProductBox, findsWidgets);

After:

expectLater(await findProductBox, findsWidgets);

Edit: I replicated the problem here, try:

    @GenerateMocks([http.Client])
void main() {
  group('Landing Screen', (){
    testWidgets('When get to server endpoint, then LandingScreen should shows ProductBox',

      (WidgetTester tester) async {
        final client = MockClient();
        when(client.get(any)).thenAnswer((_) async =>
          http.Response(
            '[{"product":{"id":"a1838sn18f","createdByUserId":"fake","createdDate":null,"lastModifiedByUserId":null,"lastModifiedDate":null,"name":"Sabão em pó","description":"Tests","supplierItemCode":"35","unityMeasurement":"kg","unityMeasurementQuantity":5,"image":"https://a-static.mlcdn.com.br/1500x1500/sabao-em-pedra-ype-5-unidades-ype/costaatacado/90131/fd9f6b24567bc11702c189709a9f8dd3.jpg"}'
              ',"supplier":{"id":"asdle2","createdByUserId":null,"createdDate":null,"lastModifiedByUserId":null,"lastModifiedDate":null,"fantasyName":"aisdiaj","companyName":"TEste s1","cnpj":"5575554"}}]'
              , 200
            )
        );

        await tester.pumpWidget(MaterialApp(home: LandingScreen(client: client),));

        await tester.pumpAndSettle();

        expectLater(find.byType(ProductBox, skipOffstage: false), findsWidgets);


      });

    });
}

In the screen:

  return Column(children: [
      Expanded(
          child: FutureBuilder<List<ProductBoxModel>>(
              future: LandingScreenRoutes(client).getLastSearch('5'),
              builder: (_, snapshot) {
                if (snapshot.hasData) {
                  return Padding(
                      padding: const EdgeInsets.all(16),
                      child: ListView.builder(
                        itemCount: snapshot.data!.length,
                        itemBuilder: (_, index) {
                          return ProductBox(
                            key: Key(snapshot.data![index].product.id),
                            productBoxModel: snapshot.data![index],
                          );
                        },
                      ));
                } else {
                  return Container(
                    alignment: Alignment.center,
                    child: const CircularProgressIndicator(),
                  );
                }
              }))
    ]);

You can use pumpAndSettle to wait until all asynchronous methods are resolved and the parameter skipOffstage: false has the function to check items that are outside the screen boundary, very useful for testing with lists, as some items may not be visible

Upvotes: 0

Related Questions