Tô ligado que nessa semana, provavelmente, você teve algum Pull Request [PR] barrado pelo sonar por conta de Quality Gate [QG].

E é por isso que nessa série de artigos vamos falar de técnicas para teste! Dai você não passa esse perrengue em pré feriado ou na sexta-feira.

Contexto

A série de artigos vai ser dividida em dois períodos: Sem o uso do MockTail [S.M] e Com o uso do MockTail [C.M]. Antes de prosseguirmos para nosso primeiro papo, você pode pensar

Leitor: Pô, Caíque. Mas eu uso {Qualquer outra lib de Mock} e ai?

Pode ficar tranquilo, todos os conceitos apresentados nos artigos [C.M] vão te ajudar a entender seu framework e dai é contigo. Capture them all

Imagina que o seu [QG] falhou por falta de cobertura no método connect do seu Repository.

Dando uma olhada nele, você percebe que lá existe a conexão e a escuta dos eventos de um WebSocket. O código parece ser algo do tipo:

final StreamController<dynamic> _streamController = StreamController<dynamic>();
Stream<dynamic> get events => _streamController.stream;
 
Future<void> connect(String url) async {
  _webSocket = await WebSocket.connect(url, {}, []);
  _listen();
}
 
void _listen(){
  _webSocket.listen(
    (data) {
      _streamController.add(data);
    },
    onError: (error) {
      _streamController.addError(error);
    },
    onDone: () {
      _streamController.close();
    }
  )
}

É, tu pensou que ia acabar o expediente mais cedo , né? Depois desse artigo, você vai mesmo.

O nosso teste

Para começar nosso teste, vamos precisar adicionar em nosso pubspec.yml

dev_dependencies:
  mocktail: ^0.2.0

E teremos algo do tipo

test(
  'Mocking emission from webserver`,
  () {
    when(
      () => mockWebSocket.connect(any())
    ).thenAswer(_) async => mockWebsocket);
 
  when(
      () => mockWebSocket.listen(
              any(),
              onError: any(named: 'onError'),
              onDone: any(named: 'onDone'),
      )
  ).thenAnswer(
      (invocation) {
        final onData = invocation.positionalArguments[0] as void        Function(dynamic);
        final onError = invocation.namedArguments[#onError] as void Function(Object);
        final onDone = invocation.namedArguments[#onDone] as void Function();
      return streamController.stream.listen(onData, onError: onError, onDone: onDone);
      },
  );
 
 await repository.connect(url);
 
 expectLater(repository.events, emits('Hello'));
 
 streamController.add('Hello');
 },
);

Pronto, você já pode sextar! Mas, antes de ir, da um confere em como funciona a função when do mocktail.

A estrutura da função When

A estrutura da função when sempre será algo do tipo

 when(() => objectoMockado.nomeDoMetodo(arg1, arg2, ...)).thenAnswer(
    (invocation) {
     // Voce pode fazer qualquer coisa aqui desde que você retorne o tipo indicado pelo método `nomeDoMetodo`
    },
  );

Algumas vezes, você verá que ao invés de passar o argumento, a pessoa passará uma função chamada any(). Ela tem como funcionalidade realizar o match de qualquer argumento na etapa de estruturação do seu mock.

Apesar de ela não ligar para o argumento, você pode ser um pouco mais específico em seu uso, isso ocorre através dos atributos named e that.

Especificando através do named

Permite que você realize o match através de um argumento nomeado.

Leitor: Que beleza! Vou usar isso em todos meus setup de mocks.

Calma lá. Existem alguns cenários no qual o uso do named irá levar a erros. Por exemplo,

void exampleFunction(int a, {int? b}) {
  print("a: $a, b: $b");
}

Agora imagine que queremos realizar o mock dessa função. Na abordagem do nosso leitor, teríamos:

when(mock.exampleFunction(any(named: 'a'))).thenReturn('someValue');

Esse trecho de código retornará um erro, pois o argumento que estamos tentando realizar o ‘match’ não é nomeado.

Ajustando esse setup, teríamos:

when(mock.exampleFunction(1, b: any(named: 'b'))).thenReturn('someValue');

Mas o que aconteceria se precisássemos realizar o mock de um atributo dado uma determinada condição?

Especificando através de that

Quando precisamos aplicar condições ao nosso setup, podemos utilizar o atributo that ao invés do named ou com ele.

Esse atributo permite que passemos ‘Matchers’ para realizar o ‘Match’. Pô, frase complexa. Bora lá dar uma olhada nesse caso para termos um melhor entendimento.

Imagine que estamos mockando um callback. Caso o parâmetro do callback seja par, então iremos retornar um valor. Já, caso ele seja ímpar, retornaremos outro. Para se obter esse comportamento:

when(mock.someFunction(any(that: (int x) => x % 2 == 0))).thenReturn('Even');
when(mock.someFunction(any(that: (int x) => x % 2 != 0))).thenReturn('Odd');

Agora sabemos como fazer os Matches necessários para concluirmos o setup do nosso Mock. Porem como controlamos o que ele retornará ?

Controlando o retorno do nosso objeto de mock

Quando precisamos de um controle maior sobre o que precisamos retornar, podemos utilizar o thenAnswer.

Olhando para a sua assinatura, vemos que o seu retorno é um callback, o qual possui uma variável do tipo Invocation.

Esse objeto contém todos os metadados relacionados à invocação daquela chamada para o mock.

Tá, estava fácil, mas parece que complicou. Vamos tentar entender esse método através de duas explicações: Em uma delas iremos utilizar o thenAnswer para lidar com futures e a segunda na qual iremos interceptar os valores passados para o mock e compor o resultado dele.

Utilizando thenAnswer para retorno de futures:

Caso a função do seu mock retorne um Future, o thenAnswer pode te ajudar nesse caso. Por exemplo:

when(() => meuMock.metodoAssincrono()).thenAnswer((\_) async => valorRetornado);

Utilizando thenAnswer para interceptar valores passados para o mock

Outra utilidade poderosa do thenAnswer é a capacidade de acessar os argumentos que foram passados para o método mockado.

Isso é feito através do objeto Invocation que é passado para o callback. Por exemplo:

 when(() => meuMock.metodoComArgumentos(any())).thenAnswer((invocation) {
  final arg = invocation.positionalArguments\[0];
  return 'Valor baseado em $arg';
});

Neste exemplo, interceptamos o argumento passado para metodoComArgumentos e retornamos um valor baseado nele.

É isso ai

Bom, até agora vimos o uso do ´when´ para a estruturação dos comportamentos do nosso objeto de mock. Ainda nesse tema, nos aprofundamos em utilizar uma técnica de ´matching´ para termos maior flexibilidade na hora do setup.

Os próximos tópicos vão englobar um pouco mais sobre teoria de teste e criação de um repositório com os cenários mais comuns que você enfrentará no seu dia a dia.

Mocktail - Pub Dev