Mocking

Mocking #

Mocking adalah teknik menggantikan dependensi nyata (database, HTTP client, filesystem) dengan implementasi tiruan yang bisa dikontrol sepenuhnya dari dalam test. Ini memungkinkan pengujian unit yang cepat, deterministik, dan terisolasi — tanpa koneksi jaringan, tanpa data nyata, dan tanpa efek samping. Dart menggunakan mockito sebagai library mocking utama, yang sejak versi 5 menggunakan code generation untuk type safety penuh. Artikel ini membahas cara penggunaan modern mockito, perbedaan antara Mock, Fake, dan Stub, serta kapan sebaiknya tidak menggunakan mock.

Mock, Fake, Stub, dan Spy — Bedanya Apa? #

Keempat istilah ini sering digunakan bergantian tapi memiliki perbedaan penting:

Istilah Deskripsi Digunakan untuk
Mock Objek tiruan yang memverifikasi interaksi — method apa dipanggil, berapa kali, dengan argumen apa Verifikasi perilaku (behaviour testing)
Stub Objek tiruan yang hanya mengembalikan nilai tertentu tanpa memverifikasi pemanggilan Menyediakan data yang dikontrol
Fake Implementasi nyata tapi disederhanakan (misal: in-memory database) Ketika mock terlalu verbose
Spy Membungkus objek nyata dan merekam interaksinya Verifikasi pada objek nyata
// Stub — hanya kembalikan nilai, tidak verifikasi pemanggilan
when(mockRepo.ambilById('P001')).thenReturn(produkFix);

// Mock — kembalikan nilai DAN verifikasi pemanggilan
when(mockRepo.ambilById(any)).thenReturn(produkFix);
// ... kode yang diuji ...
verify(mockRepo.ambilById('P001')).called(1); // verifikasi interaksi

// Fake — implementasi nyata yang disederhanakan
class FakeProdukRepository implements ProdukRepository {
  final Map<String, Produk> _data = {};
  @override
  Future<Produk?> ambilById(String id) async => _data[id];
  @override
  Future<Produk> simpan(Produk p) async { _data[p.id] = p; return p; }
}

Setup Mockito Modern (Versi 5+) #

Mockito 5+ menggunakan code generation berbasis anotasi — lebih aman, lebih ekspresif, dan bekerja penuh dengan null safety:

# pubspec.yaml
dev_dependencies:
  mockito: ^5.4.0
  build_runner: ^2.4.0
  test: ^1.25.0

Langkah 1: Anotasi File Test #

// test/layanan/produk_service_test.dart
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:test/test.dart';
import 'package:aplikasi_toko/src/domain/repository/produk_repository.dart';
import 'package:aplikasi_toko/src/domain/layanan/produk_service.dart';

// Anotasi — generate mock untuk semua kelas yang didaftar
@GenerateMocks([ProdukRepository])
// Atau: @GenerateNiceMocks([MockSpec<ProdukRepository>()]) untuk nice mocks
void main() { /* ... */ }

Langkah 2: Generate Kode Mock #

# Generate file mock (hasilkan file *_test.mocks.dart)
dart run build_runner build

# Atau watch mode — auto-regenerate saat file berubah
dart run build_runner watch

File produk_service_test.mocks.dart akan dibuat otomatis dan berisi kelas MockProdukRepository.

Langkah 3: Gunakan Mock di Test #

// Import file yang di-generate
import 'produk_service_test.mocks.dart';

void main() {
  group('ProdukService', () {
    late MockProdukRepository mockRepo;
    late ProdukService service;

    setUp(() {
      mockRepo = MockProdukRepository();
      service = ProdukService(mockRepo);
    });

    test('ambilProduk mengembalikan produk yang ditemukan', () async {
      // Arrange
      final produkDiharapkan = Produk(
        id: 'P001',
        nama: 'Laptop',
        harga: 15_000_000,
      );

      when(mockRepo.ambilById('P001'))
          .thenAnswer((_) async => produkDiharapkan);

      // Act
      final hasil = await service.ambilProduk('P001');

      // Assert
      expect(hasil?.nama, equals('Laptop'));
      expect(hasil?.harga, equals(15_000_000));
    });
  });
}

when — Mengatur Perilaku Mock #

// thenReturn — nilai synchronous
when(mock.metodeSinkron()).thenReturn('nilai');

// thenAnswer — nilai asynchronous atau bergantung argumen
when(mock.metodAsync()).thenAnswer((_) async => 'nilai async');

// Menggunakan invocation untuk akses argumen
when(mock.cariProduk(any)).thenAnswer((invocation) async {
  final query = invocation.positionalArguments[0] as String;
  return query.isEmpty ? [] : [Produk(id: 'P1', nama: 'Hasil $query')];
});

// thenThrow — simulasi exception
when(mock.ambilById('TIDAK_ADA'))
    .thenThrow(NotFoundException('Produk tidak ditemukan'));

// thenReturnInOrder — urutan nilai berbeda per pemanggilan
when(mock.metodePanggilan())
    .thenReturnInOrder(['pertama', 'kedua', 'ketiga']);

// Berantai — panggil pertama return nilai, setelahnya throw
when(mock.metode())
    ..thenReturn('sukses')
    ..thenThrow(Exception('gagal setelahnya'));

Argument Matchers #

Argument matchers memungkinkan stubbing dan verifikasi tanpa peduli nilai argumen spesifik:

import 'package:mockito/mockito.dart';

// any — cocok dengan argumen apapun dari tipe yang diinferensikan
when(mock.cari(any)).thenReturn([]);
when(mock.buat(any, jumlah: anyNamed('jumlah'))).thenReturn(null);

// argThat — cocok berdasarkan kondisi kustom
when(mock.cari(argThat(startsWith('dart'))))
    .thenReturn([produkDart]);

when(mock.simpan(argThat(isA<Produk>().having(
  (p) => p.harga,
  'harga',
  greaterThan(0),
)))).thenAnswer((_) async => true);

// Named argument matchers
when(mock.cariDenganFilter(
  nama: anyNamed('nama'),
  hargaMaks: anyNamed('hargaMaks'),
)).thenReturn([]);

// captureAny — tangkap nilai argumen untuk diverifikasi
when(mock.kirimEmail(captureAny, captureAny))
    .thenAnswer((_) async {});
// ...setelah pemanggilan...
final captured = verify(mock.kirimEmail(captureAny, captureAny)).captured;
expect(captured[0], '[email protected]');  // argumen pertama
expect(captured[1], contains('terima kasih'));  // argumen kedua

verify — Memverifikasi Interaksi #

// Verifikasi dasar — method dipanggil sekali
verify(mock.ambilById('P001')).called(1);

// Dipanggil tepat N kali
verify(mock.kirimLog(any)).called(3);

// Dipanggil minimal N kali
verify(mock.catat(any)).called(greaterThanOrEqualTo(1));

// Tidak pernah dipanggil
verifyNever(mock.hapus(any));

// Verifikasi tidak ada interaksi sama sekali
verifyNoMoreInteractions(mock);
verifyZeroInteractions(mock); // tidak ada satupun method dipanggil

// Verifikasi urutan pemanggilan
verifyInOrder([
  mock.mulaiTransaksi(),
  mock.simpanOrder(any),
  mock.kirimEmail(any, any),
  mock.selesaikanTransaksi(),
]);

captureThat — Tangkap dan Verifikasi Argumen #

// Tangkap argumen yang dikirim ke mock untuk inspeksi lebih detail
test('layanan mengirim email dengan konten yang benar', () async {
  when(mockEmail.kirim(any, captureAny))
      .thenAnswer((_) async {});

  await service.prosesPendaftaran(pengguna);

  final isiEmail = verify(mockEmail.kirim(any, captureAny))
      .captured.single as String;

  expect(isiEmail, contains('Selamat bergabung'));
  expect(isiEmail, contains(pengguna.nama));
  expect(isiEmail, contains('Klik link berikut untuk verifikasi'));
});

@GenerateNiceMocks — Mock yang Lebih Lenient #

Mock biasa (dari @GenerateMocks) akan melempar MissingStubError jika method dipanggil tanpa stub terlebih dahulu. “Nice mock” mengembalikan nilai default (null, 0, false, []) tanpa throw:

// Mock biasa — strict, throw jika tidak ada stub
@GenerateMocks([ProdukRepository])

// Nice mock — lenient, kembalikan default jika tidak ada stub
@GenerateNiceMocks([MockSpec<ProdukRepository>()])

void main() {
  test('dengan nice mock', () {
    final mock = MockProdukRepository();
    // Tidak perlu stub semua method — yang tidak di-stub return null/default
    expect(mock.ambilById('P001'), completion(isNull)); // null tanpa stub
  });
}

Gunakan mock biasa untuk test yang perlu presisi — memastikan tidak ada method yang dipanggil secara tidak sengaja. Gunakan nice mock ketika hanya sebagian kecil method yang relevan untuk test tersebut.


Fake — Alternatif Mock yang Lebih Realistis #

Ketika mock menjadi terlalu verbose (terlalu banyak when stub), pertimbangkan Fake — implementasi nyata yang disederhanakan:

// Repository fake — in-memory, tidak perlu database nyata
class FakeProdukRepository implements ProdukRepository {
  final Map<String, Produk> _store = {};
  bool hapusDipanggil = false;

  @override
  Future<Produk?> ambilById(String id) async => _store[id];

  @override
  Future<List<Produk>> cariSemua() async => _store.values.toList();

  @override
  Future<Produk> simpan(Produk produk) async {
    _store[produk.id] = produk;
    return produk;
  }

  @override
  Future<void> hapus(String id) async {
    hapusDipanggil = true;
    _store.remove(id);
  }
}

// Penggunaan — tidak perlu stub, perilaku sudah terdefinisi
void main() {
  late FakeProdukRepository fakeRepo;
  late ProdukService service;

  setUp(() {
    fakeRepo = FakeProdukRepository();
    service = ProdukService(fakeRepo);
  });

  test('simpan dan ambil produk', () async {
    final produk = Produk(id: 'P001', nama: 'Laptop', harga: 15_000_000);

    await service.buatProduk(produk);
    final hasil = await service.ambilProduk('P001');

    expect(hasil?.nama, equals('Laptop'));
    // Tidak perlu verify — Fake sudah menyimpan state nyata
  });

  test('hapus produk menghapus dari store', () async {
    fakeRepo._store['P001'] = Produk(id: 'P001', nama: 'Test', harga: 0);
    await service.hapusProduk('P001');

    expect(fakeRepo._store.containsKey('P001'), isFalse);
    expect(fakeRepo.hapusDipanggil, isTrue);
  });
}

Skenario Mocking yang Umum #

Mocking HTTP Client #

@GenerateMocks([http.Client])
void main() {
  group('ProdukApiClient', () {
    late MockClient mockHttpClient;
    late ProdukApiClient client;

    setUp(() {
      mockHttpClient = MockClient();
      client = ProdukApiClient(httpClient: mockHttpClient);
    });

    test('GET /produk mengembalikan list produk', () async {
      final responseJson = jsonEncode([
        {'id': 'P1', 'nama': 'Laptop', 'harga': 15000000},
      ]);

      when(mockHttpClient.get(
        Uri.parse('https://api.example.com/produk'),
        headers: anyNamed('headers'),
      )).thenAnswer((_) async => http.Response(responseJson, 200));

      final produk = await client.ambilSemuaProduk();

      expect(produk, hasLength(1));
      expect(produk.first.nama, equals('Laptop'));
    });

    test('menangani HTTP 404 dengan benar', () async {
      when(mockHttpClient.get(any, headers: anyNamed('headers')))
          .thenAnswer((_) async => http.Response('Not Found', 404));

      expect(
        () async => await client.ambilProduk('TIDAK_ADA'),
        throwsA(isA<NotFoundException>()),
      );
    });

    test('menangani network error', () async {
      when(mockHttpClient.get(any, headers: anyNamed('headers')))
          .thenThrow(SocketException('Network unavailable'));

      expect(
        () async => await client.ambilSemuaProduk(),
        throwsA(isA<NetworkException>()),
      );
    });
  });
}

Mocking dengan Multiple Dependencies #

@GenerateMocks([ProdukRepository, EmailService, LogService])
void main() {
  group('OrderService', () {
    late MockProdukRepository mockProdukRepo;
    late MockEmailService mockEmail;
    late MockLogService mockLog;
    late OrderService service;

    setUp(() {
      mockProdukRepo = MockProdukRepository();
      mockEmail = MockEmailService();
      mockLog = MockLogService();
      service = OrderService(
        produkRepo: mockProdukRepo,
        emailService: mockEmail,
        logService: mockLog,
      );
    });

    test('buat order — sukses penuh', () async {
      // Arrange semua dependensi
      when(mockProdukRepo.ambilById('P001'))
          .thenAnswer((_) async => Produk(id: 'P001', stok: 10, harga: 100));
      when(mockEmail.kirim(any, any)).thenAnswer((_) async {});
      when(mockLog.catat(any)).thenReturn(null);

      // Act
      final order = await service.buatOrder('P001', qty: 2, emailPembeli: '[email protected]');

      // Assert hasil
      expect(order.total, equals(200));

      // Assert interaksi — urutan penting!
      verifyInOrder([
        mockProdukRepo.ambilById('P001'),
        mockEmail.kirim('[email protected]', any),
        mockLog.catat(any),
      ]);
    });
  });
}

Kapan Tidak Menggunakan Mock #

Over-mocking adalah anti-pattern yang membuat test rapuh dan sulit dipelihara. Beberapa panduan:

GUNAKAN mock ketika:
  ✓ Dependensi melibatkan I/O nyata (HTTP, database, filesystem)
  ✓ Dependensi memiliki efek samping (kirim email, charge kartu kredit)
  ✓ Dependensi lambat (sleep, timeout jaringan)
  ✓ Ingin mengontrol skenario error yang sulit direproduksi

JANGAN mock ketika:
  ✗ Logika murni (fungsi matematika, transformasi data) — test langsung
  ✗ Value object sederhana — buat instance nyata
  ✗ Kode yang bisa diuji dengan Fake yang lebih sederhana
  ✗ Setiap dependensi secara reflex — ini over-mocking
// ANTI-PATTERN: mock untuk logika murni yang tidak perlu mock
test('hitung diskon', () {
  final mockKalkulasi = MockKalkulasiDiskon();
  when(mockKalkulasi.hitung(100, 10)).thenReturn(10.0);
  // ✗ mengapa perlu mock? fungsi ini deterministik dan tidak ada I/O

  expect(mockKalkulasi.hitung(100, 10), 10.0);
});

// BENAR: test langsung tanpa mock
test('hitung diskon', () {
  final kalkulasi = KalkulasiDiskon();
  expect(kalkulasi.hitung(harga: 100, persen: 10), equals(10.0));
});

Ringkasan #

  • Mockito 5+ menggunakan code generation — anotasi @GenerateMocks([KelasA, KelasB]) lalu dart run build_runner build menghasilkan file *_test.mocks.dart dengan type safety penuh.
  • Mock vs Fake — gunakan Mock untuk memverifikasi interaksi (method apa dipanggil), Fake untuk implementasi in-memory yang lebih realistis dan tidak butuh banyak stub.
  • @GenerateNiceMocks menghasilkan mock yang lenient (kembalikan null/default tanpa stub) — berguna ketika hanya sebagian method yang relevan untuk test.
  • Argument matchers (any, argThat, anyNamed) memungkinkan stub yang fleksibel tanpa peduli nilai argumen persis — sangat berguna untuk argumen kompleks.
  • captureAny dan captureThat merekam argumen aktual yang dikirim ke mock — berguna untuk verifikasi konten yang dikirim (isi email, payload API).
  • verifyInOrder memastikan method dipanggil dalam urutan yang benar — penting untuk memverifikasi alur bisnis yang punya dependensi urutan.
  • verifyNever dan verifyNoMoreInteractions memastikan tidak ada interaksi yang tidak diharapkan — berguna untuk memverifikasi bahwa path error tidak melakukan operasi yang seharusnya tidak dilakukan.
  • Jangan mock logika murni — fungsi deterministik tanpa I/O atau efek samping tidak perlu di-mock. Test langsung dengan input dan output nyata jauh lebih bermakna.
  • thenAnswer dengan akses invocation memungkinkan response dinamis berdasarkan argumen yang diterima — berguna untuk mock yang berperilaku berbeda tergantung input.
  • Over-mocking membuat test rapuh — test yang penuh verify untuk setiap interaksi akan patah setiap kali implementasi direfaktor, meski perilaku eksternal tidak berubah.

← Sebelumnya: Unit Test   Berikutnya: JSON →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact