Teste Bloc: Escreva seu primeiro teste de unidade simples em Flutter

Tempo de leitura: 5 minutes

Durante o desenvolvimento, precisamos ter certeza de que tudo funciona bem. Então, precisamos testar nosso código. Existem várias etapas de teste e uma delas é o Teste de Unidade. Neste artigo, escreveremos um BloC simples que buscará e filtrará itens. Em seguida, escreveremos o teste para o nosso BloC.

dependencies:
  flutter:
    sdk: flutter
  flutter_bloc: ^8.1.3
  equatable: ^2.0.5

dev_dependencies:
  flutter_lints: ^2.0.0
  flutter_test:
    sdk: flutter
  bloc_test: ^9.1.3

Equatable é usado para simplificar os processos de igualdade (no Dart, se compararmos com o objeto usando ==, ele não os comparará com os campos, mas com o hashcode e o operador ==. Para simplificar esse processo, adicionei esta biblioteca.

 

Modelo Simples

Usaremos o seguinte modelo para nossos dados:

import 'package:equatable/equatable.dart';

class Task extends Equatable {
  const Task({
    required this.id,
    required this.title,
    this.isUrgent = false,
  });

  final String id;
  final String title;
  final bool isUrgent;

  @override
  List<Object> get props => [id, title, isUrgent];
}

 

Abstração

A abstração desempenha um papel importante no teste. A melhor maneira de entendê-lo é olhar para a zombaria. Usaremos essa abstração para injeção de dependência. Isso significa que, usando abstração, seremos capazes de adicionar implementação simulada e injetá-la no bloco para simulação.

Import ‘task.dart’;

Abstract class ITaskRepository {
   Future<List<Task>> fetchAllTasks();

   Future<List<Task>> fetchUrgentTasks();
}

 

Mocking

Então, suponha que temos uma implementação concreta para buscar tarefas de qualquer API. Mas nos testes, casos reais – como operações http, funcionalidades nativas do dispositivo, operações de banco de dados e etc não estão funcionando corretamente. Devemos usar mock para teste. Então vamos criar a classe mock para lidar com a implementação do nosso repositório:

import 'i_task_repository.dart';
import 'task.dart';

const mockTasks = <Task>[
  Task(id: '1', title: 'Task 1'),
  Task(id: '2', title: 'Task 2'),
  Task(id: '3', title: 'Task 3'),
  Task(id: '4', title: 'Task 4', isUrgent: true),
  Task(id: '5', title: 'Task 5'),
  Task(id: '6', title: 'Task 6'),
  Task(id: '7', title: 'Task 7', isUrgent: true),
  Task(id: '8', title: 'Task 8'),
  Task(id: '9', title: 'Task 9', isUrgent: true),
  Task(id: '10', title: 'Task 10', isUrgent: true),
];

class MockTaskRepository implements ITaskRepository {
  @override
  Future<List<Task>> fetchAllTasks() async =>
      Future.delayed(const Duration(seconds: 2), () => [...mockTasks]);

  @override
  Future<List<Task>> fetchUrgentTasks() async {
    await Future.delayed(const Duration(seconds: 2));

    final urgentTasks =
        List<Task>.from(mockTasks.where((task) => task.isUrgent));

    return urgentTasks;
  }
}

 

Task BloC

Vamos codificar nosso simples Task Cubit. Estados seguirão:

import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart';

import 'task.dart';

@immutable
abstract class TaskState extends Equatable {
  @override
  List<Object> get props => [];
}

class TaskLoadInitial extends TaskState {}

class TaskLoadInProgress extends TaskState {}

class TaskLoadSuccess extends TaskState {
  TaskLoadSuccess(this.tasks);

  final List<Task> tasks;

  @override
  List<Object> get props => [...tasks];
}

class TaskLoadFailure extends TaskState {}

E o TaskCubit ficará assim:

import 'package:flutter_bloc/flutter_bloc.dart';

import 'i_task_repository.dart';
import 'task_state.dart';

class TaskCubit extends Cubit<TaskState> {
  TaskCubit(this.taskRepository) : super(TaskLoadInitial());

  final ITaskRepository taskRepository;

  void getAllTasks() async {
    try {
      emit(TaskLoadInProgress());

      final tasks = await taskRepository.fetchAllTasks();
      emit(TaskLoadSuccess(tasks));
    } catch (_) {
      emit(TaskLoadFailure());
    }
  }

  void getUrgentTasks() async {
    try {
      emit(TaskLoadInProgress());

      final urgentTasks = await taskRepository.fetchUrgentTasks();
      emit(TaskLoadSuccess(urgentTasks));
    } catch (_) {
      emit(TaskLoadFailure());
    }
  }
}

Como você pode ver, não injetamos o próprio MockTaskRepository. Usamos ImpleTaskRepository para passar nosso objeto de repositório para o BloC. Ele nos permite injetar nossos repositórios fictícios ou reais no BloC. Então, vamos injetar MockTaskRepository no BloC no processo de teste. Yay!

 

Teste

Normalmente, o teste consiste em 3 partes: inicialização ou configuração, operações e expectativas.

  • Initialization => nesta parte, devemos inicializar todas as coisas que são importantes para o processo de teste.
  • Operations => Chamamos métodos ou algo mais, que deve ser testado.
  • Expectations => No final, adicionamos alguns casos que esperamos que seja assim.

 

Teste de BloC

Em primeiro lugar, definimos nosso grupo (você pode alterar a estrutura, é a minha abordagem) e, usando setUp, definimos nosso TaskCubit para todos os testes e, com tearDown, fechamos o TaskCubit quando todos os testes foram concluídos no grupo de teste TaskCubit:

import 'package:bloc_test/bloc_test.dart';
import 'package:bloc_testing_example/mock_task_repository.dart';
import 'package:bloc_testing_example/task.dart';
import 'package:bloc_testing_example/task_cubit.dart';
import 'package:bloc_testing_example/task_state.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  group('TaskCubit test', () {
    late TaskCubit taskCubit;
    MockTaskRepository mockTaskRepository;

    setUp(() {
      EquatableConfig.stringify = true;
      mockTaskRepository = MockTaskRepository();
      taskCubit = TaskCubit(mockTaskRepository);
    });

    tearDown(() {
      taskCubit.close();
    });
  });
}

EquatableConfig.stringify = true é adicionado para mostrar os estados do BloC e seus parâmetros de forma bonita.

import 'package:bloc_test/bloc_test.dart';
import 'package:bloc_testing_example/mock_task_repository.dart';
import 'package:bloc_testing_example/task.dart';
import 'package:bloc_testing_example/task_cubit.dart';
import 'package:bloc_testing_example/task_state.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  group('TaskCubit test', () {
    late TaskCubit taskCubit;
    MockTaskRepository mockTaskRepository;

    setUp(() {
      EquatableConfig.stringify = true;
      mockTaskRepository = MockTaskRepository();
      taskCubit = TaskCubit(mockTaskRepository);
    });

    blocTest<TaskCubit, TaskState>(
      'emits [TaskLoadInProgress, TaskLoadSuccess] states for'
      'successful task loads',
      build: () => taskCubit,
      act: (cubit) => cubit.getAllTasks(),
      expect: () => [
        TaskLoadInProgress(),
        TaskLoadSuccess(mockTasks),
      ],
    );

    blocTest<TaskCubit, TaskState>(
      'emits [TaskLoadInProgress, TaskLoadSuccess] with correct urgent tasks',
      build: () => taskCubit,
      act: (cubit) => cubit.getUrgentTasks(),
      expect: () => [
        TaskLoadInProgress(),
        TaskLoadSuccess(const [
          Task(id: '4', title: 'Task 4', isUrgent: true),
          Task(id: '7', title: 'Task 7', isUrgent: true),
          Task(id: '9', title: 'Task 9', isUrgent: true),
          Task(id: '10', title: 'Task 10', isUrgent: true),
        ]),
      ],
    );

    tearDown(() {
      taskCubit.close();
    });
  });
}

Agora, adicionamos nosso primeiro teste. blocTest é fornecido pela biblioteca bloc_test para testar facilmente os blocos. Nosso primeiro teste inclui o seguinte:

  • Texto de descrição para mostrar quando o teste é executado.
  • build => é usado para criar o objeto BLoC ou Cubit para teste.
  • act => é usado para adicionar evento ou para chamar o método para o gatilho BLoC.
  • expect => é um iterável simples, que é usado para garantir que o ato chamado do parâmetro anterior retornará o estado ou os estados anotados aqui.

Para executar o teste, podemos usar o seguinte comando (ou executar com VS Code ou Android Studio):

Flutter_test

E vai imprimir isso:

Yuppi, nosso primeiro teste funciona bem. Agora, é hora de testar as próximas funcionalidades.

Vamos escrever o teste para o próximo caso.

Nosso teste falhou. Parece que o TaskCubit retorna tarefas que não são urgentes. Por que não funciona?

Oh meu deus!!! Temos um erro no MockTaskRepository!

Por engano, adicionamos um ponto de exclamação (!) na parte da condição de where. É por isso que retorna tarefas que não são urgentes.

@override
Future<List<Task>> fetchUrgentTasks() async {
  await Future.delayed(const Duration(seconds: 2));

  final urgentTasks =
      List<Task>.from(mockTasks.where((task) => !task.isUrgent));

  return urgentTasks;
}

Agora já corrigi:

import 'i_task_repository.dart';
import 'task.dart';

const mockTasks = <Task>[
  Task(id: '1', title: 'Task 1'),
  Task(id: '2', title: 'Task 2'),
  Task(id: '3', title: 'Task 3'),
  Task(id: '4', title: 'Task 4', isUrgent: true),
  Task(id: '5', title: 'Task 5'),
  Task(id: '6', title: 'Task 6'),
  Task(id: '7', title: 'Task 7', isUrgent: true),
  Task(id: '8', title: 'Task 8'),
  Task(id: '9', title: 'Task 9', isUrgent: true),
  Task(id: '10', title: 'Task 10', isUrgent: true),
];

class MockTaskRepository implements ITaskRepository {
  @override
  Future<List<Task>> fetchAllTasks() async =>
      Future.delayed(const Duration(seconds: 2), () => [...mockTasks]);

  @override
  Future<List<Task>> fetchUrgentTasks() async {
    await Future.delayed(const Duration(seconds: 2));

    final urgentTasks =
        List<Task>.from(mockTasks.where((task) => task.isUrgent));

    return urgentTasks;
  }
}

Vamos fazer os testes novamente:

Yupi! Todos os testes passaram!

Você pode encontrar todos os códigos relacionados ao artigo no seguinte repositório: GitHub

 

Não deixe de conhecer meus Ebooks de Flutter/Dart