昨天最後有介紹到Mockito這個套件的用途和用法,由於今天要測試的Bloc內的商業邏輯都有使用的網路的服務(Firebase、TMDb API...),所以需要使用Mockito來為我們模擬這些服務加速運行測試的速度。
再次提醒記得要在pubspec.yaml引用test和Mockito套件
※ Authentication和Login Bloc程式碼參考於Felix Angelov的Medium文章
在test/bloc資料夾下新增「authentication_bloc_test.dart」
引用所需的檔案和套件,並建立一個mock UserRepository 的實體來模擬原本的UserRepository。
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:fluttube/firebase/user_repository.dart';
import 'package:fluttube/authentication_bloc/bloc.dart';
class MockUserRepository extends Mock implements UserRepository {}
還記得昨天提過的setUp()嗎? setUp內的程式碼會在每一個測試開始前執行,在這裡設定每次測試開始前初始化MockUserRepository和AuthenticationBloc
。
void main() {
AuthenticationBloc authenticationBloc;
MockUserRepository userRepository;
setUp(() {
userRepository = MockUserRepository();
authenticationBloc = AuthenticationBloc(userRepository: userRepository);
});
...
}
第一個要測試的是Bloc的初始值是否正確以及關閉(dispose)Bloc後還有沒有產生State。
test('initial state is correct', () {
expect(authenticationBloc.initialState, Uninitialized());
});
test('dispose does not emit new states', () {
expectLater(
authenticationBloc.state,
emitsInOrder([]),
);
authenticationBloc.dispose();
});
emitsInOrder([])
會回傳StreamMatcher,用來和authenticationBloc.state
比較。
測試當Event是「AppStarted」的情況,分成兩種
group('AppStarted', () {
test('emits [uninitialized, unauthenticated] for invalid token', () {
final expectedResponse = [
Uninitialized(),
Unauthenticated()
];
when(userRepository.isSignedIn()).thenAnswer((_) => Future.value(false));
expectLater(
authenticationBloc.state,
emitsInOrder(expectedResponse),
);
authenticationBloc.dispatch(AppStarted());
});
test('emits [uninitialized, authenticated] for valid token', () {
final expectedResponse = [
Uninitialized(),
Authenticated("tester")
];
when(userRepository.isSignedIn()).thenAnswer((_) => Future.value(true));
when(userRepository.getUser()).thenAnswer((_) => Future.value('tester'));
expectLater(
authenticationBloc.state,
emitsInOrder(expectedResponse),
);
authenticationBloc.dispatch(AppStarted());
});
});
測試觸發Login和Logout Event,State是否有正確轉換。
group('LoggedIn', () {
test(
'emits [uninitialized, authenticated] when token is persisted',
() {
final expectedResponse = [
Uninitialized(),
Authenticated("tester"),
];
when(userRepository.getUser()).thenAnswer((_) => Future.value("tester"));
expectLater(
authenticationBloc.state,
emitsInOrder(expectedResponse),
);
authenticationBloc.dispatch(LoggedIn());
});
});
group('LoggedOut', () {
test(
'emits [uninitialized, unauthenticated] when token is deleted',
() {
final expectedResponse = [
Uninitialized(),
Unauthenticated(),
];
expectLater(
authenticationBloc.state,
emitsInOrder(expectedResponse),
);
authenticationBloc.dispatch(LoggedOut());
});
});
在test/bloc資料夾下新增「login_bloc_test.dart」
引用所需的檔案和套件,並建立一個mock UserRepository 的實體來模擬原本的UserRepository。
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:fluttube/firebase/user_repository.dart';
import 'package:fluttube/login/bloc/bloc.dart';
class MockUserRepository extends Mock implements UserRepository {}
使用setUp()
在每次測試開始前初始化userRepository和loginBloc。
void main() {
MockUserRepository userRepository;
LoginBloc loginBloc;
setUp(() {
userRepository = MockUserRepository();
loginBloc = LoginBloc(userRepository: userRepository);
});
測試initState和dispose是否正常運作。
這裡需要使用到toString()
把State內的值轉成字串來做比較,因為LoginState並沒有繼承Equatable協助比較兩個實體是否相同。所以即使比較的兩個實體內的屬性值都相同,仍然會被判定為「不等於」,導致測試不會過。
而AuthenticationState有繼承Equatable所以可以直接進行比較
你也可以選擇Override LoginState的hashCode
,也能有同樣的效果
test('initial state is correct', () {
expect(LoginState.empty().toString(), loginBloc.initialState.toString());
});
test('dispose does not emit new states', () {
expectLater(
loginBloc.state,
emitsInOrder([]),
);
loginBloc.dispose();
});
測試輸入正確和錯誤的帳號密碼,LoginState有無正確的轉換。
group('LoginButtonPressed', () {
test('emits token on success', () {
final expectedResponse = [
LoginState.empty().toString(),
LoginState.loading().toString(),
LoginState.success().toString(),
];
expectLater(
loginBloc.state.map((state) => state.toString()),
emitsInOrder(expectedResponse),
);
loginBloc.dispatch(LoginWithCredentialsPressed(
email: 'tester123@gmail.com',
password: 'password123',
));
});
test('throw error on fail', (){
final expectedResponse = [
LoginState.empty().toString(),
LoginState.loading().toString(),
LoginState.failure().toString(),
];
when(userRepository.signInWithCredentials("wrong", "wrong")).thenThrow(Exception);
expectLater(
loginBloc.state.map((state) => state.toString()),
emitsInOrder(expectedResponse),
);
loginBloc.dispatch(LoginWithCredentialsPressed(
email: 'wrong',
password: 'wrong',
));
});
});
測試Validator有沒有正確的檢查信箱和密碼格式。
需要加上 await Future.delayed(Duration(milliseconds: 400))
是因為在實作LoginBloc時有使用到debounceTime讓Validator會等待使用者0.3秒的時間才進行驗證,所以這邊也必須等到debounceTime到了才再觸發Event。
group('test validation do the right judgement', (){
test('test email validator', () async{
final expectedResponse = [
LoginState.empty().toString(),
LoginState.empty().update(isEmailValid: false, isPasswordValid: true).toString(),
LoginState.empty().update(isEmailValid: true, isPasswordValid: true).toString(),
];
expectLater(
loginBloc.state.map((state) => state.toString()),
emitsInOrder(expectedResponse),
);
loginBloc.dispatch(EmailChanged(email: 'WRONG'));
await Future.delayed(Duration(milliseconds: 400));
loginBloc.dispatch(EmailChanged(email: 'GOOD@gmail.com'));
});
test('test password validator', () async{
final expectedResponse = [
LoginState.empty().toString(),
LoginState.empty().update(isEmailValid: true, isPasswordValid: false).toString(),
LoginState.empty().update(isEmailValid: true, isPasswordValid: true).toString(),
];
expectLater(
loginBloc.state.map((state) => state.toString()),
emitsInOrder(expectedResponse),
);
loginBloc.dispatch(PasswordChanged(password: 'Bad'));
await Future.delayed(Duration(milliseconds: 400));
loginBloc.dispatch(PasswordChanged(password: 'goodPassword123'));
});
});
在test/bloc資料夾下新增「register_bloc_test.dart」
Register和Login的測試程式碼基本上相同,就不額外解釋囉。
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:fluttube/firebase/user_repository.dart';
import 'package:fluttube/register/bloc/bloc.dart';
class MockUserRepository extends Mock implements UserRepository {}
void main() {
MockUserRepository userRepository;
RegisterBloc registerBloc;
setUp(() {
userRepository = MockUserRepository();
registerBloc = RegisterBloc(userRepository: userRepository);
});
test('initial state is correct', () {
expect(RegisterState.empty().toString(), registerBloc.initialState.toString());
});
test('dispose does not emit new states', () {
expectLater(
registerBloc.state,
emitsInOrder([]),
);
registerBloc.dispose();
});
group('LoginButtonPressed', () {
test('emits token on success', () {
final expectedResponse = [
RegisterState.empty().toString(),
RegisterState.loading().toString(),
RegisterState.success().toString(),
];
expectLater(
registerBloc.state.map((state) => state.toString()),
emitsInOrder(expectedResponse),
);
registerBloc.dispatch(Submitted(
email: 'tester123@gmail.com',
password: 'password123',
));
});
test('throw error on fail', (){
final expectedResponse = [
RegisterState.empty().toString(),
RegisterState.loading().toString(),
RegisterState.failure().toString(),
];
when(userRepository.signUp(email: "wrong", password: "wrong")).thenThrow(Exception);
expectLater(
registerBloc.state.map((state) => state.toString()),
emitsInOrder(expectedResponse),
);
registerBloc.dispatch(Submitted(
email: 'wrong',
password: 'wrong',
));
});
});
group('test validation do the right judgement', (){
test('test email validator', () async{
final expectedResponse = [
RegisterState.empty().toString(),
RegisterState.empty().update(isEmailValid: false, isPasswordValid: true).toString(),
RegisterState.empty().update(isEmailValid: true, isPasswordValid: true).toString(),
];
expectLater(
registerBloc.state.map((state) => state.toString()),
emitsInOrder(expectedResponse),
);
registerBloc.dispatch(EmailChanged(email: 'WRONG'));
await Future.delayed(Duration(milliseconds: 400));
registerBloc.dispatch(EmailChanged(email: 'GOOD@gmail.com'));
});
test('test password validator', () async{
final expectedResponse = [
RegisterState.empty().toString(),
RegisterState.empty().update(isEmailValid: true, isPasswordValid: false).toString(),
RegisterState.empty().update(isEmailValid: true, isPasswordValid: true).toString(),
];
expectLater(
registerBloc.state.map((state) => state.toString()),
emitsInOrder(expectedResponse),
);
registerBloc.dispatch(PasswordChanged(password: 'Bad'));
await Future.delayed(Duration(milliseconds: 400));
registerBloc.dispatch(PasswordChanged(password: 'goodPassword123'));
});
});
}
今天幫Authentication、Login、Register三個Bloc寫了測試,確保State都有在正確的時機和順序被轉換。
經過實際使用Mockito後應該能瞭解使用Mock服務的好處,除了能省去網路傳輸的不穩定以及測試時間外。假如是要測試像「SignUp」的功能,應該不可能每次都輸入不同的帳號密碼去實際註冊測試對吧,但是用模擬的方式就不受影響。
明天會完成Movie Bloc和Movie API的測試。
完整程式碼在這裡-> FlutTube Github