Skip to content

Latest commit

 

History

History
275 lines (207 loc) · 9.47 KB

File metadata and controls

275 lines (207 loc) · 9.47 KB

Testing

For more documentation on testing, read the references listed at the bottom.

Unit Testing

  • We use flutter_test to create tests. You can create a test like this :

    // Import the test package
    import 'package:test/test.dart';
    
    void main() {
      test('X should do something', () {
        ...
      });
    }
  • To assert the result of a test, use expect()

    /* 
    void expect(
      // actual value to be verified
    	dynamic actual,
      // characterises the expected result
    	dynamic matcher, {
    	// added in case of failure
    	String? reason,
    	// true or a String with the reason to skip
    	dynamic skip,
    	}
    )
    */
    
    expect(actual, value);
    expect(actual, isNull);
    expect(actual, isNotNull);
    
    ... and many more 

    Read more : Assertions in Dart and Flutter tests: an (almost) ultimate cheat sheet

  • We can use Mockito to mock classes.

    import 'package:mockito/annotations.dart';
    import 'package:mockito/mockito.dart';
    
    // Annotation which generates the cat.mocks.dart library and the MockCat class.
    @GenerateNiceMocks([MockSpec<Cat>()])
    import 'cat.mocks.dart';
    
    // Real class
    class Cat {
    String sound() => "Meow";
    bool eatFood(String food, {bool? hungry}) => true;
    Future<void> chew() async => print("Chewing...");
    int walk(List<String> places) => 7;
    void sleep() {}
    void hunt(String place, String prey) {}
    int lives = 9;
    }
    
    void main() {
    // Create mock object.
    var cat = MockCat();
    }
  • In some cases it's easier to mock with the use of the decorator pattern. First we decorate our repository like this :

     class DadJokesRepositoryDecorator implements  DadJokesRepository{
     	final DadJokesRepository _innerRepository;
     	final MockingRepository _mockingRepository;
    
     	DadJokesRepositoryDecorator(this._innerRepository, this._mockingRepository);
    
     	@override
     	Future<DadJokeResponseData> getDadJokes() async {
     		var isMockingEnabled = await _mockingRepository.checkMockingEnabled();
    
     		if (isMockingEnabled) {
     			return mockedDadJokeResponse;
     		} else {
     			return _innerRepository.getDadJokes();
     		}
     	}
     }

    Then, enable mock by calling this method :

     	await GetIt.I.get<MockingRepository>().setMocking(true);

    This gives us the same behavior as with mockito without having to run the build_runner command for testing with the added benefit of having data mocking for debugging.

  • We use http-mock-adapter to mock request-response communication with Dio.

    void main() {
    	test('get example', () async {
    		// Arrange
    		final dio = Dio(BaseOptions());
    	  final dioAdapter = DioAdapter(dio: dio);
    
    	  const path = 'https://example.com';
    
    	  dioAdapter.onGet(
    	    path,
    	    (server) => server.reply(
    	      200,
    	      {'message': 'Success!'},
    	      // Reply would wait for one-sec before returning data.
    	      delay: const Duration(seconds: 1),
    	    ),
    	  );
    
    		// Act
    	  final response = await dio.get(path);
    
    		// Assert
    		expect(response.data, {'message': 'Success!'});
    	});	
    }

Functional/Integration Testing

  • We use integration_test to create functional/integration tests. You can create a test like this :

     // Import the test package
     import 'package:flutter_test/flutter_test.dart';
     import 'package:integration_test/integration_test.dart';
    
     void dadJokesTest() {
     	testWidgets('Get Dad Jokes', (WidgetTester tester) async {
     		// Arrange
    
     		// Act
     		await tester.pumpWidget(const App());
    
     		// Without this the dadjokesContainer isn't there yet.
     		await tester.pumpAndSettle();
    
     		// Assert
     		var dadjokesContainer = find.byKey(const Key('DadJokesContainer'));
    
     		var dadJokes = find.descendant(
     			of: dadjokesContainer,
     			matching: find.byType(DadJokeListItem),
     		);
    
     		expect(dadJokes, findsAtLeast(1));
     	});
     }

    After that, you can add it to the function that runs the test. They are all run in the same main because of this issue.

    To test different behaviors and interactions between the components of the app you need to simulate user interactions with the tester.tap(target) method like this:

    await tester.tap(dadJokes.first);
  • To assert the result of a test, use expect() exactly like with unit tests.

  • tester.pumpAndSettle() is used both to trigger a frame change and to wait for the last pump to have settled before moving on. For example, we use it after pumping the app widget and we also use it when we navigated and we want to update the UI.

Mocking

For functional testing we use the decorator pattern because we are testing the actual behavior of the app so we don't want to use mockito to mock the logic of the classes of the app. To use the decorator pattern you simply call the mocking repository and the method called setMocking(true). It's important to set the mocking in the main function before the tests otherwise the mocking doesn't take effect. In most cases, except for api tests, the data sources (repositories) should be mocked for integration testing.

Running the tests

  • You can run the integration test with this command :
flutter test integration_test --dart-define ENV=Development

We need to set the env because the environment manager doesn't set one by default when we load the app it is set for each build accordingly so we need to manually set it.

  • When testing locally, the device on which you are testing must be open and running, as the tests will interact with the app in real-time. Otherwise it will throw unclear exceptions that don't point you at this issue.

Code coverage

You can collect the code coverage locally using the following command lines.

Installing lcov/genhtml

In order to visualize the test coverage you need to install lcov and by extension genhtml.

On windows you need chocolatey and run this command.

chocolatey install lcov

On macOS you need to have lcov installed on your system (brew install lcov) to use the genhtml command.

Then you need to add the genhtml path to your environment variables. Go to the properties of your PC then go to advanced system settings -> Environment variables, then in system variables find the path variable and edit it. Add this path C:\ProgramData\chocolatey\lib\lcov\tools\bin and now open a git bash terminal against your src/app folder and enter these commands.

# Generate `coverage/lcov.info` file
flutter test --coverage
# Generate HTML report
genhtml coverage/lcov.info -o coverage/html

It should have created an html folder in your coverage folder where you can open the index.html file and see a visualization of your test coverage.

The report created can then be used to drill into different files and see what lines are covered or not (blue means covered, red means uncovered).

coverage_visualization_example

There’s also a couple of VSCode extensions you can use to visualize your coverage if you want.

Flutter Coverage (Not working at the moment) will add a new section to your Testing tab.

flutter_coverage_demo

Coverage Gutters will show you which lines in your code isn’t covered by a test.

coverage_gutters_demo

For both of these extensions to work, you must generate a lcov.info file before hand, or whenever you make a change and want to see your coverage.

Naming

In general, test files should reside inside a test folder located at the root of your Flutter application or package. Test files should always end with _test.dart, this is the convention used by the test runner when searching for tests.

Your file structure should look like this:

FlutterApplicationTemplate/
	src/app/
		lib/
			business/
				dad_jokes_service.dart
		test/
			business/
				dad_jokes_service_test.dart

As for the tests themselves their names should indicate either the expected result with or without a condition or the action performed.

For example :

test('Get all favorite jokes', () async {
  var result = await SUT.getFavoriteDadJokes();
  var mockedJokesList = getFavoriteDadJokesList()
	  .map(
		(favoriteDadJoke) => DadJoke.fromData(
		  favoriteDadJoke,
		  isFavorite: true,
		),
	  )
	  .toList();

  expect(result, mockedJokesList);
});

References