Elementary Testing, or Elementary testing

Elementary Testing, or Elementary testing / Habr

Hello everyone, Surf is in touch. Earlier, we looked at the process of creating a small application using the Elementary package and figured out how it works. Now let’s talk about testing.

In the article I will tell you what types of tests there are, and by example I will show you how to cover an application written using Elementary with tests.

The package is available at pub.dev. The source code can be viewed at GitHub.

Comparison of types of tests

There are three basic types of tests in the official documentation: Unit, Widget, Integration.

Unit test checks a single function, method, or class. The goal is to make sure that the function is performed correctly under various conditions.

Widget test checks one widget. The goal is to make sure that the widget’s user interface looks and behaves as expected. Widget testing involves several classes and requires a test environment: it provides the appropriate context.

Integration test checks the full application or most of it. The goal is to make sure that all widgets and services work together as expected. In addition, integration tests are used to check the performance of the application.

Surf uses another type of tests — Golden. It is not mentioned in the official documentation, but thanks to the package golden_toolkit golden tests in Flutter have become possible.

Golden test checks individual widgets and the whole screen. The visual representation of the component is compared with previous test results.

The final table of comparison of types of testing

Testing the UI

We will cover the finished application with tests, which was written as part of the article “Elementary: a new look at the architecture of Flutter applications”. We will check the visual representation using golden tests. They are easy to learn and maintain – if you follow the dependencies correctly.

If you are using IDEA, add a test run configuration to generate golden.

Also, the generation of golden tests can be performed via the CLI using the command.

flutter test -- update-goldens

Before you start working with golden tests, you need to add the configuration: put the flutter_test_config.dart test in the folder.

The code below is executed at each test and loads fonts.

Future<void> testExecutable(FutureOr<void> Function() testMain) async {
 await loadAppFonts();
 return testMain();
}

When the golden generation command is executed, we will get a set of images: they will be located in the directory with the test file.

Due to the mock interface, you can set various scenarios and get visual representations.

The magic of simplicity lies in the fact that even at the stage of writing WM, we create an interface that defines a set of parameters. Next, just copy the desired values and run them through the test.

City Selection Screen Test

void main() {
  const selectAddressScreen = SelectAddressScreen();
  final selectAddressWm = SelectAddressWMMock();

  setUp(() {
    when(() => selectAddressWm.predictions).thenAnswer(
      (_) => ValueNotifier<List<Location>>([]),
    );
    when(() => selectAddressWm.searchFieldController).thenAnswer(
      (_) => TextEditingController(),
    );
  });

  testGoldens('select address screen default golden test', (tester) async {
    await tester.pumpWidgetBuilder(selectAddressScreen.build(selectAddressWm));
    await multiScreenGolden(tester, 'select_address_screen');
  });

  testGoldens('select address screen with data golden test', (tester) async {
    when(() => selectAddressWm.predictions).thenAnswer(
      (_) => ValueNotifier<List<Location>>(_locationMock),
    );

    await tester.pumpWidgetBuilder(selectAddressScreen.build(selectAddressWm));
    await multiScreenGolden(tester, 'select_address_screen_data');
  });
}

Weather Forecast screen test

void main() {
  final wm = WeatherScreenWMMock();
  const weatherScreen = WeatherScreen();

  setUp(() {
    when(() => wm.topPadding).thenReturn(16);

    when(() => wm.currentWeather).thenReturn(
      EntityStateNotifier.value(_mockWeathers),
    );

    when(() => wm.locationTitle).thenReturn(_locationMock.title);
  });

  testGoldens('weather details screen with data golden test', (tester) async {
    await tester.pumpWidgetBuilder(weatherScreen.build(wm));
    await multiScreenGolden(tester, 'weather_details_screen_data');
  });

  testGoldens('weather details screen with error golden test', (tester) async {
    when(() => wm.currentWeather).thenReturn(
      EntityStateNotifier.value([])..error(Exception()),
    );

    await tester.pumpWidgetBuilder(weatherScreen.build(wm));
    await multiScreenGolden(tester, 'weather_details_screen_err');
  });
}

Testing the WidgetModel

To test the WidgetModel from the Elementary package, you need to connect the library elementary_test: it offers several convenient tools for testing.

testWidgetModel is a function of the test itself. When passing WidgetModel, it describes its behavior and checks the result. The function also uses the tester to manipulate the phases of the WidgetModel lifecycle, and the BuildContext to simulate them.

When writing a test, it is worth keeping in mind that the environment around the tested object can be completely locked.

Test widget models of the city selection screen

void main() {
  group('init select address screen wm', () {
    final getIt = GetIt.instance;

    setUp(() {
      getIt.registerSingleton<AppModel>(AppModel());
    });

    test('createSelectAddressWM', () {
      expect(() => createSelectAddressWM(BuildContextMock()), returnsNormally);
    });
  });

  group('select address screen wm testing', () {
    late SelectAddressModelMock modelData;
    late NavigationHelperMock navigatorStateMock;

    SelectAddressWM setupWm() {
      modelData = SelectAddressModelMock();
      navigatorStateMock = NavigationHelperMock();

      when(() => modelData.getCityPrediction(any()))
          .thenAnswer((invocation) => Future.value());

      registerFallbackValue(MaterialPageRoute<void>(builder: (_) {
        return const Center();
      }));

      return SelectAddressWM(modelData, navigatorStateMock);
    }

    testWidgetModel<SelectAddressWM, SelectAddressScreen>(
      'onTapLocation call onLocationSelected and navigate to next screen',
      setupWm,
      (wm, tester, context) async {
        tester.init();
        wm.onTapLocation(_locationMock);

        verify(() => modelData.onLocationSelected(_locationMock));
        verify(() => navigatorStateMock.push(context, any()));
      },
    );

    testWidgetModel<SelectAddressWM, SelectAddressScreen>(
      'onTextChanged call getCityPrediction',
      setupWm,
      (wm, tester, context) async {
        tester.init();

        wm.searchFieldController.text="Test";
        verify(() => modelData.getCityPrediction(any()));
      },
    );
  });
}

Test widget models of the weather forecast screen

void main() {
  group('WeatherScreenWm init', () {
    final getIt = GetIt.instance;

    setUp(() {
      getIt.registerSingleton<AppModel>(AppModel());
    });

    test('createWeatherScreenWM', () {
      expect(() => createWeatherScreenWM(BuildContextMock()), returnsNormally);
    });
  });

  group('WeatherScreenWM', () {
    final modelData = WeatherScreenModelMock();
    final contextHelperMock = ContextHelperMock();

    WeatherScreenWM setupWm() {
      when(modelData.getWeather).thenAnswer((invocation) => Future.value([]));

      return WeatherScreenWM(contextHelperMock, modelData);
    }

    testWidgetModel<WeatherScreenWM, WeatherScreen>(
      'getWeather called after init wm ',
      setupWm,
      (wm, tester, context) async {
        tester.init();
        verify(modelData.getWeather);
      },
    );

    testWidgetModel<WeatherScreenWM, WeatherScreen>(
      'topPadding getter return padding',
      setupWm,
      (wm, tester, context) async {
        tester.init();

        when(
          () => contextHelperMock.getMediaQuery(context),
        ).thenReturn(const MediaQueryData());

        expect(wm.topPadding, 16);
      },
    );

    testWidgetModel<WeatherScreenWM, WeatherScreen>(
      'onRetryPressed call getWeather',
      setupWm,
      (wm, tester, context) async {
        tester.init();
        wm.onRetryPressed();
        verify(modelData.getWeather);
      },
    );
  });
}

Testing the model

To test the model, we use unit tests. Before writing, it is advisable to familiarize yourself with a set of ready-made matchers: same, isTrue, isFalse, returnsNormally, and so on.

City Selection Screen Model Test

void main() {
  final addressServiceMock = AddressServiceMock();
  late SelectAddressModel model;

  setUp(() {
    model = SelectAddressModel(addressServiceMock, AppModel());
  });

  test('init with empty list', () async {
    when(() => addressServiceMock.getCityPredictions('')).thenAnswer(
      (_) => Future.value([]),
    );
    expect(model.predictions.value, isEmpty);
  });

  test('getCityPrediction return empty list', () async {
    when(() => addressServiceMock.getCityPredictions('Test')).thenAnswer(
      (_) => Future.value(_locationMock),
    );
    await model.getCityPrediction('');
    expect(model.predictions.value, isEmpty);
  });

  test('getCityPrediction return prediction list', () async {
    await model.getCityPrediction('Test');
    expect(model.predictions.value, same(_locationMock));
  });
}

Weather Forecast Screen Model test

void main() {
  late WeatherScreenModel wm;
  final weatherServiceMock = WeatherServiceMock();

  setUp(() {
    wm = WeatherScreenModel(weatherServiceMock, _locationMock);
  });

  test('location getter return selected location', () {
    expect(wm.location, same(_locationMock));
  });

  test('method getWeather return weather from weather service', () async {
    when(() => weatherServiceMock.getWeather(any())).thenAnswer(
      (invocation) => Future.value(_weatherMock),
    );

    expect(await wm.getWeather(), same(_weatherMock));
  });
}

Checking the coverage

Instructions for checking the test coverage:

  1. We collect information about test coverage.
  flutter test --coverage --update-goldens
  1. Generating an HTML report.
genhtml coverage/lcov.info -o coverage/html
  1. Open the generated report.
open coverage/html

We get detailed information about the state of the test coverage. You can also see places that have not yet been covered by tests. This is a very handy tool if you are actively writing tests.

We have seen in practice that an application written using the Elementary package can be easily tested with all kinds of tests. This became possible due to the separation of the application into layers and the lack of great connectivity between them.

Unity 3D Development Outsourcing | IT Outsource Support

Ready to see us in action:

More To Explore

IWanta.tech
Logo
Enable registration in settings - general
Have any project in mind?

Contact us:

small_c_popup.png