Fala, pessoal! Tudo certo? Todos nós já nos deparamos com cenários de teste em que o objeto sob teste (SUT
) depende de outro objeto, cujo retorno é feito a partir de um callback
. Como poderíamos estressar nosso SUT
? Nesse artigo, vamos explorar como testar esses cenários!
Contexto
Imagine que nosso sistema possui uma funcionalidade de transferência entre contas. Geralmente, o processo de transferência envolve algumas etapas, como verificar o saldo, validar a conta de destino e receber o identificador de sucesso da transferência.
Em termos de código teríamos
abstract class BankRepository {
void transfer(
required Function(String transactionId) onSuccess,
required Function(String error) onError,
required String fromAccount,
required String toAccount,
required double amount,
);
}
import 'package:flutter_bloc/flutter_bloc.dart';
abstract class BankState {}
class InitialState extends BankState {}
class TransferInProgress extends BankState {}
class TransferSuccess extends BankState {
final String transactionId;
TransferSuccess(this.transactionId);
}
class TransferError extends BankState {
final String error;
TransferError(this.error);
}
class BankCubit extends Cubit<BankState> {
final BankService _bankService;
BankCubit(this._bankService) : super(InitialState());
void transfer(String fromAccount, String toAccount, double amount) {
emit(TransferInProgress());
_bankService.transferMoney(
fromAccount: fromAccount,
toAccount: toAccount,
amount: amount,
onSuccess: (transactionId) {
emit(TransferSuccess(transactionId));
},
onError: (error) {
emit(TransferError(error));
},
);
}
}
Vamos dar uma olhada como poderíamos garantir a emissão tanto no estado de sucesso quanto no de erro?
Garantindo o estado de sucesso
import 'package:bloc_test/bloc_test.dart';
import 'package:test/test.dart';
import 'package:mocktail/mocktail.dart';
class MockBankService extends Mock implements BankService {}
void main() {
group('BankCubit', () {
late BankCubit bankCubit;
late MockBankService mockBankService;
setUp(() {
mockBankService = MockBankService();
bankCubit = BankCubit(mockBankService);
});
blocTest<BankCubit, BankState>(
'emits [TransferInProgress, TransferSuccess] when transfer is successful',
build: () {
when(() => mockBankService.transferMoney(
fromAccount: any(named: 'fromAccount'),
toAccount: any(named: 'toAccount'),
amount: any(named: 'amount'),
onSuccess: any(named: 'onSuccess'),
onError: any(named: 'onError'),
)).thenAnswer((invocation) {
final onSuccess = invocation.namedArguments[#onSuccess] as Function(String);
onSuccess("TX123456");
});
return bankCubit;
},
act: (cubit) => cubit.transfer("123", "456", 100.0),
expect: () => [
TransferInProgress(),
TransferSuccess("TX123456"),
],
);
});
}
Garantindo o estado de erro
blocTest<BankCubit, BankState>(
'emits [TransferInProgress, TransferError] when transfer fails',
build: () {
when(() => mockBankService.transferMoney(
fromAccount: any(named: 'fromAccount'),
toAccount: any(named: 'toAccount'),
amount: any(named: 'amount'),
onSuccess: any(named: 'onSuccess'),
onError: any(named: 'onError'),
)).thenAnswer((invocation) {
final onError = invocation.namedArguments[#onError] as Function(String);
onError("Insufficient funds");
});
return bankCubit;
},
act: (cubit) => cubit.transfer("123", "456", 2000.0),
expect: () => [
TransferInProgress(),
TransferError("Insufficient funds"),
],
);
Explicação
Tanto no cenário de erro quanto no de sucesso, capturamos o Callback
correspondente usando o invocation
.
O invocation
atua como um ‘gravador’, armazenando todas as interações com o método mockado, juntamento com os parâmetros utilizados.
Como acessar os argumentos nomeados?
Para acessarmos os argumentos nomeados por meio do invocation
utilizaremos a propriedade namedArguments
. Dessa forma conseguiremos ter acesso ao callback
que foi passado para o método e chamá-lo manualmente, permitindo que exercitemos os cenários de teste. Para isso:
final onSuccess = invocation.namedArguments[const Symbol('onSuccess')];
ou
final onSuccess = invocation.namedArguments[#onSuccess];
Conclusão
Em alguns casos, podemos nos deparar com callbacks
em nossos SUT
, e, à primeira vista, eles podem parecer desfiadores. Mas, como vimos, o uso do mocktail
facilita nossa vida nesses casos.
Ao capturarmos os argumentos passados para o callback
através do invocation
, conseguimos simular os valores necessários dado o contexto de nosso cenário. Em nosso exemplo, isso permitiu garantir a emissão correta dos estados, de acordo com o retorno de nosso serviço.