Baseflow / flutter_cached_network_image

Download, cache and show images in a flutter app
https://baseflow.com
2.44k stars 656 forks source link

How to widget test Cached network manager #307

Open flutteradv opened 4 years ago

flutteradv commented 4 years ago

Is there any tests code sample on Cached network manager? how we should write widget tests for it

aBuder commented 4 years ago

I always have the same problem

wheel1992 commented 4 years ago

Hi, does anyone successfully test integration test with CachedNetworkImage?

jogboms commented 4 years ago

@renefloor any pointers on this one? Or do we just mock the dependencies.

renefloor commented 4 years ago

That's indeed something that should be improved I think. With smaller unit tests you can mock the CacheManager in the image widget.

DevNico commented 4 years ago

Any updates on this? What's the way to go here?

ncuillery commented 4 years ago

This blog post might help: https://medium.com/flutter-community/golden-testing-using-cachednetworkimage-1b488c653af3

woprandi commented 2 years ago

I have a issue to test when I use CircularProgressIndicator as progress widget like this :

 CachedNetworkImage(
    imageUrl: '$baseUrl/pictures/${image}',
    cacheManager: context.watch<CacheManager>(),
    progressIndicatorBuilder: (context, url, downloadProgress)
      return Center(
        child: CircularProgressIndicator(
          value: downloadProgress.progress,
        ),
      );
    },
  ),

I didn't check code yet but the behavior looks like the animation never complete since WidgetTester.pumpAndSettle() will always timeout. Replacing with a SizedBox when downloadProgress.progress is null fixe the issue

cedvdb commented 2 years ago

it would be nice to

That's indeed something that should be improved I think. With smaller unit tests you can mock the CacheManager in the image widget.

This is not always an option or even recommended.

It would be nice if the cache manager could be set project wide instead on having to pass it directly on the widget.

fzyzcjy commented 2 years ago

I see the following error:

══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════
The following MissingPluginException was thrown running a test:
MissingPluginException(No implementation found for method getDatabasesPath on channel
com.tekartik.sqflite)

When the exception was thrown, this was the stack:
#0      MethodChannel._invokeMethod (package:flutter/src/services/platform_channel.dart:165:7)
<asynchronous suspension>
<asynchronous suspension>
(elided one frame from package:stack_trace)

The test description was:
  simplest
════════════════════════════════════════════════════════════════════════════════════════════════════

anyone has the same problem?

diegogarciar commented 2 years ago

Just tried with updated code from bloc post

class MockBaseCacheManager extends Mock implements BaseCacheManager {
  static const fileSystem = LocalFileSystem();
  @override
  Stream<FileResponse> getFileStream(
    String url, {
    String? key,
    Map<String, String>? headers,
    bool? withProgress,
  }) async* {
    final fileName = './test/assets/${File(url).uri.pathSegments.last}';

    final file = fileSystem.file(fileName);

    yield FileInfo(
      file, // Path to the asset
      FileSource.Cache, // Simulate a cache hit
      DateTime(2050), // Very long validity
      url, // Source url
    );
  }
}

my issue is that on _image_loader.dart line 58, file.readAsBytes() never completes. I did some research, it seems that widget tests run on some isolate and that's the reason why it fails. Any ideas?

susatthi commented 2 years ago

It worked at v3.2.1!

// ignore_for_file: depend_on_referenced_packages

import 'package:cached_network_image/cached_network_image.dart';
import 'package:file/local.dart';
import 'package:flutter/material.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';

void main() {
  testWidgets('The image should be displayed', (tester) async {
    await tester.runAsync(() async {
      ImageProvider? receiveImageProvider;

      await tester.pumpWidget(
        CachedNetworkImage(
          imageUrl: 'dummy url',
          cacheManager: MockCacheManager(),
          imageBuilder: (context, imageProvider) {
            receiveImageProvider = imageProvider;
            return Image(
              image: imageProvider,
            );
          },
        ),
      );

      expect(receiveImageProvider, isNull);

      // Wait for the image to be loaded.
      await Future<void>.delayed(const Duration(milliseconds: 1000));
      await tester.pump();

      expect(receiveImageProvider, isNotNull);
    });
  });
}

class MockCacheManager extends Mock implements DefaultCacheManager {
  static const fileSystem = LocalFileSystem();

  @override
  Stream<FileResponse> getImageFile(
    String url, {
    String? key,
    Map<String, String>? headers,
    bool withProgress = false,
    int? maxHeight,
    int? maxWidth,
  }) async* {
    yield FileInfo(
      fileSystem
          .file('./test/assets/13707135.png'), // Return your image file path
      FileSource.Cache,
      DateTime(2050),
      url,
    );
  }
}
smith897 commented 2 years ago

The best and simplest solution I've found is to create a wrapper widget for CachedNetworkImage that uses a regular network image if it detects it's a testing environment. It'd be nice to not need a workaround like that.

yasht01 commented 1 year ago

Are there any updates to this issue?

fzyzcjy commented 1 year ago

I personally workaround this by using JsonCacheInfoRepository instead of the default repository.

Maxim-Filimonov commented 1 year ago

The best and simplest solution I've found is to create a wrapper widget for CachedNetworkImage that uses a regular network image if it detects it's a testing environment. It'd be nice to not need a workaround like that.

Offtopic: But how do you detect test environment?

jogboms commented 1 year ago

Offtopic: But how do you detect test environment?

You could use this Platform.environment.containsKey('FLUTTER_TEST') @Maxim-Filimonov

fzyzcjy commented 1 year ago

Offtopic: But how do you detect test environment?

Or check the type of binding

Maxim-Filimonov commented 1 year ago

On the note of testing Cached Network Manager. I spent about 2 hours debugging one of the tests using MockCacheManager yesterday.

I had two different cache managers to test both loading and error case:

class MockCacheManager extends Mock implements BaseCacheManager {
  static const fileSystem = LocalFileSystem();
  @override
  Stream<FileResponse> getFileStream(
    String url, {
    String? key,
    Map<String, String>? headers,
    bool? withProgress,
  }) async* {
    final fileName = ImageConstant.img1splashscreen;

    final file = fileSystem.file(fileName);

    yield FileInfo(
      file, // Path to the asset
      FileSource.Cache, // Simulate a cache hit
      DateTime(2050), // Very long validity
      url, // Source url
    );
  }
}
class InvalidCacheManager extends Mock implements BaseCacheManager {
  @override
  Stream<FileResponse> getFileStream(
    String url, {
    String? key,
    Map<String, String>? headers,
    bool? withProgress,
  }) async* {
    throw Exception('Cannot download file');
  }
}

And tests:

testWidgets('can render network image', (tester) async {
    // Create a mock cache manager instance
    final cacheManager = MockCacheManager();

    const url = 'https://local_file_system.com/200.jpeg';
    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          body: CachedNetworkImage(
            imageUrl: url,
            cacheManager: cacheManager,
          ),
        ),
      ),
    );
});
testWidgets('renders error when cannot load network image', (tester) async {
    // Create a mock cache manager instance
    final cacheManager = InvalidCacheManager();

    const url = 'https://local_file_system.com/200.jpeg';
    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          body: CachedNetworkImage(
            imageUrl: url,
            cacheManager: cacheManager,
          ),
        ),
      ),
    );
});

The culprit was the url. If it's the same exact url cache_manager is not even called by flutter itself so you get EXACTLY the same output in both of these scenarios as flutter image caching is sharing amongst all tests. Something to keep in mind when testing those that url ALWAYS need to be different.

resultanyildizi commented 1 month ago

I've also achieved golden testing in version v3.4.1 .

late BaseCacheManager cacheManager;

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  cacheManager = DefaultCacheManager();
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'MyApp',
      home: HomePage(),
    );
  }
}

class HomePage extends StatelessWidget {
  const HomePage({super.key});

  static const image = 'https://picsum.photos/200/300';

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Padding(
        padding: const EdgeInsets.all(8),
        child: GridView.builder(
          gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
            crossAxisCount: 3,
            mainAxisSpacing: 8,
            crossAxisSpacing: 8,
          ),
          itemCount: 40,
          itemBuilder: (context, index) {
            return ClipRRect(
              borderRadius: BorderRadius.circular(12),
              child: CachedNetworkImage(
                imageUrl: image,
                cacheManager: cacheManager,
                fit: BoxFit.cover,
              ),
            );
          },
        ),
      ),
    );
  }
}
@visibleForTesting
class MockCacheManager extends Mock implements BaseCacheManager {
  @override
  Stream<FileResponse> getFileStream(
    String url, {
    String? key,
    Map<String, String>? headers,
    bool? withProgress,
  }) async* {
    File file = const LocalFileSystem().file('test/test.jpg');
    yield FileInfo(
      file,
      FileSource.Online, // Simulate a cache hit
      DateTime(2050), // Very long validity
      url, // Source url
    );
  }
}
import 'package:cached_network_image_golden_test/main.dart';
import 'package:file/local.dart';
import 'package:flutter/material.dart';
import 'package:flutter_cache_manager/file.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:golden_toolkit/golden_toolkit.dart';
import 'package:mockito/mockito.dart';

void main() {
  // You can use a service locator like GetIt instead.
  cacheManager = MockCacheManager();

  testWidgets(
    'should match with the golden file (basic)',
    (WidgetTester tester) async {
      await tester.runAsync(() async {
        const widget = MaterialApp(
          home: HomePage(),
          debugShowCheckedModeBanner: false,
        );
        await tester.pumpWidgetBuilder(widget);
        final elements = tester.elementList(find.byType(Image));
        for (var element in elements) {
          final image = element.widget as Image;
          await precacheImage(image.image, element);
        }
        await tester.pumpAndSettle();
      });

      await expectLater(
        find.byType(HomePage),
        matchesGoldenFile('goldens/home_page.png'),
      );
    },
  );

  testGoldens(
    'should match with the golden file (golden_toolkit)',
    (WidgetTester tester) async {
      await tester.runAsync(() async {
        const widget = MaterialApp(
          home: HomePage(),
          debugShowCheckedModeBanner: false,
        );
        await tester.pumpWidgetBuilder(widget);
        final elements = tester.elementList(find.byType(Image));
        for (var element in elements) {
          final image = element.widget as Image;
          await precacheImage(image.image, element);
        }
        await tester.pumpAndSettle();
      });

      await screenMatchesGolden(tester, 'home_page');
    },
  );
}