Chapters

Hide chapters

Flutter Apprentice

Fourth Edition · Flutter 3.16.9 · Dart 3.2.6 · Android Studio 2023.1.1

Section II: Everything’s a Widget

Section 2: 5 chapters
Show chapters Hide chapters

Section IV: Networking, Persistence & State

Section 4: 6 chapters
Show chapters Hide chapters

17. Introduction to Testing
Written by Alejandro Ulate Fallas

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

In this chapter, you’ll revisit work on the Recipe Finder app from previous chapters. While doing so, you’ll learn:

  • About the importance of testing your code.
  • The types of tests you can carry out in a Flutter project.
  • How to perform unit testing.
  • Good practices while testing.
  • Mocking dependencies when necessary.

Improving Code Quality With Tests

Ensuring the quality of your Flutter project is essential for its success, that’s where testing comes in. It’ll help you identify defects, errors or issues within your project and increase your confidence in your code.

With testing, you can ensure that your project functions as expected, meets the specified requirements and delivers a reliable and high-quality user experience. Here are a few reasons why you should consider adding tests in all your projects:

  1. Error Identification: Testing helps identify and locate software errors, defects or bugs. These errors can be simple syntax mistakes or more complex logic issues. Identifying and fixing these issues is important to prevent them from causing problems for your end-users.

  2. Risk Mitigation: It helps manage and reduce project risks by detecting issues early in the development process. That way, developers can address them quickly, minimizing the potential impact on project timelines, budgets and customer satisfaction.

  3. Requirement Verification: Testing also verifies that the software meets the specified requirements and aligns with the project’s goals. It ensures that the software does what it’s supposed to do and doesn’t introduce unexpected behavior.

  4. Continuous Improvement: Testing isn’t a one-time activity. It’s an ongoing process. It allows you to gather feedback, make improvements and release updates that enhance the software’s performance, reliability and security.

  5. Regression Prevention: As your app evolves and you add new features, there’s a risk of introducing new defects while fixing existing ones. Testing, especially regression testing, helps prevent these regressions by ensuring that changes don’t break existing functionality.

  6. Security and Compliance: Testing is essential for identifying security vulnerabilities and ensuring compliance with industry standards and regulations. It helps protect sensitive data, user privacy and the overall integrity of the software.

  7. Cost-Efficiency: Early detection and resolution of defects through testing are typically more cost-effective than addressing issues that arise after the software is in production. Testing reduces the expenses associated with fixing bugs in the later stages of development.

  8. Confidence and Trust: Thorough testing increases confidence in both the development team and end-users. It demonstrates a commitment to quality and reliability.

In summary, testing ensures that your Flutter project is high quality, meets user expectations and is free from defects.

Learning About Tests

There are three main kinds of tests: unit tests, widget tests and integration tests. Each one has a different utility and effort related to them.

test: ^1.24.3

flutter test test/data/models/ingredient_test.dart

void main() {
}

Adding Unit Tests

Now that you’ve created a new test file, it’s time to add some tests.

Testing the Ingredient Class

Add the following imports at the top of ingredient_test.dart to import the model and testing libraries:

import 'package:recipes/data/models/ingredient.dart';
import 'package:test/test.dart';
// 1.
group('Ingredient', () {
  // 2.
  test('can instantiate', () {
  });
});
// Arrange
late Ingredient ingredient;

// Act
ingredient = const Ingredient();

// Assert
expect(ingredient, isNotNull);

test('can set default properties', () {
  // Arrange
  late Ingredient ingredient;

  // Act
  ingredient = const Ingredient();

  // Assert
  expect(ingredient.id, isNull);
  expect(ingredient.recipeId, isNull);
  expect(ingredient.name, isNull);
  expect(ingredient.amount, isNull);
});
test('can receive parameters', () {
  // Arrange
  late Ingredient ingredient;
  const id = 123;
  const recipeId = 54321;
  const name = 'Parmesan Cheese';
  const amount = 1.0;

  // Act
  ingredient = const Ingredient(
    id: id,
    recipeId: recipeId,
    name: name,
    amount: amount,
  );

  // Assert
  expect(ingredient.id, equals(id));
  expect(ingredient.recipeId, equals(recipeId));
  expect(ingredient.name, equals(name));
  expect(ingredient.amount, equals(amount));
});
test('can instantiate from JSON', () {
  late Ingredient ingredient;
  // 1. 
  final jsonMap = <String, dynamic>{
    'id': 123,
    'recipeId': 54321,
    'name': 'Parmesan Cheese',
    'weight': 50.0,
    'amount': 1,
  };
  const id = 123;
  const recipeId = 54321;
  const name = 'Parmesan Cheese';
  const amount = 1.0;

  // 2.
  ingredient = Ingredient.fromJson(jsonMap);

  expect(ingredient.id, equals(id));
  expect(ingredient.recipeId, equals(recipeId));
  expect(ingredient.name, equals(name));
  expect(ingredient.amount, equals(amount));
});

Testing Recipe Class

Now it’s time to do the same for Recipe.

import 'package:recipes/data/models/models.dart';
import 'package:test/test.dart';

void main() {
  group('Recipe', () {
    test('can instantiate', () {
      // Arrange
      late Recipe recipe;

      // Act
      recipe = const Recipe();

      // Assert
      expect(recipe, isNotNull);
    });
  });
}

test('can receive parameters', () {
  late Recipe recipe;
  const id = 123;
  const label = 'Pasta with Garlic, Scallions, Cauliflower & Breadcrumbs';
  const image = 'https://spoonacular.com/recipeImages/716429-556x370.jpg';
  const description =
      'Pasta with Garlic, Scallions, Cauliflower & Breadcrumbs might be a good recipe to expand your main course repertoire. One portion of this dish contains approximately <b>19g of protein </b>,  <b>20g of fat </b>, and a total of  <b>584 calories </b>. For  <b>\$1.63 per serving </b>, this recipe  <b>covers 23% </b> of your daily requirements of vitamins and minerals. This recipe serves 2. It is brought to you by fullbellysisters.blogspot.com. 209 people were glad they tried this recipe. A mixture of scallions, salt and pepper, white wine, and a handful of other ingredients are all it takes to make this recipe so scrumptious. From preparation to the plate, this recipe takes approximately  <b>45 minutes </b>. All things considered, we decided this recipe  <b>deserves a spoonacular score of 83% </b>. This score is awesome. If you like this recipe, take a look at these similar recipes: <a href="https://spoonacular.com/recipes/cauliflower-gratin-with-garlic-breadcrumbs-318375">Cauliflower Gratin with Garlic Breadcrumbs</a>, < href="https://spoonacular.com/recipes/pasta-with-cauliflower-sausage-breadcrumbs-30437">Pasta With Cauliflower, Sausage, & Breadcrumbs</a>, and <a href="https://spoonacular.com/recipes/pasta-with-roasted-cauliflower-parsley-and-breadcrumbs-30738">Pasta With Roasted Cauliflower, Parsley, And Breadcrumbs</a>.';
  const bookmarked = true;
  // 1.
  const ingredients = [
    Ingredient(
      id: 1123,
      recipeId: 123,
      name: 'Pasta',
      amount: 1.0,
    ),
    Ingredient(
      id: 1124,
      recipeId: 123,
      name: 'Garlic',
      amount: 1.0,
    ),
    Ingredient(
      id: 1125,
      recipeId: 123,
      name: 'Breadcrumbs',
      amount: 5.0,
    ),
  ];

  // 2.
  recipe = const Recipe(
    id: id,
    label: label,
    image: image,
    description: description,
    bookmarked: bookmarked,
    ingredients: ingredients,
  );

  // Assert
  expect(recipe.id, equals(id));
  expect(recipe.label, equals(label));
  expect(recipe.image, equals(image));
  expect(recipe.description, equals(description));
  expect(recipe.bookmarked, equals(bookmarked));
  // 3.
  expect(recipe.ingredients, equals(ingredients));
});

Understanding Mocks

If you’ve ever done testing before, you might be familiar with the term mocking. But if you aren’t, you’ll understand the basics after this chapter.

mockito: ^5.4.2
import 'package:recipes/data/repositories/db_repository.dart';
import 'package:test/test.dart';

void main() {
  group('DBRepository', () {
    test('can instantiate', () {
      // Arrange
      late DBRepository dbRepository;

      // Act
      dbRepository = DBRepository();

      // Assert
      expect(dbRepository, isNotNull);
      expect(dbRepository.recipeDatabase, isNotNull);
    });
  });
}

Making Your Code Testable

DBRepository has a hidden dependency that is not exposed in the constructor of the class. This makes it crash when you try to access recipeDatabase, and it’s not initialized. To top it all, this property is key for other class variables and functions to work as expected.

DBRepository({RecipeDatabase? recipeDatabase})
    : recipeDatabase = recipeDatabase ?? RecipeDatabase();
@override
Future init() async {
  _recipeDao = recipeDatabase.recipeDao;
  _ingredientDao = recipeDatabase.ingredientDao;
}

Mocking With Mockito

You’ve already read that it’s important to isolate the code under test from external dependencies when creating unit tests. In this case, having a fully functional RecipeDatabase might not be what you want in the tests for DBRepository. So, it’s time to take out your magic wand and use some mocking spells.

import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:recipes/data/database/recipe_db.dart';
import 'package:recipes/data/models/ingredient.dart';
@GenerateNiceMocks([
  MockSpec<RecipeDatabase>(),
  MockSpec<RecipeDao>(),
  MockSpec<IngredientDao>(),
])
import 'db_repository_test.mocks.dart';
// 1.
final mockDb = MockRecipeDatabase();
final mockIngredientDao = MockIngredientDao();
final mockRecipeDao = MockRecipeDao();

// 2.
final randomIngredients = [
  const Ingredient(
    id: 1123,
    recipeId: 123,
    name: 'Pasta',
    amount: 1.0,
  ),
  const Ingredient(
    id: 1124,
    recipeId: 123,
    name: 'Garlic',
    amount: 1.0,
  ),
  const Ingredient(
    id: 1125,
    recipeId: 123,
    name: 'Breadcrumbs',
    amount: 5.0,
  ),
];

// 3.
when(mockDb.ingredientDao).thenReturn(mockIngredientDao);
when(mockDb.recipeDao).thenReturn(mockRecipeDao);
dbRepository = DBRepository(
  recipeDatabase: mockDb,
);

test('can findAllIngredients', () async {
  // TODO: Arrange
  // TODO: Act
  // TODO: Assert
});
// 1.
final dbRepository = DBRepository(
  recipeDatabase: mockDb,
);
await dbRepository.init();
// 2.
when(mockIngredientDao.findAllIngredients()).thenAnswer(
  (_) async => randomIngredients
      .map((e) => DbIngredientData(
            id: e.id!,
            recipeId: e.recipeId!,
            name: e.name!,
            amount: e.amount!,
          ))
      .toList(),
);
final result = await dbRepository.findAllIngredients();
// 3.
verify(mockIngredientDao.findAllIngredients()).called(1);
// 4.
expect(result, equals(randomIngredients));

Key Points

  • Testing ensures that your Flutter project is of high quality, meets user expectations and is free from defects.
  • Testing your code improves confidence when releasing a new version of your app.
  • There are multiple types of tests that vary according to different requirements.
  • Unit testing is great for building robust and maintainable Flutter apps.
  • You can bundle tests together with group().
  • To run unit tests, you’ll need to use test().
  • The complexity of a class matters when you think about testing them.
  • Consider mocking when dealing with external dependencies.

Where to Go From Here?

Unit testing is great for building robust and maintainable Flutter apps. In this chapter, you learned the essentials of unit testing a Flutter project, mocking dependencies with mockito, organizing and running tests, handling asynchronous testing and best practices.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now