Dalam banyak kasus saya mungkin memiliki kelas yang sudah ada dengan beberapa perilaku:
class Lion
{
public void Eat(Herbivore herbivore) { ... }
}
... dan saya memiliki unit test ...
[TestMethod]
public void Lion_can_eat_herbivore()
{
var herbivore = buildHerbivoreForEating();
var test = BuildLionForTest();
test.Eat(herbivore);
Assert.IsEaten(herbivore);
}
Sekarang, apa yang terjadi adalah saya perlu membuat kelas Tiger dengan perilaku yang identik dengan singa:
class Tiger
{
public void Eat(Herbivore herbivore) { ... }
}
... dan karena saya ingin perilaku yang sama, saya perlu menjalankan tes yang sama, saya melakukan sesuatu seperti ini:
interface IHerbivoreEater
{
void Eat(Herbivore herbivore);
}
... dan saya memperbaiki pengujian saya:
[TestMethod]
public void Lion_can_eat_herbivore()
{
IHerbivoreEater_can_eat_herbivore(BuildLionForTest);
}
public void IHerbivoreEater_can_eat_herbivore(Func<IHerbivoreEater> builder)
{
var herbivore = buildHerbivoreForEating();
var test = builder();
test.Eat(herbivore);
Assert.IsEaten(herbivore);
}
... dan kemudian saya menambahkan tes lain untuk Tiger
kelas baru saya :
[TestMethod]
public void Tiger_can_eat_herbivore()
{
IHerbivoreEater_can_eat_herbivore(BuildTigerForTest);
}
... dan kemudian saya memperbaiki kelas saya Lion
dan Tiger
(biasanya dengan warisan, tetapi kadang-kadang dengan komposisi):
class Lion : HerbivoreEater { }
class Tiger : HerbivoreEater { }
abstract class HerbivoreEater : IHerbivoreEater
{
public void Eat(Herbivore herbivore) { ... }
}
... dan semuanya baik-baik saja. Namun, karena fungsionalitasnya sekarang ada di HerbivoreEater
kelas, sekarang rasanya ada yang salah dengan melakukan tes untuk masing-masing perilaku ini di setiap subkelas. Namun itu adalah subclass yang benar-benar dikonsumsi, dan itu hanya detail implementasi bahwa mereka berbagi perilaku yang tumpang tindih ( Lions
dan Tigers
mungkin memiliki penggunaan akhir yang sama sekali berbeda, misalnya).
Tampaknya berlebihan untuk menguji kode yang sama beberapa kali, tetapi ada kasus di mana subclass dapat dan tidak menimpa fungsionalitas kelas dasar (ya, itu mungkin melanggar LSP, tetapi mari kita hadapi itu, IHerbivoreEater
hanya antarmuka pengujian yang nyaman - itu mungkin tidak masalah bagi pengguna akhir). Jadi tes ini memang memiliki nilai, saya pikir.
Apa yang dilakukan orang lain dalam situasi ini? Apakah Anda hanya memindahkan tes Anda ke kelas dasar, atau apakah Anda menguji semua subclass untuk perilaku yang diharapkan?
EDIT :
Berdasarkan jawaban dari @ pdr saya pikir kita harus mempertimbangkan ini: IHerbivoreEater
itu hanya kontrak metode tanda tangan; itu tidak menentukan perilaku. Contohnya:
[TestMethod]
public void Tiger_eats_herbivore_haunches_first()
{
IHerbivoreEater_eats_herbivore_haunches_first(BuildTigerForTest);
}
[TestMethod]
public void Cheetah_eats_herbivore_haunches_first()
{
IHerbivoreEater_eats_herbivore_haunches_first(BuildCheetahForTest);
}
[TestMethod]
public void Lion_eats_herbivore_head_first()
{
IHerbivoreEater_eats_herbivore_head_first(BuildLionForTest);
}
sumber
Animal
kelas yang berisiEat
? Semua hewan makan, dan karenanya kelasTiger
danLion
dapat mewarisi dari hewan.Eat
perilaku di kelas dasar, maka semua subclass harus menunjukkanEat
perilaku yang sama . Namun, saya berbicara tentang 2 kelas yang relatif tidak berhubungan yang terjadi untuk berbagi perilaku. Pertimbangkan, misalnya,Fly
perilakuBrick
danPerson
yang, dapat kita asumsikan, menunjukkan perilaku terbang yang serupa, tetapi tidak selalu masuk akal untuk membuat mereka berasal dari kelas dasar yang sama.Jawaban:
Ini bagus karena ini menunjukkan bagaimana tes benar-benar mendorong cara Anda berpikir tentang desain. Anda merasakan masalah dalam desain dan mengajukan pertanyaan yang tepat.
Ada dua cara untuk melihatnya.
IHerbivoreEater adalah kontrak. Semua IHerbivoreEaters harus memiliki metode Makan yang menerima Herbivore. Sekarang, tes Anda tidak peduli bagaimana dimakan; Singa Anda mungkin mulai dengan paha dan Tiger mungkin mulai dari tenggorokan. Semua tes Anda peduli adalah bahwa setelah itu disebut Makan, Herbivore dimakan.
Di sisi lain, bagian dari apa yang Anda katakan adalah bahwa semua IHerbivoreEaters memakan Herbivore dengan cara yang persis sama (karenanya kelas dasar). Karena itu, tidak ada gunanya memiliki kontrak IHerbivoreEater sama sekali. Itu tidak menawarkan apa pun. Anda mungkin juga mewarisi dari HerbivoreEater.
Atau mungkin menyingkirkan Lion and Tiger sepenuhnya.
Tetapi, jika Lion dan Tiger berbeda dalam segala hal selain dari kebiasaan makan mereka, maka Anda perlu mulai bertanya-tanya apakah Anda akan mengalami masalah dengan pohon warisan yang kompleks. Bagaimana jika Anda juga ingin menurunkan kedua kelas dari Feline, atau hanya kelas Lion dari KingOfItsDomain (bersama dengan Shark, mungkin). Di sinilah LSP benar-benar masuk.
Saya menyarankan bahwa kode umum lebih baik dienkapsulasi.
Hal yang sama berlaku untuk Tiger.
Sekarang, ini adalah hal yang indah berkembang (cantik karena saya tidak bermaksud). Jika Anda hanya membuat konstruktor pribadi yang tersedia untuk pengujian, Anda bisa meneruskan IHerbivoreEatingStrategy palsu dan cukup menguji bahwa pesan tersebut diteruskan ke objek yang dienkapsulasi dengan benar.
Dan tes kompleks Anda, yang Anda khawatirkan sejak awal, hanya perlu menguji StandardHerbivoreEatingStrategy. Satu kelas, satu set tes, tidak ada kode duplikat untuk dikhawatirkan.
Dan jika, nanti, Anda ingin memberi tahu harimau bahwa mereka harus memakan herbivora dengan cara yang berbeda, tidak ada tes yang harus diubah. Anda cukup membuat HerbivoreEatingStrategy baru dan mengujinya. Pengkabelan diuji pada tingkat uji integrasi.
sumber
IHerbivoreEater
adalah kontrak, tetapi hanya sebanyak yang saya butuhkan untuk pengujian. Menurut saya ini adalah salah satu kasus di mana mengetik bebek akan sangat membantu. Saya hanya ingin mengirim keduanya ke logika tes yang sama sekarang. Saya tidak berpikir antarmuka harus menjanjikan perilaku. Tes harus melakukan itu.Secara keseluruhan, Anda bertanya apakah layak menggunakan pengetahuan kotak putih untuk menghilangkan beberapa tes. Dari perspektif kotak hitam,
Lion
danTiger
kelas yang berbeda. Jadi seseorang yang tidak terbiasa dengan kode akan mengujinya, tetapi Anda dengan pengetahuan implementasi yang mendalam tahu bahwa Anda bisa lolos hanya dengan menguji satu hewan.Bagian dari alasan untuk mengembangkan unit test adalah untuk memungkinkan diri Anda untuk melakukan refactor nanti tetapi mempertahankan antarmuka black-box yang sama . Tes unit membantu Anda memastikan kelas Anda terus memenuhi kontrak mereka dengan klien, atau paling tidak memaksa Anda untuk menyadari dan berpikir hati-hati bagaimana kontrak mungkin berubah. Anda sendiri menyadari hal itu
Lion
atauTiger
mungkin menimpaEat
di beberapa titik nanti. Jika ini memungkinkan, uji unit sederhana yang dapat dimakan setiap hewan yang Anda makan, dengan cara:harus sangat sederhana untuk dilakukan dan cukup dan akan memastikan bahwa Anda dapat mendeteksi ketika benda gagal memenuhi kontrak mereka.
sumber
Kamu melakukannya dengan benar. Pikirkan uji unit sebagai tes perilaku penggunaan satu kode baru Anda. Ini adalah panggilan yang sama dengan yang Anda lakukan dari kode produksi.
Dalam situasi itu, Anda sepenuhnya benar; pengguna Lion atau Tiger tidak akan (tidak harus, setidaknya) peduli bahwa mereka berdua HerbivoreEaters dan bahwa kode yang benar-benar berjalan untuk metode ini adalah umum untuk keduanya di kelas dasar. Demikian pula, pengguna abstrak HerbivoreEater (disediakan oleh Lion atau Tiger beton) tidak akan peduli dengan apa yang dimilikinya. Apa yang mereka pedulikan adalah bahwa Singa, Harimau, atau implementasi konkret HerbivoreEater mereka yang tidak diketahui akan memakan () seekor herbivora dengan benar.
Jadi, yang pada dasarnya Anda uji adalah bahwa Singa akan Makan sebagaimana dimaksud, dan bahwa Harimau akan Makan sebagaimana dimaksud. Penting untuk menguji keduanya, karena mungkin tidak selalu benar bahwa mereka berdua makan dengan cara yang persis sama; dengan menguji keduanya, Anda memastikan bahwa yang Anda tidak ingin berubah, tidak. Karena keduanya adalah HerbivoreEaters yang ditentukan, setidaknya sampai Anda menambahkan Cheetah Anda juga telah menguji bahwa semua HerbivoreEaters akan Makan sebagaimana dimaksud. Pengujian Anda sepenuhnya mencakup dan melatih kode secara memadai (asalkan Anda juga membuat semua pernyataan yang diharapkan tentang apa yang dihasilkan dari HerbivoreEater yang memakan Herbivore).
sumber