How to Create Testable Mixins in Dart/Flutter

Creating Unit Test-Friendly Mixins in Dart/Flutter

Jessica Jimantoro
6 min readMay 28, 2023
Photo by Mel Poole on Unsplash

Hey guys! 👋

This article is reserved for those who want to create a mixin and do the unit test on the mixin or anyone who wants to know more about Dart’s mixin!

What is Mixin?

Based on the documentation, Mixins are a way of reusing a class’s code in multiple class hierarchies, without inheritance.

When to Use A Mixin?

The primary use case for mixin is when you want to incorporate a behavior into a class, but the behavior is not related to the parent class through inheritance. Besides that, it also can enforce the Single Responsibility Principle. For example, LoggerMixin is not related to Login but it’s needed.

These are some cases where to use mixin such as logger, tracker, validator, etc.

Create A Simple Mixin (#1)

Create A Mixin

For the example, we’re gonna create a LoggerMixin with tag property and log method.

/// Create a Mixin
mixin LoggerMixin {
String tag = '[Default]';

void log(String message) {
print('$tag: $message');
}
}

Apply Mixin to Class

You can apply the LoginViewModel with LoggerMixin through with keyword.

/// Apply Mixin into LoginViewModel class
class LoginViewModel with LoggerMixin {
LoginViewModel() {
tag = '[LoginViewModel]';
}

void login() {
log('Login!');
}
}

Call the login()

void main() {
final viewModel = LoginViewModel();
viewModel.login(); // Output: [LoginViewModel]: Login!
}

Unit Test

Here, you need to add mock packages like mocktail or mockito to your dev dependencies. For the example, I will use the mocktail.

Because you can’t test the mixin immediately, you can simply mock it into class and do the unit test.

This one below is how to test the mixin.

class MockLoggerMixinClass with LoggerMixin {}

void main() {
MockLoggerMixinClass logger = MockLoggerMixinClass();

group('LoggerMixin', () {
test('should have a correct behavior', () {
logger.tag = '[Testing]';
logger.log('Foo!');

expect(logger.tag, '[Testing]');
});
});
}

How about the LoginViewModel unit test?

You can’t verify whether the mixin’s method gets called or not if you just test the class like this since the mixin is not a mocktail object 🙅‍♀.️

void main() {
...
group('LoginViewModel', () {
test('should successfully log message', () {
LoginViewModel viewModel = LoginViewModel();

viewModel.tag = '[Login]';
viewModel.login();

verify(() => viewModel.log(any())).called(1);
expect(viewModel.tag, '[Login]');
});
});
}

So these are the way you can do it.
First of all, you need to mock the log call function.

/// Mock a log call function
abstract class MockFunctionWithAParam {
void call(String message);
}

class MockOnLog extends Mock implements MockFunctionWithAParam {}

MockOnLog onLogFunction = MockOnLog();

Then, create a mixin mock of LoggerMixin and use the onLogFunction.

mixin MockLoggerMixin on LoggerMixin {
@override
void log(String message) => onLogFunction.call(message);
}

Just apply the mixin into the view model mock class.

class MockLoginViewModelWithMixin extends LoginViewModel 
with MockLoggerMixin {}

Last, apply the unit test using the created mock.


void main() {
...
group('LoginViewModel', () {
test('should successfully log message', () {
MockLoginViewModelWithMixin viewModel = MockLoginViewModelWithMixin();

viewModel.tag = '[Login]';
viewModel.login();

verify(() => onLogFunction.call(any())).called(1);
expect(viewModel.tag, '[Login]');
});
});
}

Done! 🎉

Create A Class with Multiple Mixins (#2)

Sometimes, there are conditions that you will need to have more than 1 mixin i.e. we’ll create a login view model with validator and logger mixins.

Create Mixins

mixin LoggerMixin {
String tag = '[Default]';

void log(String message) {
print('$tag: $message');
}
}

mixin ValidatorMixin {
bool validateEmail(String email) {
return email.contains('@');
}

bool validatePassword(String password) {
return password.length > 6;
}
}

Apply Mixins to Class

You only need to add the ValidatorMixin to the class along with LoggerMixin.

class LoginViewModel with LoggerMixin, ValidatorMixin {
LoginViewModel() {
tag = '[Login]';
}

bool login({
required String email,
required String password,
}) {
if (validateEmail(email) && validatePassword(password)) {
log('Login Success!');
return true;
}
log('Login Failed!');
return false;
}
}

Call the login()

void main() {
final viewModel = LoginViewModel();
print(
viewModel.login(
email: 'something@mail.com',
password: '12345678',
),
);
/// [Login]: Login Success!
/// true
}

Unit Test

For the Logger mixin, you have done it in the first example (#1). So we’ll do the validator mixin test. The way to do it is the same as LoggerMixin’s unit test.

class MockValidatorMixinClass with ValidatorMixin {}

void main() {
MockValidatorMixinClass validator = MockValidatorMixinClass();

group('ValidatorMixin', () {
test('should return true when email contains @', () {
expect(validator.validateEmail('something@mail.com'), true);
});
test('should return false when email does not contain @', () {
expect(validator.validateEmail('somethingmail.com'), false);
});
test('should return true when password is more than 6 characters', () {
expect(validator.validatePassword('1234567'), true);
});
test('should return false when password is not more than 6 characters', () {
expect(validator.validatePassword('12'), false);
});
});
}

Next, mock the mixins with the same procedures.

/// Mock Functions
abstract class MockFunctionWithAParam {
void call(String param);
}

abstract class MockFunctionWithAParamAndBoolReturn {
bool call(String param);
}

class MockOnLog extends Mock implements MockFunctionWithAParam {}

class MockOnValidateEmail extends Mock
implements MockFunctionWithAParamAndBoolReturn {}

class MockOnValidatePassword extends Mock
implements MockFunctionWithAParamAndBoolReturn {}

MockOnLog onLogFunction = MockOnLog();
MockOnValidateEmail onValidateEmailFunction = MockOnValidateEmail();
MockOnValidatePassword onValidatePasswordFunction = MockOnValidatePassword();

/// Create a mocked mixin and apply the mocked functions
mixin MockLoggerMixin on LoggerMixin {
@override
void log(String message) => onLogFunction.call(message);
}

mixin MockValidatorMixin on ValidatorMixin {
@override
bool validateEmail(String email) => onValidateEmailFunction.call(email);

@override
bool validatePassword(String password) =>
onValidatePasswordFunction.call(password);
}

You already do a small test on the mixins. On the view model test, you can just stub what the function should return.

Why do we do it this way instead of testing the view model immediately without mocking the mixin?
It’s because we want to isolate the mixin test so you can focus to test your view model function and control the mixin’s state.

void main() {
...
group('LoginViewModel', () {
test('should return true and log message', () {
MockLoginViewModelWithMixin viewModel = MockLoginViewModelWithMixin();
viewModel.tag = '[Login]';

when(() => onValidateEmailFunction.call(any())).thenAnswer(
(_) => true,
);

when(() => onValidatePasswordFunction.call(any())).thenAnswer(
(_) => true,
);

bool isSuccess = viewModel.login(
email: 'something@mail.com',
password: '1234567',
);

expect(isSuccess, true);
expect(viewModel.tag, '[Login]');
verify(() => onLogFunction.call(any())).called(1);
});

test('should return false and log message', () {
MockLoginViewModelWithMixin viewModel = MockLoginViewModelWithMixin();
viewModel.tag = '[Login]';

when(() => onValidateEmailFunction.call(any())).thenAnswer(
(_) => true,
);

when(() => onValidatePasswordFunction.call(any())).thenAnswer(
(_) => false,
);

bool isSuccess = viewModel.login(
email: 'somethingmail.com',
password: '12345',
);

expect(isSuccess, false);
expect(viewModel.tag, '[Login]');
verify(() => onLogFunction.call(any())).called(1);
});
});
}

Create A Mixin Interface (#3)

Overriding Conflicting Mixin Members

Look at this code below!

mixin AMixin {
void foo() {
print('Foo');
}
}

mixin BMixin {
void foo() {
print('Bar');
}
}

class MyClass with AMixin, BMixin {
void printSomething() {
foo();
}
}

void main() {
final myClass = MyClass();
myClass.printSomething();
}

What will be printed?
It will print Bar because BMixin is on the last trails.

And of course, we won’t this case happen on our code. It will be hard to avoid conflict when the code goes long.

The method to solve this is by using the interface. Another benefit is you can control what is inside your mixin, like fetching API or increasing readability.

Note that the interface is not necessarily required except if you need to call the API or enhance the readability.

Create An Interface for the Mixin

abstract class A {
void foo();
}

mixin AMixin implements A {
@override
void foo() {
print('Foo');
}
}

// class with AMixin and BMixin
class MyClass with AMixin, BMixin {
void printSomething() {
foo();
}
}

Unit Test the Class

As usual, create a mocked function, create a mocked mixin on its interface, and create a mocked class with mixin.

abstract class MockFunction {
void call();
}

class MockOnFoo extends Mock implements MockFunction {}

MockOnFoo onFooFunction = MockOnFoo();

mixin MockAMixin on A {
@override
void foo() => onFooFunction.call();
}

class MockMyClassWithMixin extends MyClass with MockAMixin {}

Test the class!

void main() {
group('MyClass', () {
test('should call foo', () {
MockMyClassWithMixin myClass = MockMyClassWithMixin();

myClass.printSomething();

verify(() => onFooFunction.call()).called(1);
});
});
}

It’s a wrap!

Feel free to leave a comment below and let’s get connected on LinkedIn!
For the source codes, you can click on this Github!

Thank you for reading! 🐈

--

--

Jessica Jimantoro

Software Engineer & Flutter Developer 👩‍💻, Amateur anime-style sculptor 🧸, Tarot Reader 🔮