Saat ini, manfaat menulis unit test sangat besar. Saya rasa sebagian besar proyek yang baru dimulai berisi pengujian unit apa pun. Dalam aplikasi perusahaan dengan banyak logika bisnis, pengujian unit adalah pengujian yang paling penting, karena pengujian tersebut cepat dan dapat langsung memastikan bahwa penerapan kami sudah benar. Namun, saya sering melihat masalah dengan pengujian yang baik dalam proyek, meskipun manfaat pengujian ini hanya besar jika Anda memiliki pengujian unit yang baik. Jadi dalam contoh ini, saya akan mencoba membagikan beberapa tips tentang apa yang harus dilakukan untuk menulis unit test yang baik.
Versi yang mudah dibaca: https://testing-tips.sarvendev.com/
Kamil Ruczyński
Blog: https://sarvendev.com/
LinkedIn: https://www.linkedin.com/in/kamilruczynski/
Dukungan Anda sangat berarti bagi saya! Jika Anda menikmati panduan ini dan menemukan nilai dari pengetahuan yang dibagikan, pertimbangkan untuk mendukung saya di BuyMeCoffee:
atau cukup tinggalkan bintang di repositori dan ikuti saya di Twitter dan Github untuk mengetahui semua pembaruan. Kemurahan hati Anda mengobarkan semangat saya untuk membuat konten yang lebih berwawasan luas untuk Anda.
Jika Anda memiliki ide perbaikan atau topik untuk ditulis, silakan siapkan permintaan penarikan atau beri tahu saya.
Berlangganan dan kuasai pengujian unit dengan eBuku GRATIS saya!
Detail
Saya masih memiliki daftar TODO yang cukup panjang untuk perbaikan pada panduan tentang Pengujian Unit ini dan saya akan memperkenalkannya dalam waktu dekat.
Perkenalan
Pengarang
Tesnya berlipat ganda
Penamaan
pola AAA
Objek ibu
Pembangun
Menegaskan objek
Tes berparameter
Dua sekolah pengujian unit
Klasik
mengejek
Ketergantungan
Mock vs Stub
Tiga gaya pengujian unit
Keluaran
Negara
Komunikasi
Arsitektur dan pengujian fungsional
Perilaku yang dapat diamati vs detail implementasi
Satuan perilaku
Pola yang rendah hati
Tes sepele
Tes yang rapuh
Perlengkapan tes
Pengujian umum anti-pola
Mengekspos swasta negara
Membocorkan detail domain
Mengejek kelas konkret
Menguji metode pribadi
Waktu sebagai ketergantungan yang mudah berubah
Cakupan Tes 100% seharusnya tidak menjadi tujuan
Buku yang direkomendasikan
Uji ganda adalah dependensi palsu yang digunakan dalam pengujian.
Dummy hanyalah implementasi sederhana yang tidak melakukan apa pun.
kelas terakhir Mailer mengimplementasikan MailerInterface {fungsi publik kirim(Pesan $pesan): batal{ } }
Yang palsu adalah implementasi yang disederhanakan untuk mensimulasikan perilaku aslinya.
kelas terakhir InMemoryCustomerRepository mengimplementasikan CustomerRepositoryInterface {/** * @var Pelanggan[] */array pribadi $pelanggan;fungsi publik __construct() {$ini->pelanggan = []; }penyimpanan fungsi publik(Pelanggan $pelanggan): void{$ini->pelanggan[(string) $pelanggan->id()->id()] = $pelanggan; }fungsi publik get(CustomerId $id): Pelanggan{if (!isset($this->customers[(string) $id->id()])) {throw new CustomerNotFoundException(); }kembalikan $ini->pelanggan[(string) $id->id()]; }fungsi publik findByEmail(Email $email): Pelanggan{foreach ($this->customers as $customer) {if ($customer->getEmail()->isEqual($email)) {return $customer; } }melemparkan CustomerNotFoundException(); } }
Stub adalah implementasi paling sederhana dengan perilaku hardcode.
kelas terakhir UniqueEmailSpecificationStub mengimplementasikan UniqueEmailSpecificationInterface {fungsi publik isUnique(Email $email): bool{return true; } }
$spesifikasiStub = $ini->createStub(UniqueEmailSpecificationInterface::class);$spesifikasiStub->metode('isUnique')->willReturn(true);
Mata-mata adalah implementasi untuk memverifikasi perilaku tertentu.
kelas terakhir Mailer mengimplementasikan MailerInterface {/** * @var Pesan[] */array pribadi $pesan; fungsi publik __konstruksi() {$ini->pesan = []; }fungsi publik kirim(Pesan $pesan): void{$ini->pesan[] = $pesan; }fungsi publik getCountOfSentMessages(): int{return count($this->messages); } }
Tiruan adalah tiruan yang dikonfigurasikan untuk memverifikasi panggilan pada kolaborator.
$message = Pesan baru('[email protected]', 'Test', 'Test test test');$mailer = $this->createMock(MailerInterface::class);$mailer->expects($this-> sekali()) ->metode('kirim') ->dengan($ini->sama dengan($pesan));
[!ATTENTION] Untuk memverifikasi interaksi masuk, gunakan stub, tetapi untuk memverifikasi interaksi keluar, gunakan tiruan.
Lebih lanjut: Mock vs Stub
[!PERINGATAN|gaya:datar|label:TIDAK BAIK]
kelas terakhir TestExample memperluas TestCase {/** * @test */fungsi publik sends_all_notifications(): void{$message1 = new Message();$message2 = new Message();$messageRepository = $this->createMock(MessageRepositoryInterface::class);$messageRepository ->method('getAll')->willReturn([$message1, $message2]);$mailer = $this->createMock(MailerInterface::class);$sut = new NotificationService($mailer, $messageRepository);$mailer->expects(self::exactly(2))->method('send') ->withConsecutive([self::equalTo($message1)], [self::equalTo($message2)]);$sut->send(); } }
[!TIP|gaya:datar|label:LEBIH BAIK]
Resistensi yang lebih baik terhadap refactoring
Menggunakan Refactor->Rename pada metode tertentu tidak merusak pengujian
Keterbacaan yang lebih baik
Biaya pemeliharaan yang lebih rendah
Tidak perlu mempelajari kerangka kerja tiruan yang canggih itu
Hanya kode PHP biasa sederhana
kelas terakhir TestExample memperluas TestCase {/** * @test */fungsi publik sends_all_notifications(): void{$message1 = Pesan baru();$message2 = Pesan baru();$messageRepository = InMemoryMessageRepository();$messageRepository->save($message1) baru ;$messageRepository->save($message2);$mailer = SpyMailer baru();$sut = baru Layanan Pemberitahuan($mailer, $messageRepository);$sut->kirim(); $mailer->assertThatMessagesHaveBeenSent([$message1, $message2]); } }
[!PERINGATAN|gaya:datar|label:TIDAK BAIK]
tes fungsi publik(): void{$subscription = SubscriptionMother::new();$subscription->activate();self::assertSame(Status::activated(), $subscription->status()); }
[!TIP|style:flat|label:Tentukan secara eksplisit apa yang Anda uji]
fungsi publik sut(): void{// sut = Sistem di bawah test$sut = SubscriptionMother::new();$sut->activate();self::assertSame(Status::activated(), $sut->status ()); }
[!PERINGATAN|gaya:datar|label:TIDAK BAIK]
fungsi publik it_throws_invalid_credentials_Exception_when_sign_in_with_invalid_credentials(): void{ }uji fungsi publikCreatingWithATooShortPasswordIsNotPossible(): void{ }fungsi publik testDeactivateASubscription(): void{ }
[!TIP|gaya:datar|label:LEBIH BAIK]
Menggunakan garis bawah meningkatkan keterbacaan
Nama harus menggambarkan perilakunya, bukan implementasinya
Gunakan nama tanpa kata kunci teknis. Ini harus dapat dibaca oleh orang non-programmer.
fungsi publik sign_in_with_invalid_credentials_is_not_possible(): void{ }pembuatan fungsi publik_with_a_too_short_password_is_not_possible(): void{ }fungsi publik menonaktifkan_an_activated_subscription_is_valid(): void{ }fungsi publik menonaktifkan_an_inactive_subscription_is_invalid(): void{ }
Catatan
Mendeskripsikan perilaku itu penting dalam menguji skenario domain. Jika kode Anda hanyalah kode utilitas, itu kurang penting.
Mengapa berguna bagi non-programmer untuk membaca unit test?
Jika ada proyek dengan logika domain yang kompleks, logika ini harus sangat jelas bagi semua orang, sehingga pengujian menjelaskan detail domain tanpa kata kunci teknis, dan Anda dapat berbicara dengan bisnis dalam bahasa seperti dalam pengujian ini. Semua kode yang terkait dengan domain harus bebas dari detail teknis. Seorang non-programmer tidak akan membaca tes ini. Jika Anda ingin berbicara tentang domain, tes ini akan berguna untuk mengetahui apa yang dilakukan domain ini. Akan ada deskripsi tanpa rincian teknis misalnya, mengembalikan null, memunculkan pengecualian, dll. Informasi semacam ini tidak ada hubungannya dengan domain, jadi kita tidak boleh menggunakan kata kunci ini.
Ini juga umum Diberikan, Kapan, Lalu.
Pisahkan tiga bagian tes:
Arrange : Membawa sistem yang diuji ke kondisi yang diinginkan. Siapkan dependensi, argumen, dan terakhir buat SUT.
Tindakan : Memanggil elemen yang diuji.
Assert : Verifikasi hasil, keadaan akhir, atau komunikasi dengan kolaborator.
[!TIP|gaya:datar|label:BAIK]
fungsi publik aaa_pattern_example_test(): void{//Arrange|Given$sut = SubscriptionMother::new();//Act|When$sut->activate();//Assert|Thenself::assertSame(Status::activated( ), $sut->status()); }
Pola ini membantu membuat objek tertentu yang dapat digunakan kembali dalam beberapa pengujian. Oleh karena itu bagian penyusunannya ringkas dan tes secara keseluruhan lebih mudah dibaca.
Langganan kelas akhirMother {fungsi statis publik baru(): Langganan{kembalikan Langganan baru(); }fungsi statis publik diaktifkan(): Langganan{$langganan = Langganan baru();$langganan->aktifkan();return $langganan; }fungsi statis publik dinonaktifkan(): Langganan{$langganan = self::activated();$subscription->deactivate();return $subscription; } }
kelas terakhir Contoh Tes {fungsi publik example_test_with_activated_subscription(): void{$activatedSubscription = SubscriptionMother::activated();// melakukan sesuatu// memeriksa sesuatu}fungsi publik example_test_with_deactivated_subscription(): void{$deactivatedSubscription = SubscriptionMother::deactivated();// melakukan sesuatu // periksa sesuatu} }
Builder adalah pola lain yang membantu kita membuat objek dalam pengujian. Dibandingkan dengan Object Mother pattern Builder lebih baik untuk membuat objek yang lebih kompleks.
kelas terakhir OrderBuilder {private DateTimeImmutable|null $createdAt = null;/** * @var OrderItem[] */array pribadi $items = [];fungsi publik createAt(DateTimeImmutable $createdAt): self{$this->createdAt = $createdAt;return $ini; }fungsi publik withItem(string $name, int $price): self{$this->items[] = new OrderItem($name, $price);return $this; }build fungsi publik(): Pesan{ Tegaskan::notEmpty($this->items);return Pesanan baru($this->createdAt ?? new DateTimeImmutable(),$this->items, ); } }
kelas terakhir ContohTest memperluas TestCase {/** * @test */fungsi publik example_test_with_order_builder(): void{$order = (OrderBuilder() baru) ->createdAt(DateTimeImmutable baru('2022-11-10 20:00:00')) ->denganItem('Item 1', 1000) ->denganItem('Item 2', 2000) ->denganItem('Item 3', 3000) ->build();// melakukan sesuatu// memeriksa sesuatu} }
Pola objek penegasan membantu menulis bagian penegasan yang lebih mudah dibaca. Daripada menggunakan beberapa pernyataan, kita cukup menyiapkan abstraksi, dan menggunakan bahasa alami untuk menggambarkan hasil yang diharapkan.
kelas terakhir ContohTest memperluas TestCase {/** * @test */fungsi publik example_test_with_asserter(): void{$currentTime = new DateTimeImmutable('2022-11-10 20:00:00');$sut = new OrderService();$order = $sut ->buat($Waktu Saat Ini); OrderAsserter::assertThat($pesanan) ->adalahDibuatPada($Waktu Saat Ini) ->memilikiTotal(6000); } }
gunakan PHPUnitFrameworkAssert; kelas terakhir OrderAsserter {fungsi publik __construct(Pesanan $pesanan hanya baca pribadi) {}fungsi statis publik tegaskan(Pesanan $pesanan): self{return new OrderAsserter($order); }fungsi publik wasCreatedAt(DateTimeImmutable $createdAt): mandiri{ Tegaskan::assertEquals($createdAt, $this->order->createdAt);return $this; }fungsi publik hasTotal(int $total): mandiri{ Tegaskan::assertSame($total, $this->order->getTotal());return $this; } }
Pengujian berparameter adalah pilihan yang baik untuk menguji SUT dengan banyak parameter tanpa mengulangi kode.
Peringatan
Tes semacam ini kurang mudah dibaca. Untuk sedikit meningkatkan keterbacaan, contoh negatif dan positif harus dipisahkan ke dalam pengujian yang berbeda.
kelas terakhir ContohTest memperluas TestCase {/** * @test * @dataProvider getInvalidEmails */fungsi publik mendeteksi_an_invalid_email_address(string $email): void{$sut = new EmailValidator();$result = $sut->isValid($email);self::assertFalse( $hasil); }/** * @test * @dataProvider getValidEmails */fungsi publik mendeteksi_an_valid_email_address(string $email): void{$sut = new EmailValidator();$result = $sut->isValid($email);self::assertTrue( $hasil); }fungsi publik getInvalidEmails(): iterable{yield 'Email tidak valid tanpa @' => ['test'];yield 'Email tidak valid tanpa domain setelah @' => ['test@'];yield 'Email tidak valid tanpa TLD' => ['test@test'];//...}fungsi publik getValidEmails(): iterable{hasil 'Email valid dengan huruf kecil' => ['[email protected]'];yield 'Email valid dengan huruf kecil dan angka' => ['[email protected]'];yield 'Email valid dengan huruf besar dan angka' => ['Test123@ test.com'];//...} }
Catatan
Gunakan yield
dan tambahkan deskripsi teks ke kasus untuk meningkatkan keterbacaan.
Unitnya adalah satu unit perilaku, bisa berupa beberapa kelas yang terkait.
Setiap tes harus diisolasi dari yang lain. Jadi harus dimungkinkan untuk menjalankannya secara paralel atau dalam urutan apa pun.
kelas terakhir TestExample memperluas TestCase {/** * @test */fungsi publik suspending_an_subscription_with_can_always_suspend_policy_is_always_possible(): void{$canAlwaysSuspendPolicy = new CanAlwaysSuspendPolicy();$sut = new Subscription();$result = $sut->suspend($canAlwaysSuspendPolicy);self::assertTrue($result);self::assertSame(Status::suspend(), $sut->status()); } }
Unitnya adalah satu kelas.
Unit harus diisolasi dari semua kolaborator.
kelas terakhir TestExample memperluas TestCase {/** * @test */fungsi publik suspending_an_subscription_with_can_always_suspend_policy_is_always_possible(): void{$canAlwaysSuspendPolicy = $this->createStub(SuspendingPolicyInterface::class);$canAlwaysSuspendPolicy->method('suspend')->willReturn(true);$ sut = Langganan baru();$hasil = $sut->suspend($canAlwaysSuspendPolicy);self::assertTrue($result);self::assertSame(Status::suspend(), $sut->status()); } }
Catatan
Pendekatan klasik lebih baik untuk menghindari pengujian yang rapuh.
[TODO]
Contoh:
Layanan Notifikasi kelas terakhir {public function __construct(private readonly MailerInterface $mailer,private readonly MessageRepositoryInterface $messageRepository) {}fungsi publik send(): void{$messages = $this->messageRepository->getAll();foreach ($messages as $message) { $ini->mailer->kirim($pesan); } } }
[!PERINGATAN|gaya:datar|label:BURUK]
Menegaskan interaksi dengan stub akan menghasilkan pengujian yang rapuh
kelas terakhir TestExample memperluas TestCase {/** * @test */fungsi publik sends_all_notifications(): void{$message1 = new Message();$message2 = new Message();$messageRepository = $this->createMock(MessageRepositoryInterface::class);$messageRepository ->method('getAll')->willReturn([$message1, $message2]);$mailer = $this->createMock(MailerInterface::class);$sut = new NotificationService($mailer, $messageRepository);$messageRepository->expects(self::once())->method('getAll');$mailer- >mengharapkan(diri::tepat(2))->metode('kirim') ->withConsecutive([self::equalTo($message1)], [self::equalTo($message2)]);$sut->send(); } }
[!TIP|gaya:datar|label:BAIK]
kelas terakhir TestExample memperluas TestCase {/** * @test */fungsi publik sends_all_notifications(): void{$message1 = Pesan baru();$message2 = Pesan baru();$messageRepository = InMemoryMessageRepository();$messageRepository->save($message1) baru ;$messageRepository->simpan($message2);$mailer = $this->createMock(MailerInterface::class);$sut = new NotificationService($mailer, $messageRepository);// Menghapus interaksi yang menegaskan dengan metode stub$mailer->expects(self::exactly(2))-> ('mengirim') ->withConsecutive([self::equalTo($message1)], [self::equalTo($message2)]);$sut->send(); } }
[!TIP|style:flat|label: LEBIH BAIK MENGGUNAKAN SPY]
kelas terakhir TestExample memperluas TestCase {/** * @test */fungsi publik sends_all_notifications(): void{$message1 = Pesan baru();$message2 = Pesan baru();$messageRepository = InMemoryMessageRepository();$messageRepository->save($message1) baru ;$messageRepository->save($message2);$mailer = SpyMailer baru();$sut = baru Layanan Pemberitahuan($mailer, $messageRepository);$sut->kirim(); $mailer->assertThatMessagesHaveBeenSent([$message1, $message2]); } }
[!TIP|gaya:datar|label:Pilihan terbaik]
Resistensi terbaik terhadap refactoring
Akurasi terbaik
Biaya pemeliharaan terendah
Jika memungkinkan, Anda sebaiknya memilih tes semacam ini
kelas terakhir ContohTest memperluas TestCase {/** * @test * @dataProvider getInvalidEmails */fungsi publik mendeteksi_an_invalid_email_address(string $email): void{$sut = new EmailValidator();$result = $sut->isValid($email);self::assertFalse( $hasil); }/** * @test * @dataProvider getValidEmails */fungsi publik mendeteksi_an_valid_email_address(string $email): void{$sut = new EmailValidator();$result = $sut->isValid($email);self::assertTrue( $hasil); }fungsi publik getInvalidEmails(): array{return [ ['tes'], ['tes@'], ['tes tes'],//...]; }fungsi publik getValidEmails(): array{return [ ['[email protected]'], ['[email protected]'], ['[email protected]'],//...]; } }
[!PERINGATAN|gaya:datar|label:Opsi yang lebih buruk]
Resistensi yang lebih buruk terhadap pemfaktoran ulang
Akurasi yang lebih buruk
Biaya pemeliharaan yang lebih tinggi
kelas terakhir ContohTest memperluas TestCase {/** * @test */fungsi publik menambahkan_an_item_to_cart(): void{$item = new CartItem('Product');$sut = new Cart();$sut->addItem($item);self::assertSame (1, $sut->getCount());self::assertSame($item, $sut->getItems()[0]); } }
[!PERHATIAN|gaya:datar|label:Pilihan terburuk]
Resistensi terburuk terhadap refactoring
Akurasi terburuk
Biaya pemeliharaan tertinggi
kelas terakhir ContohTest memperluas TestCase {/** * @test */fungsi publik sends_all_notifications(): void{$message1 = Pesan baru();$message2 = Pesan baru();$messageRepository = InMemoryMessageRepository();$messageRepository->save($message1) baru ;$messageRepository->simpan($message2);$mailer = $this->createMock(MailerInterface::class);$sut = new NotificationService($mailer, $messageRepository);$mailer->expects(self::exactly(2))->method('send') ->withConsecutive([self::equalTo($message1)], [self::equalTo($message2)]);$sut->send(); } }
[!PERINGATAN|gaya:datar|label:BURUK]
kelas terakhir NameService {fungsi publik __construct(hanya baca pribadi CacheStorageInterface $cacheStorage) {}fungsi publik loadAll(): void{$namesCsv = array_map('str_getcsv', file(__DIR__.'/../names.csv'));$names = [ ];foreach ($namesCsv sebagai $nameData) {if (!isset($nameData[0], $namaData[1])) {lanjutkan; }$nama[] = Nama baru($namaData[0], Jenis Kelamin baru($namaData[1])); }$ini->cacheStorage->store('nama', $nama); } }
Bagaimana cara menguji kode seperti ini? Hal ini hanya mungkin dilakukan dengan pengujian integrasi karena pengujian ini secara langsung menggunakan kode infrastruktur yang terkait dengan sistem file.
[!TIP|gaya:datar|label:BAIK]
Seperti dalam arsitektur fungsional, kita perlu memisahkan kode yang memiliki efek samping dan kode yang hanya berisi logika.
kelas terakhir NameParser {/** * @param array<string[]> $namesData * @return Name[] */public function parse(array $namesData): array{$names = [];foreach ($namesData sebagai $nameData) {if (!isset($nameData[0], $nameData[1])) {lanjutkan; }$nama[] = Nama baru($namaData[0], Jenis Kelamin baru($namaData[1])); }kembalikan $nama; } }
kelas terakhir CsvNamesFileLoader {fungsi publik memuat(): array{return array_map('str_getcsv', file(__DIR__.'/../names.csv')); } }
ApplicationService kelas terakhir {fungsi publik __construct(hanya baca pribadi CsvNamesFileLoader $fileLoader, hanya baca pribadi NameParser $parser, hanya baca pribadi CacheStorageInterface $cacheStorage) {}fungsi publik loadNames(): void{$namesData = $this->fileLoader->load();$names = $ini->parser->parse($namesData);$ini->cacheStorage->store('nama', $nama); } }
kelas terakhir ValidUnitExampleTest memperluas TestCase {/** * @test */fungsi publik parse_all_names(): void{$namesData = [ ['John', 'M'], ['Lennon', 'U'], ['Sarah', 'W'] ];$sut = new NameParser();$hasil = $sut->parse($namesData); diri::menegaskanSama( [Nama baru('John', Gender baru('M')),Nama baru('Lennon', Gender baru('U')),Nama baru('Sarah', Gender baru('W')) ],$hasil); } }
[!PERINGATAN|gaya:datar|label:BURUK]
ApplicationService kelas terakhir {fungsi publik __construct(hanya baca pribadi SubscriptionRepositoryInterface $subscriptionRepository) {}fungsi publik renewSubscription(int $subscriptionId): bool{$subscription = $this->subscriptionRepository->findById($subscriptionId);if (!$subscription->getStatus() ->isEqual(Status::kedaluwarsa())) {kembali salah; }$langganan->setStatus(Status::aktif());$langganan->setModifiedAt(new DateTimeImmutable());return true; } }
Langganan kelas terakhir {fungsi publik __construct(Status pribadi $status, DateTimeImmutable $modifiedAt pribadi) {}fungsi publik getStatus(): Status{return $this->status; }fungsi publik setStatus(Status $status): void{$this->status = $status; }fungsi publik getModifiedAt(): DateTimeImmutable{return $this->modifiedAt; }fungsi publik setModifiedAt(DateTimeImmutable $modifiedAt): void{$this->modifiedAt = $modifiedAt; } }
kelas terakhir InvalidTestExample memperluas TestCase {/** * @test */fungsi publik renew_an_expired_subscription_is_possible(): void{$modifiedAt = new DateTimeImmutable();$expiredSubscription = Langganan baru(Status::expired(), $modifiedAt);$sut = new ApplicationService($this ->createRepository($expiredSubscription));$hasil = $sut->renewSubscription(1);self::assertSame(Status::active(), $expiredSubscription->getStatus());self::assertGreaterThan($modifiedAt, $expiredSubscription->getModifiedAt());self:: menegaskanBenar($hasil); }/** * @test */fungsi publik renew_an_active_subscription_is_not_possible(): void{$modifiedAt = new DateTimeImmutable();$activeSubscription = Langganan baru(Status::active(), $modifiedAt);$sut = new ApplicationService($this ->createRepository($activeSubscription));$hasil = $sut->renewSubscription(1);self::assertSame($modifiedAt, $activeSubscription->getModifiedAt());self::assertFalse($result); } fungsi pribadi createRepository(Subscription $subscription): SubscriptionRepositoryInterface{return new class ($expiredSubscription) mengimplementasikan SubscriptionRepositoryInterface {public function __construct(private readonly Subscription $subscription) {} fungsi publik findById(int $id): Langganan{return $this->langganan; } }; } }
[!TIP|gaya:datar|label:BAIK]
ApplicationService kelas terakhir {fungsi publik __construct(hanya baca pribadi SubscriptionRepositoryInterface $subscriptionRepository) {}fungsi publik renewSubscription(int $subscriptionId): bool{$subscription = $this->subscriptionRepository->findById($subscriptionId);return $subscription->renew(new DateTimeImmutable( )); } }
Langganan kelas terakhir {Status pribadi $status; DateTimeImmutable $modifiedAt;fungsi publik __construct(DateTimeImmutable $modifiedAt) pribadi {$ini->status = Status::baru();$ini->modifiedAt = $modifiedAt; }pembaruan fungsi publik(DateTimeImmutable $modifiedAt): bool{if (!$this->status->isEqual(Status::expired())) {return false; }$ini->status = Status::aktif();$ini->modifiedAt = $modifiedAt;return true; }fungsi publik aktif(DateTimeImmutable $modifiedAt): void{//sederhana$this->status = Status::active();$this->modifiedAt = $modifiedAt; }fungsi publik kedaluwarsa(DateTimeImmutable $modifiedAt): void{//simplement$this->status = Status::expired();$this->modifiedAt = $modifiedAt; }fungsi publik isActive(): bool{return $this->status->isEqual(Status::active()); } }
kelas terakhir ValidTestExample memperluas TestCase {/** * @test */fungsi publik renew_an_expired_subscription_is_possible(): void{$expiredSubscription = SubscriptionMother::expired();$sut = new ApplicationService($this->createRepository($expiredSubscription));$result = $sut- >renewSubscription(1);// lewati pemeriksaan ModifiedAt karena ini bukan bagian dari perilaku yang dapat diamati. Untuk memeriksa nilai ini kita// harus menambahkan pengambil untuk ModifiedAt, mungkin hanya untuk tujuan pengujian.self::assertTrue($expiredSubscription->isActive());self::assertTrue($result); }/** * @test */fungsi publik renew_an_active_subscription_is_not_possible(): void{$activeSubscription = SubscriptionMother::active();$sut = new ApplicationService($this->createRepository($activeSubscription));$result = $sut->renewSubscription(1);self::assertTrue($activeSubscription->isActive());self::assertFalse($result); } fungsi pribadi createRepository(Subscription $subscription): SubscriptionRepositoryInterface{return new class ($expiredSubscription) mengimplementasikan SubscriptionRepositoryInterface {public function __construct(private readonly Subscription $subscription) {} fungsi publik findById(int $id): Langganan{return $this->langganan; } }; } }
Catatan
Model berlangganan pertama memiliki desain yang buruk. Untuk menjalankan satu operasi bisnis, Anda perlu memanggil tiga metode. Juga menggunakan pengambil untuk memverifikasi operasi bukanlah praktik yang baik. Dalam hal ini, pemeriksaan perubahan modifiedAt
dilewati, mungkin pengaturan modifiedAt
tertentu selama operasi pembaruan dapat diuji dengan operasi bisnis kedaluwarsa. Pengambil untuk modifiedAt
tidak diperlukan. Tentu saja, ada kalanya menemukan kemungkinan untuk menghindari getter yang disediakan hanya untuk pengujian akan sangat sulit, namun kita harus selalu berusaha untuk tidak memperkenalkannya.
[!PERINGATAN|gaya:datar|label:BURUK]
kelas CannotSuspendExpiredSubscriptionPolicy mengimplementasikan SuspendingPolicyInterface {penangguhan fungsi publik(Langganan $langganan, DateTimeImmutable $at): bool{if ($langganan->isExpired()) {return false; }mengembalikan nilai benar; } }
kelas CannotSuspendExpiredSubscriptionPolicyTest memperluas TestCase {/** * @test */fungsi publik it_returns_false_when_a_subscription_is_expired(): void{$policy = new CannotSuspendExpiredSubscriptionPolicy();$subscription = $this->createStub(Subscription::class);$subscription->method('isExpired')->willReturn(true);self::assertFalse($policy->suspend($subscription, new DateTimeImmutable())); }/** * @test */fungsi publik it_returns_true_when_a_subscription_is_not_expired(): void{$policy = new CannotSuspendExpiredSubscriptionPolicy();$subscription = $this->createStub(Subscription::class);$subscription->method('isExpired')->willReturn(false);self::assertTrue($policy->suspend($subscription, new DateTimeImmutable())); } }
kelas CannotSuspendNewSubscriptionPolicy mengimplementasikan SuspendingPolicyInterface {penangguhan fungsi publik(Langganan $langganan, DateTimeImmutable $at): bool{if ($langganan->isNew()) {return false; }mengembalikan nilai benar; } }
kelas CannotSuspendNewSubscriptionPolicyTest memperluas TestCase {/** * @test */fungsi publik it_returns_false_when_a_subscription_is_new(): void{$policy = new CannotSuspendNewSubscriptionPolicy();$subscription = $this->createStub(Subscription::class);$subscription->method('isNew') ->willReturn(true);self::assertFalse($policy->suspend($langganan, baru DateTimeImmutable())); }/** * @test */fungsi publik it_returns_true_when_a_subscription_is_not_new(): void{$policy = new CannotSuspendNewSubscriptionPolicy();$subscription = $this->createStub(Subscription::class);$subscription->method('isNew') ->willReturn(false);self::assertTrue($policy->suspend($subscription, new DateTimeImmutable())); } }
kelas CanSuspendAfterOneMonthPolicy mengimplementasikan SuspendingPolicyInterface {penangguhan fungsi publik(Langganan $langganan, DateTimeImmutable $at): bool{$oneMonthEarlierDate = DateTime::createFromImmutable($at)->sub(new DateInterval('P1M'));return $subscription->isOlderThan(DateTimeImmutable:: createFromMutable($oneMonthEarlierDate)); } }
kelas CanSuspendAfterOneMonthPolicyTest memperluas TestCase {/** * @test */fungsi publik it_returns_true_when_a_subscription_is_older_than_one_month(): void{$date = new DateTimeImmutable('2021-01-29');$policy = new CanSuspendAfterOneMonthPolicy();$subscription = Langganan baru(baru DateTimeImmutable('28-12-2020'));self::assertTrue($policy->suspend($subscription, $date)); }/** * @test */fungsi publik it_returns_false_when_a_subscription_is_not_older_than_one_month(): void{$date = new DateTimeImmutable('2021-01-29');$policy = new CanSuspendAfterOneMonthPolicy();$subscription = Langganan baru(baru DateTimeImmutable('01-01-2020'));self::assertTrue($policy->suspend($subscription, $date)); } }
Status kelas {private const EXPIRED = 'kedaluwarsa';private const ACTIVE = 'aktif';private const NEW = 'baru';private const SUSPENDED = 'ditangguhkan';fungsi pribadi __construct(string readonly pribadi $status) {$ini->status = $status; }fungsi statis publik kadaluwarsa(): self{return new self(self::EXPIRED); }fungsi statis publik aktif(): self{return new self(self::ACTIVE); }fungsi statis publik baru(): self{return new self(self::NEW); }fungsi statis publik ditangguhkan(): self{return new self(self::SUSPENDED); }fungsi publik isEqual(self $status): bool{return $this->status === $status->status; } }
kelas StatusTest memperluas TestCase {fungsi publik testEquals(): void{$status1 = Status::active();$status2 = Status::active();self::assertTrue($status1->isEqual($status2)); }fungsi publik testNotEquals(): void{$status1 = Status::active();$status2 = Status::expired();self::assertFalse($status1->isEqual($status2)); } }
kelas SubscriptionTest memperluas TestCase {/** * @test */fungsi publik suspending_a_subscription_is_possible_when_a_policy_returns_true(): void{$policy = $this->createMock(SuspendingPolicyInterface::class);$policy->expects($this->once())->method( 'menangguhkan')->willReturn(true);$sut = Langganan baru(baru DateTimeImmutable());$result = $sut->suspend($policy, new DateTimeImmutable());self::assertTrue($result);self::assertTrue($sut->isSuspended()); }/** * @test */fungsi publik suspending_a_subscription_is_not_possible_when_a_policy_returns_false(): void{$policy = $this->createMock(SuspendingPolicyInterface::class);$policy->expects($this->once())->method( 'menangguhkan')->willReturn(false);$sut = Langganan baru(baru DateTimeImmutable());$result = $sut->suspend($policy, new DateTimeImmutable());self::assertFalse($result);self::assertFalse($sut->isSuspended()); }/** * @test */fungsi publik it_returns_true_when_a_subscription_is_older_than_one_month(): void{$date = new DateTimeImmutable();$futureDate = $date->add(new DateInterval('P1M'));$sut = Langganan baru($ tanggal);self::assertTrue($sut->isOlderThan($futureDate)); }/** * @test */fungsi publik it_returns_false_when_a_subscription_is_not_older_than_one_month(): void{$date = new DateTimeImmutable();$futureDate = $date->add(new DateInterval('P1D'));$sut = Langganan baru($ tanggal);self::assertTrue($sut->isOlderThan($futureDate)); } }
[!PERHATIAN] Jangan menulis kode 1:1, 1 kelas : 1 tes. Hal ini mengarah pada pengujian yang rapuh sehingga membuat pemfaktoran ulang menjadi sulit.
[!TIP|gaya:datar|label:BAIK]
kelas terakhir CannotSuspendExpiredSubscriptionPolicy mengimplementasikan SuspendingPolicyInterface {penangguhan fungsi publik(Langganan $langganan, DateTimeImmutable $at): bool{if ($langganan->isExpired()) {return false; }mengembalikan nilai benar; } }
kelas terakhir CannotSuspendNewSubscriptionPolicy mengimplementasikan SuspendingPolicyInterface {penangguhan fungsi publik(Langganan $langganan, DateTimeImmutable $at): bool{if ($langganan->isNew()) {return false; }mengembalikan nilai benar; } }
kelas terakhir CanSuspendAfterOneMonthPolicy mengimplementasikan SuspendingPolicyInterface {penangguhan fungsi publik(Langganan $langganan, DateTimeImmutable $at): bool{$oneMonthEarlierDate = DateTime::createFromImmutable($at)->sub(new DateInterval('P1M'));return $subscription->isOlderThan(DateTimeImmutable:: createFromMutable($oneMonthEarlierDate)); } }
Status kelas akhir {private const EXPIRED = 'kedaluwarsa';private const ACTIVE = 'aktif';private const NEW = 'baru';private const SUSPENDED = 'ditangguhkan';fungsi pribadi __construct(string readonly pribadi $status) {$ini->status = $status; }fungsi statis publik kadaluwarsa(): self{return new self(self::EXPIRED); }fungsi statis publik aktif(): self{return new self(self::ACTIVE); }fungsi statis publik baru(): self{return new self(self::NEW); }fungsi statis publik ditangguhkan(): self{return new self(self::SUSPENDED); }fungsi publik isEqual(self $status): bool{return $this->status === $status->status; } }
Langganan kelas terakhir {Status pribadi $status; DateTimeImmutable pribadi $createdAt; fungsi publik __construct(DateTimeImmutable $createdAt) {$ini->status = Status::baru();$ini->dibuatAt = $dibuatDi; }fungsi publik menangguhkan(SuspendingPolicyInterface $suspendingPolicy, DateTimeImmutable $at): bool{$result = $suspendingPolicy->suspend($this, $at);if ($result) {$this->status = Status::suspended() ; }mengembalikan $hasil; }fungsi publik isOlderThan(DateTimeImmutable $date): bool{return $this->createdAt < $date; }fungsi publik aktifkan(): void{$this->status = Status::aktif(); }fungsi publik expired(): void{$this->status = Status::expired(); }fungsi publik isExpired(): bool{return $this->status->isEqual(Status::expired()); }fungsi publik isActive(): bool{return $this->status->isEqual(Status::active()); }fungsi publik isNew(): bool{return $this->status->isEqual(Status::new()); }fungsi publik isSuspended(): bool{return $this->status->isEqual(Status::suspended()); } }
kelas terakhir SubscriptionSuspendingTest memperluas TestCase {/** * @test */fungsi publik suspending_an_expired_subscription_with_cannot_suspend_expired_policy_is_not_possible(): void{$sut = Langganan baru(dateTimeImmutable());$sut->activate();$sut->expire();$result = $sut ->menangguhkan(baru CannotSuspendExpiredSubscriptionPolicy(), baru DateTimeImmutable());self::assertFalse($result); }/** * @test */fungsi publik suspending_a_new_subscription_with_cannot_suspend_new_policy_is_not_possible(): void{$sut = Langganan baru(new DateTimeImmutable());$result = $sut->suspend(new CannotSuspendNewSubscriptionPolicy(), new DateTimeImmutable());self ::assertFalse($hasil); }/** * @test */fungsi publik suspending_an_active_subscription_with_cannot_suspend_new_policy_is_possible(): void{$sut = Langganan baru(New DateTimeImmutable());$sut->activate();$result = $sut->suspend(new CannotSuspendNewSubscriptionPolicy() , new DateTimeImmutable());self::assertTrue($result); }/** * @test */fungsi publik suspending_an_active_subscription_with_cannot_suspend_expired_policy_is_possible(): void{$sut = Langganan baru(New DateTimeImmutable());$sut->activate();$result = $sut->suspend(new CannotSuspendExpiredSubscriptionPolicy() , baru DateTimeImmutable());self::assertTrue($result); }/** * @test */fungsi publik suspending_an_subscription_before_a_one_month_is_not_possible(): void{$sut = Langganan baru(New DateTimeImmutable('2020-01-01'));$result = $sut->suspend(new CanSuspendAfterOneMonthPolicy(), baru DateTimeImmutable('10-01-2020'));self::assertFalse($result); }/** * @test */fungsi publik suspending_an_subscription_after_a_one_month_is_possible(): void{$sut = Langganan baru(New DateTimeImmutable('2020-01-01'));$result = $sut->suspend(new CanSuspendAfterOneMonthPolicy(), baru DateTimeImmutable('2020-02-02'));self::assertTrue($result); } }
Bagaimana cara menguji unit kelas seperti ini dengan benar?
kelas Layanan Aplikasi {fungsi publik __construct(OrderRepository readonly pribadi $orderRepository, FormRepository readonly pribadi $formRepository) {}fungsi publik changeFormStatus(int $orderId): void{$order = $this->orderRepository->getById($orderId);$soapResponse = $ ini->getSoapClient()->getStatusByOrderId($orderId);$form = $ini->formRepository->getByOrderId($orderId);$form->setStatus($soapResponse['status']);$form->setModifiedAt(new DateTimeImmutable());if ($soapResponse['status'] === 'diterima') {$order->setStatus('dibayar'); }$ini->formRepository->save($form);$ini->orderRepository->save($order); }fungsi pribadi getSoapClient(): SoapClient{return new SoapClient('https://legacy_system.pl/Soap/WebService', []); } }
[!TIP|gaya:datar|label:BAIK]
Diperlukan untuk membagi kode yang terlalu rumit ke kelas-kelas yang terpisah.
ApplicationService kelas terakhir {fungsi publik __construct(hanya baca pribadi OrderRepositoryInterface $orderRepository, hanya baca pribadi FormRepositoryInterface $formRepository, hanya baca pribadi FormApiInterface $formApi, hanya baca pribadi ChangeFormStatusService $changeFormStatusService) {}fungsi publik changeFormStatus(int $orderId): void{$order = $this->orderRepository->getById($orderId);$form = $this->formRepository->getByOrderId($orderId);$status = $this->formApi->getStatusByOrderId($orderId);$this->changeFormStatusService ->perubahanStatus($pesanan, $bentuk, $status);$ini->formRepository->save($form);$ini->orderRepository->save($order); } }
kelas terakhir ChangeFormStatusService {fungsi publik changeStatus(Pesanan $order, Formulir $form, string $formStatus): void{$status = FormStatus::createFromString($formStatus);$form->changeStatus($status);if ($form->isAccepted( )) {$order->changeStatus(OrderStatus::paid()); } } }
kelas terakhir ChangingFormStatusTest memperluas TestCase {/** * @test */fungsi publik changing_a_form_status_to_accepted_changes_an_order_status_to_paid(): void{$order = Pesanan baru();$form = Formulir baru();$status = 'diterima';$sut = baru ChangeFormStatusService();$sut ->perubahanStatus($pesanan, $bentuk, $status);self::assertTrue($form->isAccepted());self::assertTrue($order->isPaid()); }/** * @test */fungsi publik mengubah_a_form_status_to_refused_not_changes_an_order_status(): void{$order = Pesanan baru();$form = Formulir baru();$status = 'baru';$sut = baru ChangeFormStatusService();$sut ->perubahanStatus($pesanan, $bentuk, $status);self::assertFalse($form->isAccepted());self::assertFalse($order->isPaid()); } }
Namun, ApplicationService mungkin harus diuji dengan pengujian integrasi hanya dengan FormApiInterface yang diolok-olok.
[!PERINGATAN|gaya:datar|label:BURUK]
Pelanggan kelas akhir {fungsi publik __construct(string pribadi $nama) {}fungsi publik getName(): string{return $this->name; }fungsi publik setName(string $nama): void{$ini->nama = $nama; } }
kelas terakhir CustomerTest memperluas TestCase {fungsi publik testSetName(): void{$customer = new Customer('Jack');$customer->setName('John');self::assertSame('John', $customer->getName()); } }
EventSubscriber kelas terakhir {fungsi statis publik getSubscribedEvents(): array{return ['event' => 'onEvent']; }fungsi publik padaEvent(): void{ } }
kelas terakhir EventSubscriberTest memperluas TestCase {fungsi publik testGetSubscribedEvents(): void{$result = EventSubscriber::getSubscribedEvents();self::assertSame(['event' => 'onEvent'], $result); } }
[!ATTENTION] Menguji kode tanpa logika yang rumit tidak ada gunanya, tetapi juga mengarah pada pengujian yang rapuh.
[!PERINGATAN|gaya:datar|label:BURUK]
UserRepository kelas terakhir {fungsi publik __construct(Koneksi readonly pribadi $koneksi) {}fungsi publik getUserNameByEmail(string $email): ?array{return $this->koneksi->createQueryBuilder() ->dari('pengguna', 'kamu') ->di mana('u.email = :email') ->setParameter('email', $email) -> jalankan() ->ambil(); } }
kelas terakhir TestUserRepository memperluas TestCase {fungsi publik testGetUserNameByEmail(): void{$email = '[email protected]';$koneksi = $this->createMock(Connection::class);$queryBuilder = $this->createMock(QueryBuilder::class); $hasil = $ini->createMock(ResultStatement::class);$userRepository = baru UserRepository($koneksi);$koneksi->mengharapkan($ini->sekali()) ->metode('buatQueryBuilder') ->willReturn($queryBuilder);$queryBuilder->mengharapkan($ini->sekali()) ->metode('dari') ->dengan('pengguna', 'kamu') ->willReturn($queryBuilder);$queryBuilder->mengharapkan($ini->sekali()) ->metode('di mana') ->dengan('u.email = :email') ->willReturn($queryBuilder);$queryBuilder->mengharapkan($ini->sekali()) ->metode('setParameter') ->dengan('email', $email) ->willReturn($queryBuilder);$queryBuilder->mengharapkan($ini->sekali()) ->metode('eksekusi') -> WillReturn ($ hasil); $ result-> hargai ($ this-> sekali ()) -> Metode ('fetch') -> WillReturn (['email' => $ email]); $ result = $ userrepository-> getUserNameByemail ($ email); self :: assertsame (['email' => $ email], $ hasil); } }
[!PERHATIAN] Menguji repositori dengan cara seperti itu akan menghasilkan pengujian yang rapuh dan kemudian pemfaktoran ulang menjadi sulit. Untuk menguji repositori, tulis tes integrasi.
[!TIP|gaya:datar|label:BAIK]
Goodtest kelas akhir memperluas testcase {Private SaspliptionFactory $ sut; pengaturan fungsi publik (): void {$ this-> sut = new SubscriptionFactory (); }/** * @test */fungsi publik creates_a_subscription_for_a_given_date_range (): void {$ result = $ this-> sut-> create (datetimeimmutable (), datetimeimmutable baru ('sekarang +1 tahun'); self :: assertinstanceof (Langganan :: Kelas, $ hasil); }/** * @test */fungsi publik throws_an_exception_on_invalid_date_range (): void {$ this-> hare conseption (createSubscriptionException :: class); $ result = $ this-> sut-> create (datetimeimmutable baru ('sekarang -1 tahun'), datetimeimmutable baru ()); } }
Catatan
Kasus terbaik untuk menggunakan metode setUp adalah menguji objek tanpa kewarganegaraan.
Konfigurasi apa pun yang dibuat di dalam setUp
memasangkan pengujian bersama-sama, dan berdampak pada semua pengujian.
Lebih baik menghindari keadaan bersama di antara pengujian dan mengonfigurasi keadaan awal sesuai dengan metode pengujian.
Keterbacaan lebih buruk dibandingkan konfigurasi yang dilakukan dengan metode pengujian yang tepat.
[!TIP|gaya:datar|label:LEBIH BAIK]
Kelas terakhir BetterTest memperluas testcase {/** * @test */public function suspending_an_active_subscription_with_cannot_suspend_new_policy_is_possible(): void{$sut = $this->createAnActiveSubscription();$result = $sut->suspend(new CannotSuspendNewSubscriptionPolicy(), new DateTimeImmutable());self: : asserttrue ($ hasil); }/** * @test */public function suspending_an_active_subscription_with_cannot_suspend_expired_policy_is_possible(): void{$sut = $this->createAnActiveSubscription();$result = $sut->suspend(new CannotSuspendExpiredSubscriptionPolicy(), new DateTimeImmutable());self: : asserttrue ($ hasil); }/** * @test */fungsi publik suspending_a_new_subscription_with_cannot_suspend_new_policy_is_not_possible (): void {$ sut = $ this-> createanewsubscription (); $ result = $ sut-> SUSPEND (baru cannotsuspend); : AssertFalse ($ hasil); } function private createAnewSubscription (): langganan {return baru berlangganan (datetimeimmutable ()); } fungsi private createeanactivesubscription (): berlangganan {$ berlangganan = berlangganan baru (datetimeimmutable ()); $ langganan-> activate (); mengembalikan $ berlangganan; } }
Catatan
Pendekatan ini meningkatkan keterbacaan dan memperjelas pemisahan (kode lebih banyak dibaca daripada ditulis).
Pembantu swasta bisa jadi membosankan untuk digunakan dalam setiap metode pengujian, meskipun mereka memberikan maksud yang jelas.
Untuk berbagi objek pengujian serupa antara beberapa kelas pengujian, gunakan:
Objek ibu
Pembangun
[!PERINGATAN|gaya:datar|label:BURUK]
Pelanggan Kelas Terakhir {Private CustomerType $ type; Private DiscountCalCulationPolicyInterface $ DiscountCalCulationPolicy; Fungsi Publik __Construct () {$ this-> type = customerType :: normal (); $ this-> diskonCalCulationPolicy = new NormalDiscountPolicy (); } fungsi publik makevip (): void {$ this-> type = customerType :: vip (); $ this-> diskonCalculationPolicy = vipdiscountpolicy baru (); } fungsi publik getCustomerType (): customerType {return $ this-> type; } fungsi publik getPerCentageScount (): int {return $ this-> diskonCalculationPolicy-> getPerCentageScount (); } }
Kelas Akhir Invalidtest memperluas testcase {fungsi publik testmakevip (): void {$ sut = pelanggan baru (); $ sut-> makevip (); self :: assertsame (customerType :: vip (), $ sut-> getCustomerType ()); } }
[!TIP|gaya:datar|label:BAIK]
Pelanggan Kelas Terakhir {Private CustomerType $ type; Private DiscountCalCulationPolicyInterface $ DiscountCalCulationPolicy; Fungsi Publik __Construct () {$ this-> type = customerType :: normal (); $ this-> diskonCalCulationPolicy = new NormalDiscountPolicy (); } fungsi publik makevip (): void {$ this-> type = customerType :: vip (); $ this-> diskonCalculationPolicy = vipdiscountpolicy baru (); } fungsi publik getPerCentageScount (): int {return $ this-> diskonCalculationPolicy-> getPerCentageScount (); } }
Kelas akhir validtest memperluas testcase {/** * @test */fungsi publik a_vip_customer_has_a_25_percentage_discount (): void {$ sut = pelanggan baru (); $ sut-> makevip (); self :: assertsame (25, $ sut-> getPerCentageCount (); } }
[!ATTENTION] Menambahkan kode produksi tambahan (misalnya pengambil getCustomerType()) hanya untuk memverifikasi status dalam pengujian adalah praktik yang buruk. Ini harus diverifikasi oleh nilai signifikan domain lain (dalam hal ini getPercentageDiscount()). Tentu saja, terkadang sulit menemukan cara lain untuk memverifikasi operasi, dan kita terpaksa menambahkan kode produksi tambahan untuk memverifikasi kebenaran dalam pengujian, namun kita harus mencoba menghindarinya.
DiscountCalculator Kelas Akhir {Fungsi publik Hitung (int $ isVipFromyears): int { Assert :: GreaterThaneq ($ Isvipfromyears, 0); return min (($ isVipfromyears * 10) + 3, 80); } }
[!PERINGATAN|gaya:datar|label:BURUK]
Kelas Akhir Invalidtest memperluas testcase {/** * @DataProvider DiskonDataProvider */Fungsi publik TestCalculate (int $ vipdaysfrom, int $ diharapkan): void {$ sut = Diskon baru ($ vipdaysfrom) ); } fungsi publik DiskonDataProvider (): array {return [ [0, 0 * 10 + 3], // detail domain bocor [1, 1 * 10 + 3], [5, 5 * 10 + 3], [8, 80] ]; } }
[!TIP|gaya:datar|label:BAIK]
Kelas akhir validtest memperluas testcase {/** * @DataProvider DiskonDataProvider */Fungsi publik TestCalculate (int $ vipdaysfrom, int $ diharapkan): void {$ sut = Diskon baru ($ vipdaysfrom) ); } fungsi publik DiskonDataProvider (): array {return [ [0, 3], [1, 13], [5, 53], [8, 80] ]; } }
Catatan
Jangan menduplikasi logika produksi dalam pengujian. Cukup verifikasi hasil dengan nilai hardcode.
[!PERINGATAN|gaya:datar|label:BURUK]
Class DiscountCalculator {Fungsi publik CalculateInternalDiscount (int $ isVipfromyears): int { Assert :: GreaterThaneq ($ Isvipfromyears, 0); return min (($ isVipfromyears * 10) + 3, 80); } fungsi publik CalculateAdditionalDiscountFromExternalSystem (): int {// Dapatkan data dari sistem eksternal untuk menghitung diskountreturn 5; } }
Class OrderService {Fungsi Publik __Construct (Private Readonly DiscountCalculator $ DiskonCalculator) {} Fungsi publik getTotalPriceWithDiscount (int $ totalPrice, int $ vipfromdays): int {$ internaldiscount = $ this-> diskon $eskulator-> calculateIndiscount ($ vipfromday); diskon $ nexculator-> calculatelcount ($ vipfromday); diskon; $ this-> DiskonCalculator-> CalculateAdditionDiscountFromExternalSySyStem (); $ diskon = $ internalDiscount + $ externaldiscount; return $ totalPrice-(int) ceil (($ totalPrice * $ diskon) / 100); } }
Kelas Akhir Invalidtest memperluas testcase {/** * @dataProvider orderDataProvider */fungsi publik testgetTotalPriceWithDiscount (int $ totalPrice, int $ vipdaysfrom, int $ diharapkan): void {$ diskonCalculator = $ this-> createPartialMock (DiskonCalculator :: class, ['CalculateAdditionDiscountFromExternalSyStem']); $ DiscountCalculator-> Metode ('CalculateAdditionDiscountFromExternalSyStem')-> WillReturn (5); $ SUT = PESANAN BARU ($ DISCOUNCLOCULATE); self :: AssertSame ($ yang diharapkan, $ tut, $ scalculator); self :: assertsame ($ yang diharapkan, $ sut , $ vipdaysfrom)); } fungsi publik orderDataProvider (): array {return [ [1000, 0, 920], [500, 1, 410], [644, 5, 270], ]; } }
[!TIP|gaya:datar|label:BAIK]
Antarmuka ExtericalDiscountCalculatorInterface {Fungsi Publik Hitung (): int; }
Kelas akhir internalcountcalculator {Fungsi publik Hitung (int $ isVipFromyears): int { Assert :: GreaterThaneq ($ Isvipfromyears, 0); return min (($ isVipfromyears * 10) + 3, 80); } }
Layanan Pesanan Kelas Akhir {Fungsi Publik __Construct (Private Readonly InternalDiscountCalculator $ DiscountCalculator, Private Readonly ExternalDiscountCalculatorInterface $ ExternalDiscountCalculator) {} Fungsi Publik GetTOTPriceWithDiscount (Int $ TOOPRICE, INT $ VIPFROMDAYS): Int {$ internal) (Int $ TOOPRICE, INT $ VIPFROMDYS): Int {$ Internal $ this-> DiskonCalculator-> CHOCKULASI ($ VIPFROMDAYS); $ ExternalDiscount = $ this-> ExternalDiscountCalculator-> CHOTULATE (); $ DISCURNSUM = $ internalDiscount + $ ExternalDiscount; kembalikan $ total harga-(int) ($ TotalPrice * $ $ $ $ $ diskon) / 100); } }
Kelas akhir validtest memperluas testcase {/** * @dataProvider orderDataProvider */public function testGetTotalPriceWithDiscount(int $totalPrice, int $vipDaysFrom, int $expected): void{$externalDiscountCalculator = new class() implements ExternalDiscountCalculatorInterface {public function calculate(): int{return 5; } }; $ sut = New OrderService (new InternaldiscountCalculator (), $ externaldiscountCalculator); self :: assertsame ($ diharapkan, $ sut-> getTotalPriceWithDiscount ($ totalprice, $ vipdaysfrom)); } fungsi publik orderDataProvider (): array {return [ [1000, 0, 920], [500, 1, 410], [644, 5, 270], ]; } }
Catatan
Keharusan untuk mengolok-olok kelas konkrit untuk menggantikan sebagian dari perilakunya berarti bahwa kelas ini mungkin terlalu rumit dan melanggar Prinsip Tanggung Jawab Tunggal.
orderitem kelas akhir {Fungsi publik __construct (public readonly int $ total) {} }
pesanan kelas terakhir {/** * @param orderitem [] $ item * @param int $ transportCost */fungsi publik __construct (private array $ item, private int $ transportCost) {} fungsi publik getTotal (): int {return $ this-> getItemstotal () + $ this-> transportCost; } fungsi pribadi getItemStotal (): int {return array_reduce (array_map (fn (pesanan $ item) => $ item-> total, $ this-> item), fn (int $ sum, int $ total) = $ sum + = $ total, 0); } }
[!PERINGATAN|gaya:datar|label:BURUK]
Kelas Akhir Invalidtest memperluas testcase {/** * @test * @DataProvider ordersDataProvider */fungsi publik get_total_returns_a_total_cost_of_a_whole_order (pesanan $ order, int $ order-> gettotal): batal {self :: assertsame ($ diharapkan, $ order-> gettotal): void {self:) ($ diharapkan, $ order-> gettotal (void {);); }/** * @test * @dataProvider orderItemsDataProvider */fungsi publik get_items_total_returns_a_total_cost_of_all_items (pesanan $ order, int $ happectotal): void {self :: assertsame ($ diharapkan, $ ini-> invokePetape (void :: assertsame ($ diharapkan, $ this-> } public function ordersdataProvider (): array {return [ [Orde Baru ([New OrderItem (20), New OrderItem (20), New OrderItem (20)], 15), 75], [Orde Baru ([New OrderItem (20), New OrderItem (30), New OrderItem (40)], 0), 90], [Orde Baru ([New OrderItem (99), New OrderItem (99), New OrderItem (99)], 9), 306] ]; } fungsi publik orderItemDataProvider (): array {return [ [Orde Baru ([New OrderItem (20), New OrderItem (20), New OrderItem (20)], 15), 60], [Orde Baru ([New OrderItem (20), New OrderItem (30), New OrderItem (40)], 0), 90], [Orde Baru ([New OrderItem (99), New OrderItem (99), New OrderItem (99)], 9), 297] ]; } fungsi pribadi InvokePrivateMethodGetItemStotal (pesanan & $ order): int {$ reflection = new reflectionClass (get_class ($ order)); $ method = $ reflection-> getMethod ('getItemstotal'); $ method-> setAccessible (true); return); return $ Method-> InvokeARGS ($ order, []); } }
[!TIP|gaya:datar|label:BAIK]
Kelas akhir validtest memperluas testcase {/** * @test * @DataProvider ordersDataProvider */fungsi publik get_total_returns_a_total_cost_of_a_whole_order (pesanan $ order, int $ order-> gettotal): batal {self :: assertsame ($ diharapkan, $ order-> gettotal): void {self:) ($ diharapkan, $ order-> gettotal (void {);); } public function ordersdataProvider (): array {return [ [Orde Baru ([New OrderItem (20), New OrderItem (20), New OrderItem (20)], 15), 75], [Orde Baru ([New OrderItem (20), New OrderItem (30), New OrderItem (40)], 0), 90], [Orde Baru ([New OrderItem (99), New OrderItem (99), New OrderItem (99)], 9), 306] ]; } }
[!ATTENTION] Pengujian seharusnya hanya memverifikasi API publik.
Waktu merupakan ketergantungan yang fluktuatif karena bersifat non-deterministik. Setiap pemanggilan mengembalikan hasil yang berbeda.
[!PERINGATAN|gaya:datar|label:BURUK]
jam kelas terakhir {public static DateTime | null $ currentDateTime = null; fungsi statis publik getCurrentDateTime (): datetime {if (null === self :: $ currentDateTime) {self :: $ currentDateTime = new DateTime (); } return self :: $ currentDateTime; } set fungsi statis publik (datetime $ datetime): void {self :: $ currentDateTime = $ datetime; } public static function reset (): void {self :: $ currentDateTime = null; } }
Pelanggan Kelas Terakhir {private DateTime $ createTat; fungsi publik __construct () {$ this-> createTat = clock :: getCurrentDateTime (); } fungsi publik isvip (): bool {return $ this-> createTat-> diff (clock :: getCurrentDateTime ())-> y> = 1; } }
Kelas Akhir Invalidtest memperluas testcase {/** * @test */fungsi publik a_customer_registered_more_than_a_one_year_ago_is_a_vip (): void { Jam :: set (datetime baru ('2019-01-01')); $ sut = pelanggan baru (); Jam :: reset (); // Anda harus ingat tentang mengatur ulang negara bagian bersama :: asserttrue ($ sut-> isvip ()); }/** * @test */fungsi publik a_customer_registered_less_than_a_one_year_ago_is_not_a_vip (): void { Jam :: set ((datetime baru ())-> sub (new DateInterval ('p2m')))); $ sut = pelanggan baru (); Jam :: reset (); // Anda harus ingat tentang mengatur ulang negara bagian bersama :: AssertFalse ($ sut-> isvip ()); } }
[!TIP|gaya:datar|label:BAIK]
antarmuka clockinterface {fungsi publik getCurrentTime (): datetimeimmutable; }
Jam kelas akhir mengimplementasikan clockinterface {Fungsi pribadi __construct () { } public static function create (): self {return new new self (); } fungsi publik getCurrentTime (): datetimeimmutable {return new DateTimeImMutable (); } }
Kelas Terakhir FixedClock mengimplementasikan clockinterface {function pribadi __construct (private readonly datetimeimmutable $ fixedDate) {} function statis public create (datetimeimmutable $ fixedDate): self {return new self ($ fixedDate); } fungsi publik getCurrentTime (): datetimeimmutable {return $ this-> fixedDate; } }
Pelanggan Kelas Terakhir {Fungsi publik __construct (private readonly datetimeimmutable $ createdat) {} fungsi publik isVIP (datetimeimmutable $ currentDate): bool {return $ this-> createTat-> diff ($ currentDate)-> y> = 1; } }
Kelas akhir validtest memperluas testcase {/** * @test */fungsi publik a_customer_registered_more_than_a_one_year_ago_is_a_vip (): void {$ sut = Pelanggan baru (fixedclock :: create (datetimeimmutable ('2019-01-01'))-> getCurrenttime ()); AssertTrue ($ sut-> isVIP (FixedClock :: create (baru Datetimeimmutable ('2020-01-02'))-> getCurrentTime ())); }/** * @test */fungsi publik a_customer_registered_less_than_a_one_year_ago_is_not_a_vip (): void {$ sut = Pelanggan baru (FixedClock :: create (baru DateTimeImMutable ('2019-01-01'))-> getCurrentTime ()); self :: assertFalse ($ sut-> isvip (fixedclock :: create (datetimeimmutable ('2019-05-02'))-> getCurrentTime ('2019-05-02'))-> getCurrentTime ( ))); } }
Catatan
Waktu dan nomor acak tidak boleh dihasilkan secara langsung dalam kode domain. Untuk menguji perilaku kita harus mendapatkan hasil deterministik, jadi kita perlu memasukkan nilai-nilai ini ke dalam objek domain seperti pada contoh di atas.
Cakupan 100% bukanlah tujuan atau bahkan tidak diinginkan karena jika ada cakupan 100%, pengujian mungkin akan sangat rapuh, yang berarti pemfaktoran ulang akan sangat sulit. Pengujian mutasi memberikan umpan balik yang lebih baik tentang kualitas pengujian. Baca selengkapnya
Pengembangan Test Driven: Dengan contoh / Kent Beck - klasik
Prinsip, Praktik, dan Pola Pengujian Unit / Vladimir Khorikov - buku terbaik tentang tes yang pernah saya baca
Kamil Ruczyński
Twitter: https://twitter.com/Sarvendev
Blog: https://sarvendev.com/
LinkedIn: https://www.linkedin.com/in/kamilruczynski/