19
Generating Fake Data in Flutter using the Factory Pattern for Unit Testing
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.
We will only require the following packages for us to seamlessly generate fake data.
- faker: Will handle generating of random data of any data type.
- uuid: Will generate a random uuid for us.
- build_runner: Responsible for auto-generating us some boilerplate code.
- freezed: Is where we create our data models.
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
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 theFaker
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 fromgenerateFake
method.
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.
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());
}
}
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.
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