Generating Fake Data in Flutter using the Factory Pattern for Unit Testing

Introduction

In this article we will learn how we can seamlessly generate fake data in Flutter that we can use with-in our test suites, or just for some placeholder data in our app when the backend is not completely setup yet or any other scenario that we just need a fake data in general. You will also learn how to create a Flutter package and use that package with-in your application, it can be used both in lib and test folder. And lastly you will learn how to setup a basic monorepo for your future projects, however I am not going to tackle about monorepos in this post on what are its pros and cons.

However, if you don't really need the monorepo setup, just skip over to the actual implementation of the Factory Pattern.

Packages

We will only require the following packages for us to seamlessly generate fake data.

Setup

To follow along, we'll start to setup a monorepo, first create your terminal and navigate into that directory on where you put up all your projects. I prefer to have mine under /dev/personal-projects so I'll cd into it.

Next up is create a directory and call it "flutter-fake-data-factory-pattern" via mkdir terminal command.

mkdir flutter-fake-data-factory-pattern

Then next is to initialize Git in this directory so we can start up to track changes. Or maybe upload this into your repository or not. Whichever you prefer.

cd flutter-fake-data-factory-pattern && git init

Create a new directory called packages and create a Flutter package inside that directory and call it app_core

mkdir packages

cd packages

flutter create --template=package app_core

Once that is done you can proceed with installing the dependencies mentioned above.

Have the following in your dependencies pubspec.yaml file

# Add this so our local package will not be published to pub.dev
publish_to: "none"

dependencies:
    freezed_annotation: ^0.14.2
    json_annotation: ^4.0.1
    faker: ^2.0.0
    uuid: ^3.0.4

dev_dependencies:
    build_runner: ^2.0.5
    freezed: ^0.14.2
    json_serializable: ^4.1.3

Install the dependencies afterwards.

cd app_core && flutter pub get

Next is to create a Flutter project is where our Flutter app lives. Now go up 2 directories via

cd ../..

Then create another directory and call it "apps", it is where we will house our Flutter apps.

mkdir apps

flutter create client_app

Then finally, just import the local package in "client_app" pubspec.yaml

app_core:
    path: ../../packages/app_core

Now that it is all setup and done we can proceed with the implementation of the Factory Pattern.

Keep in mind that since we have this kind of setup, all of our data models will be coming from the local package that we created app_core

Creating the ModelFactory abstract

We'll want to create an abstract class for our factory classes so that they will be aligned with the actual implementation, then we'll just have a generic type argument to pass down to differ its results when other classes implements the abstract.

import 'package:faker/faker.dart';
import 'package:uuid/uuid.dart';

abstract class ModelFactory<T> {
  Faker get faker => Faker();

  /// Creates a fake uuid.
  String createFakeUuid() {
    return Uuid().v4();
  }

    /// Generate a single fake model.
  T generateFake();

  /// Generate fake list based on provided length.
  List<T> generateFakeList({required int length});
}

We are only keeping it simple, we just want a single item and a List of items of a particular data model.

  • The getter faker is the Faker instance that got instantiated, it is where we grab all random data of any type.
  • Next is the createFakeUuid method which is responsible for generating us a fake uuid out of the fly. Typically uuids are used when you have a data provider like Firestore or NoSQL databases like MongoDB, or a relational database that has a primary key type of uuid. But you can switch this up any however you want it to be.
  • The generateFake is responsible for creating a single data model that requires implementation details; We'll have the implementation details for this in the factory class that extends this abstract class.
  • Lastly, the generateFakeList will return a list, implementation details are not concerned here as well and it is being handled in the factory class the implements this abstract. It will just simply return a list of what is returned from generateFake method.

Defining our data models

We'll just have it simple, we'll want two for now a User and a Tweet data model.

import 'package:freezed_annotation/freezed_annotation.dart';

part 'user.g.dart';
part 'user.freezed.dart';


class User with _$User {
  factory User({
    required String id,
    required String name,
    required String email,
    int? age,
    (false) bool suspended,
  }) = _User;

  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
}
import 'package:freezed_annotation/freezed_annotation.dart';

part 'tweet.g.dart';
part 'tweet.freezed.dart';


class Tweet with _$Tweet {
  factory Tweet({
    required String id,
    required String content,
    ([]) List<String> replies,
    (0) int likes,
    (0) int retweets,
  }) = _Tweet;

  factory Tweet.fromJson(Map<String, dynamic> json) => _$TweetFromJson(json);
}

Don't forget to run build_runner to let it generated the boilerplate code for us. Run it via this terminal command (You have to be in app_core directory otherwise this won't run):

flutter pub run build_runner build --delete-conflicting-outputs

If you want to learn more about data modeling with freezed package, read more about it from this tutorial that I wrote.

Creating a factory class

Now to implement the abstract class ModelFactory

import 'package:app_core/app_core.dart';

class UserFactory extends ModelFactory<User> {
  
  User generateFake() {
    return User(
      id: createFakeUuid(),
      email: faker.internet.email(),
      name: '${faker.person.firstName()} ${faker.person.lastName()}'.trim(),
      age: faker.randomGenerator.integer(25),
      suspended: faker.randomGenerator.boolean(),
    );
  }

  
  List<User> generateFakeList({required int length}) {
    return List.generate(length, (index) => generateFake());
  }
}
import 'package:app_core/app_core.dart';

class TweetFactory extends ModelFactory<Tweet> {
  
  Tweet generateFake() {
    return Tweet(
      id: createFakeUuid(),
      content: faker.lorem.words(99).join(' '),
      likes: faker.randomGenerator.integer(5000),
      retweets: faker.randomGenerator.integer(2500),
      replies: List.generate(
        faker.randomGenerator.integer(105),
        (index) => faker.lorem.words(120).join(' '),
      ),
    );
  }

  
  List<Tweet> generateFakeList({required int length}) {
    return List.generate(length, (index) => generateFake());
  }
}

Sample usage for generating fake data from the factory class

It's so easy and straight forward just like what you are thinking right now on how we can use these factory classes to generate us some fake data.

/// A new instance of [UserFactory]
final userFactory = UserFactory();

/// Generates a [User] data model
final fakeUser = userFactory.generateFake();

/// Will generate a [List] of 10 [User] data model
final fakeUsers = userFactory.generateFakeList(10);

We can just basically import it anywhere and we don't have to manually set up the values for each property a User data model has.

Conclusion

We learned how to use Faker package and applied abstraction to avoid writing up the same code again and again for other data models to generate fake data. This would come quite so handy when we write a lot of unit tests with mocking as we don't have to create a data model instance and define each properties for each test case. And lastly we can also make use of this in the UI for placeholder data when the API is not yet ready to integrate with the frontend app.

Full source code can be found from the repository

19