File tunggal atau banyak untuk unit yang menguji satu kelas?

20

Dalam meneliti unit pengujian praktik terbaik untuk membantu menyusun pedoman untuk organisasi saya, saya telah mengalami pertanyaan apakah lebih baik atau berguna untuk memisahkan perlengkapan pengujian (kelas uji) atau untuk menyimpan semua tes untuk satu kelas dalam satu file.

Namun, saya mengacu pada "tes unit" dalam arti murni bahwa itu adalah tes kotak putih yang menargetkan kelas tunggal, satu pernyataan per tes, semua dependensi diejek, dll.

Skenario contoh adalah kelas (sebut saja Dokumen) yang memiliki dua metode: CheckIn dan CheckOut. Setiap metode mengimplementasikan berbagai aturan, dll. Yang mengontrol perilaku mereka. Mengikuti aturan satu-pernyataan-per-tes, saya akan memiliki beberapa tes untuk setiap metode. Saya bisa menempatkan semua tes dalam satu DocumentTestskelas dengan nama seperti CheckInShouldThrowExceptionWhenUserIsUnauthorizeddan CheckOutShouldThrowExceptionWhenUserIsUnauthorized.

Atau, saya dapat memiliki dua kelas tes terpisah: CheckInShoulddan CheckOutShould. Dalam hal ini, nama pengujian saya akan disingkat tetapi akan diatur sehingga semua tes untuk perilaku (metode) tertentu bersamaan.

Saya yakin ada pro dan kontra untuk pendekatan baik dan saya bertanya-tanya apakah ada yang telah mengambil rute dengan beberapa file dan, jika demikian, mengapa? Atau, jika Anda memilih pendekatan file tunggal, mengapa Anda merasa lebih baik?

SonOfPirate
sumber
2
Nama metode pengujian Anda terlalu panjang. Sederhanakan, bahkan jika itu berarti suatu metode pengujian akan mengandung banyak pernyataan.
Bernard
12
@Bernard: dianggap praktik buruk untuk memiliki beberapa pernyataan per metode, namun nama metode uji panjang TIDAK dianggap praktik buruk. Kami biasanya mendokumentasikan apa metode pengujian dalam nama itu sendiri. Misalnya constructor_nullSomeList (), setUserName_UserNameIsNotValid () dll ...
c_maker
4
@Bernard: saya menggunakan nama tes yang panjang (dan hanya di sana!) Juga. Saya suka mereka, karena sangat jelas apa yang tidak berfungsi (jika nama Anda pilihan yang baik;)).
Sebastian Bauer
Pernyataan berulang bukanlah praktik yang buruk, jika semuanya menguji hasil yang sama. Misalnya. kamu tidak akan punya testResponseContainsSuccessTrue(), testResponseContainsMyData()dan testResponseStatusCodeIsOk(). Anda akan memiliki mereka dalam satu testResponse()yang telah tiga menegaskan: assertEquals(200, response.status), assertEquals({"data": "mydata"}, response.data)danassertEquals(true, response.success)
Juha Untinen

Jawaban:

18

Ini jarang terjadi, tetapi terkadang masuk akal untuk memiliki beberapa kelas tes untuk kelas tertentu yang sedang diuji. Biasanya saya akan melakukan ini ketika pengaturan yang berbeda diperlukan, dan dibagikan di seluruh bagian dari tes.

Carl Manaster
sumber
3
Contoh yang baik mengapa pendekatan itu disajikan kepada saya di tempat pertama. Sebagai contoh, ketika saya ingin menguji metode CheckIn, saya selalu ingin objek yang sedang diuji diatur satu arah tetapi ketika saya menguji metode CheckOut, saya memerlukan pengaturan berbeda. Poin bagus.
SonOfPirate
14

Tidak dapat benar-benar melihat alasan kuat mengapa Anda akan membagi tes untuk satu kelas menjadi beberapa kelas uji. Karena ide mengemudi harusnya mempertahankan kohesi pada tingkat kelas, Anda juga harus berusaha keras untuk itu pada tingkat ujian. Hanya beberapa alasan acak:

  1. Tidak perlu menggandakan (dan memelihara beberapa versi) kode pengaturan
  2. Lebih mudah untuk menjalankan semua tes untuk kelas dari IDE jika mereka dikelompokkan dalam satu kelas uji
  3. Pemecahan masalah yang lebih mudah dengan pemetaan satu-ke-satu antara kelas uji dan kelas.
pap
sumber
Poin bagus. Jika kelas yang diuji adalah kludge yang mengerikan, saya tidak akan mengesampingkan beberapa kelas tes, di mana beberapa di antaranya berisi logika pembantu. Saya hanya tidak suka aturan yang sangat kaku. Selain itu, poin bagus.
Pekerjaan
1
Saya akan menghilangkan kode penyiapan duplikat dengan memiliki kelas dasar umum untuk perlengkapan uji yang bekerja terhadap satu kelas. Tidak yakin saya setuju dengan dua poin lainnya sama sekali.
SonOfPirate
Di JUnit misalnya Anda mungkin ingin menguji sesuatu menggunakan Runner berbeda yang mengarah pada kebutuhan untuk kelas yang berbeda.
Joachim Nilsson
Ketika saya menulis kelas dengan sejumlah besar metode dengan matematika kompleks di masing-masing, saya menemukan saya memiliki 85 tes dan hanya tes tertulis untuk 24% dari metode sejauh ini. Saya menemukan diri saya di sini karena saya bertanya-tanya apakah ini gila untuk melakukan sejumlah besar pengujian ke dalam beberapa kategori yang terpisah sehingga saya dapat menjalankan subset.
Mooing Duck
7

Jika Anda dipaksa untuk membagi tes unit untuk kelas di beberapa file, itu mungkin merupakan indikasi bahwa kelas itu sendiri dirancang dengan buruk. Saya tidak dapat memikirkan skenario mana pun yang lebih bermanfaat untuk membagi tes unit untuk kelas yang cukup mematuhi Prinsip Tanggung Jawab Tunggal dan praktik terbaik pemrograman lainnya.

Selain itu, memiliki nama metode yang lebih panjang dalam pengujian unit dapat diterima, tetapi jika itu mengganggu Anda, Anda selalu dapat memikirkan kembali konvensi penamaan tes unit Anda untuk mempersingkat nama.

FishBasketGordo
sumber
Sayangnya, di dunia nyata tidak semua kelas mematuhi SRP et al ... dan ini menjadi masalah ketika Anda menguji kode warisan unit.
Péter Török
3
Tidak yakin bagaimana SRP terkait di sini. Saya memiliki banyak metode di kelas saya dan saya perlu menulis unit test untuk semua persyaratan berbeda yang mendorong perilaku mereka. Apakah lebih baik memiliki satu kelas tes dengan 50 metode tes atau 5 kelas tes dengan masing-masing 10 tes yang berhubungan dengan metode tertentu di kelas yang diuji?
SonOfPirate
2
@SonOfPirate: Jika Anda membutuhkan begitu banyak tes, itu mungkin berarti metode Anda melakukan terlalu banyak. Jadi SRP sangat relevan di sini. Jika Anda menerapkan SRP ke kelas serta metode, Anda akan memiliki banyak kelas kecil dan metode yang mudah untuk diuji dan Anda tidak akan membutuhkan begitu banyak metode pengujian. (Tapi saya setuju dengan Péter Török bahwa ketika Anda menguji kode warisan, praktik-praktik yang baik ada di luar pintu dan Anda hanya perlu melakukan yang terbaik yang dapat Anda lakukan dengan apa yang diberikan kepada Anda ...)
c_maker
Contoh terbaik yang bisa saya berikan adalah metode CheckIn di mana kami memiliki aturan bisnis yang menentukan siapa yang diizinkan untuk melakukan tindakan (otorisasi), jika objek tersebut dalam kondisi yang tepat untuk diperiksa seperti apakah itu check-out dan jika perubahan telah dibuat. Kami akan memiliki unit test yang memverifikasi aturan otorisasi sedang ditegakkan serta tes untuk memverifikasi bahwa metode berperilaku dengan benar jika CheckIn dipanggil ketika objek tidak diperiksa, tidak diubah, dll. Itulah sebabnya kami berakhir dengan banyak tes. Apakah Anda menyarankan ini salah?
SonOfPirate
Saya tidak berpikir ada jawaban yang sulit dan cepat, benar atau salah tentang hal ini. Jika apa yang Anda lakukan bermanfaat bagi Anda, itu bagus. Ini hanya sudut pandang saya tentang pedoman umum.
FishBasketGordo
2

Salah satu argumen saya terhadap pemisahan tes menjadi beberapa kelas adalah bahwa menjadi lebih sulit bagi pengembang lain dalam tim (terutama mereka yang tidak begitu paham tes) untuk menemukan tes yang ada ( Ya ampun saya bertanya-tanya apakah sudah ada tes untuk ini metode? Saya ingin tahu di mana itu akan? ) dan juga di mana harus meletakkan tes baru ( saya akan menulis tes untuk metode ini di kelas ini, tetapi tidak yakin apakah saya harus meletakkannya di file baru, atau yang sudah ada satu? )

Dalam kasus di mana tes yang berbeda memerlukan pengaturan yang berbeda secara drastis, saya telah melihat beberapa praktisi TDD meletakkan "fixture" atau "setup" di kelas / file yang berbeda, tetapi bukan tes itu sendiri.

reli25rs
sumber
1

Saya berharap saya dapat mengingat / menemukan tautan di mana teknik yang saya pilih untuk diadopsi pertama kali diperlihatkan. Pada dasarnya saya membuat satu, kelas abstrak untuk setiap kelas yang diuji yang berisi perlengkapan tes bersarang (kelas) untuk setiap anggota yang diuji. Ini memberikan pemisahan yang awalnya diinginkan tetapi menyimpan semua tes dalam file yang sama untuk setiap lokasi. Selain itu, ini menghasilkan nama kelas yang memungkinkan pengelompokan dan pengurutan yang mudah dalam test runner.

Berikut adalah contoh bagaimana pendekatan ini diterapkan pada skenario asli saya:

public abstract class DocumentTests : TestBase
{
    [TestClass()]
    public sealed class CheckInShould : DocumentTests
    {
        [TestMethod()]
        public void ThrowExceptionWhenUserIsNotAuthorized()
        {
        }
    }

    [TestClass()]
    public sealed class CheckOutShould : DocumentTests
    {
        [TestMethod()]
        public void ThrowExceptionWhenUserIsNotAuthorized()
        {
        }
    }
}

Ini menghasilkan tes berikut yang muncul dalam daftar tes:

DocumentTests+CheckInShould.ThrowExceptionWhenUserIsNotAuthorized
DocumentTests+CheckOutShould.ThrowExceptionWhenUserIsNotAuthorized

Karena lebih banyak tes ditambahkan, mereka mudah dikelompokkan dan disortir berdasarkan nama kelas yang juga menyimpan semua tes untuk kelas yang diuji terdaftar bersama-sama.

Keuntungan lain dari pendekatan ini yang telah saya pelajari untuk meningkatkan adalah sifat berorientasi objek dari struktur ini berarti saya dapat mendefinisikan kode di dalam startup dan metode pembersihan dari kelas dasar DocumentTests yang akan dibagikan oleh semua kelas bersarang serta di dalam masing-masing kelas bersarang untuk tes yang dikandungnya.

SonOfPirate
sumber
1

Cobalah untuk berpikir sebaliknya selama satu menit.

Mengapa Anda hanya memiliki satu kelas tes per kelas? Apakah Anda melakukan tes kelas atau tes unit? Apakah Anda bahkan bergantung pada kelas ?

Tes unit seharusnya menguji perilaku tertentu, dalam konteks tertentu. Fakta bahwa kelas Anda sudah memiliki CheckInsarana harus ada perilaku yang membutuhkannya sejak awal.

Bagaimana pendapat Anda tentang pseudo-code ini:

// check_in_test.file

class CheckInTest extends TestCase {
        /** @test */
        public function unauthorized_users_cannot_check_in() {
                $this->expectException();

                $document = new Document($unauthorizedUser);

                $document->checkIn();
        }
}

Sekarang Anda tidak langsung menguji checkInmetode ini. Alih-alih, Anda sedang menguji perilaku (yaitu untuk mengecek untungnya;)), dan jika Anda perlu melakukan refactor Documentdan membaginya menjadi kelas yang berbeda, atau menggabungkan kelas lain ke dalamnya, file pengujian Anda masih koheren, mereka masih masuk akal karena Anda tidak pernah mengubah logika saat refactoring, hanya strukturnya.

Satu file tes untuk satu kelas hanya membuat sulit untuk refactor kapan pun Anda membutuhkannya, dan tes juga kurang masuk akal dalam hal domain / logika kode itu sendiri.

Steve Chamaillard
sumber
0

Secara umum saya akan merekomendasikan berpikir tentang tes sebagai pengujian perilaku kelas daripada metode. Yaitu beberapa tes Anda mungkin perlu memanggil kedua metode di kelas untuk menguji beberapa perilaku yang diharapkan.

Biasanya saya mulai dengan satu kelas uji unit per kelas produksi, tetapi pada akhirnya dapat membagi kelas uji unit menjadi beberapa kelas uji berdasarkan perilaku yang mereka uji. Dengan kata lain saya akan merekomendasikan agar tidak memisahkan kelas uji menjadi CheckInShould dan CheckOutShould, tetapi lebih baik membelah dengan perilaku unit yang diuji.

Christian Horsdal
sumber
0

1. Pertimbangkan apa yang kemungkinan salah

Selama pengembangan awal, Anda cukup tahu apa yang Anda lakukan dan kedua solusi mungkin akan bekerja dengan baik.

Semakin menarik ketika tes gagal setelah perubahan jauh kemudian. Ada dua kemungkinan:

  1. Anda telah rusak parah CheckIn(atau CheckOut). Sekali lagi, baik file tunggal maupun solusi dua file tidak masalah dalam hal itu.
  2. Anda telah memodifikasi keduanya CheckIndan CheckOut(dan mungkin pengujian mereka) dengan cara yang masuk akal untuk masing-masing, tetapi tidak untuk keduanya secara bersamaan. Anda telah merusak koherensi pasangan. Dalam hal ini, membagi tes lebih dari dua file akan membuatnya lebih sulit untuk memahami masalahnya.

2. Pertimbangkan tes apa yang digunakan

Tes melayani dua tujuan utama:

  1. Secara otomatis memeriksa program masih berfungsi dengan benar. Apakah tes dalam satu file atau beberapa tidak penting untuk tujuan ini.
  2. Bantu pembaca manusia memahami program ini. Ini akan jauh lebih mudah jika tes untuk fungsionalitas yang terkait erat disimpan bersama, karena melihat koherensi mereka adalah jalan cepat menuju pemahaman.

Begitu?

Kedua perspektif ini menyarankan agar tes bersama dapat membantu, tetapi tidak akan merugikan.

Lutz Prechelt
sumber