Haruskah saya menguji metode yang diwariskan?

30

Misalkan saya memiliki Manajer kelas yang berasal dari Karyawan kelas dasar , dan Karyawan itu memiliki metode getEmail () yang diwarisi oleh Manajer . Haruskah saya menguji bahwa perilaku metode getEmail () manajer sebenarnya sama dengan perilaku karyawan?

Pada saat tes-tes ini ditulis, tingkah lakunya akan sama, tetapi tentu saja di beberapa titik di masa depan seseorang mungkin menimpa metode ini, mengubah perilakunya, dan karena itu menghancurkan aplikasi saya. Namun tampaknya agak aneh untuk menguji pada dasarnya tidak adanya campur tangan kode.

(Perhatikan bahwa metode pengujian Manager :: getEmail () tidak meningkatkan cakupan kode (atau bahkan metrik kualitas kode lainnya (?)) Sampai Manager :: getEmail () dibuat / diganti.)

(Jika jawabannya "Ya", beberapa informasi tentang cara mengelola tes yang dibagi antara kelas dasar dan turunan akan berguna.)

Perumusan pertanyaan yang setara:

Jika kelas turunan mewarisi metode dari kelas dasar, bagaimana Anda mengekspresikan (menguji) apakah Anda mengharapkan metode yang diwarisi untuk:

  1. Berperilaku dengan cara yang persis sama seperti yang dilakukan basis saat ini (jika perilaku basis berubah, perilaku metode yang diturunkan tidak);
  2. Berperilaku persis sama dengan basis untuk semua waktu (jika perilaku kelas dasar berubah, perilaku kelas turunan juga berubah); atau
  3. Berperilaku namun ingin (Anda tidak peduli tentang perilaku metode ini karena Anda tidak pernah menyebutnya).
mjs
sumber
1
IMO berasal Managerdari kelas Employeeadalah kesalahan besar pertama.
CodesInChaos
4
@CodesInChaos Ya, itu bisa menjadi contoh yang buruk. Tetapi masalah yang sama berlaku setiap kali Anda memiliki warisan.
mjs
1
Anda bahkan tidak perlu mengganti metode. Metode kelas dasar dapat memanggil metode instance lain yang ditimpa, dan mengeksekusi kode sumber yang sama untuk metode ini masih menciptakan perilaku yang berbeda.
gnasher729

Jawaban:

23

Saya akan mengambil pendekatan pragmatis di sini: Jika seseorang, di masa depan, menimpa Manajer :: getMail, maka tanggung jawab pengembang untuk menyediakan kode uji untuk metode baru.

Tentu saja itu hanya valid jika Manager::getEmailbenar-benar memiliki jalur kode yang sama Employee::getEmail! Bahkan jika metode ini tidak ditimpa, itu mungkin berperilaku berbeda:

  • Employee::getEmailbisa menelepon beberapa maya dilindungi getInternalEmailyang sedang ditimpa di Manager.
  • Employee::getEmaildapat mengakses beberapa kondisi internal (mis. beberapa bidang _email), yang dapat berbeda dalam dua implementasi: Misalnya, implementasi default Employeedapat memastikan hal _emailitu selalu [email protected], tetapi Managerlebih fleksibel dalam menetapkan alamat surat.

Dalam kasus seperti itu, ada kemungkinan bahwa bug memanifestasikan dirinya hanya di Manager::getEmail, meskipun implementasi dari metode itu sendiri adalah sama. Dalam hal pengujian Manager::getEmailsecara terpisah bisa masuk akal.

Heinzi
sumber
14

Saya akan.

Jika Anda berpikir "baik, itu benar-benar hanya memanggil Karyawan :: getEmail () karena saya tidak menimpanya, jadi saya tidak perlu menguji Manajer :: getEmail ()" maka Anda tidak benar-benar menguji perilaku Manajer :: getEmail ().

Saya akan memikirkan apa yang hanya harus dilakukan oleh Manajer :: getEmail (), bukan apakah itu diwarisi atau diganti. Jika perilaku Manajer :: getEmail () seharusnya mengembalikan apa pun yang Karyawan :: getMail () kembali, maka itulah ujiannya. Jika perilakunya adalah mengembalikan "[email protected]", maka itulah ujiannya. Tidak masalah apakah itu diterapkan oleh warisan atau diganti.

Yang penting adalah, jika itu berubah di masa depan, tes Anda akan menangkapnya dan Anda tahu ada yang rusak atau perlu dipertimbangkan kembali.

Beberapa orang mungkin tidak setuju dengan redundansi yang tampak dalam cek, tetapi lawan saya adalah Anda menguji perilaku Karyawan :: getMail () dan Manajer :: getMail () sebagai metode yang berbeda, apakah mereka diwariskan atau tidak ditimpa. Jika pengembang di masa depan perlu mengubah perilaku Manajer :: getMail () maka mereka juga perlu memperbarui tes.

Pendapat mungkin bervariasi, saya pikir Digger dan Heinzi memberikan alasan yang masuk akal untuk hal sebaliknya.

seggy
sumber
1
Saya suka argumen bahwa pengujian perilaku adalah ortogonal dengan cara kelas dibangun. Tampaknya agak lucu untuk sepenuhnya mengabaikan warisan. (Dan bagaimana Anda mengelola tes bersama, jika Anda memiliki banyak warisan?)
mjs
1
Baik. Hanya karena Manajer menggunakan metode yang diwariskan, sama sekali tidak berarti bahwa itu tidak boleh diuji. Seorang pemimpin saya yang hebat pernah berkata, "Jika tidak diuji, itu rusak." Untuk itu, jika Anda melakukan tes untuk Karyawan dan Manajer yang diperlukan pada saat check-in kode, Anda akan yakin bahwa pengembang yang memeriksa kode baru untuk Manajer yang dapat mengubah perilaku metode yang diwarisi akan memperbaiki tes untuk mencerminkan perilaku baru . Atau mengetuk pintu Anda.
hurricaneMitch
2
Anda benar-benar ingin menguji kelas Manajer. Fakta bahwa ini adalah subkelas Karyawan, dan bahwa getMail () diimplementasikan dengan mengandalkan warisan, hanyalah detail implementasi yang harus Anda abaikan saat membuat unit test. Bulan depan Anda mengetahui bahwa mewarisi Manajer dari Karyawan adalah ide yang buruk dan Anda mengganti seluruh struktur warisan dan menerapkan kembali semua metode. Tes unit Anda harus terus menguji kode Anda tanpa masalah.
gnasher729
5

Jangan mengujinya. Lakukan uji fungsional / penerimaan itu.

Tes unit harus menguji setiap implementasi, jika Anda tidak menyediakan implementasi baru, maka patuhi prinsip KERING. Jika Anda ingin meluangkan upaya di sini maka Anda dapat meningkatkan tes unit asli. Hanya jika Anda mengganti metode yang akan Anda menulis unit test.

Pada saat yang sama, pengujian fungsional / penerimaan harus memastikan bahwa pada akhirnya semua kode Anda melakukan apa yang seharusnya, dan semoga akan menangkap keanehan dari warisan.

MrFox
sumber
4

Aturan Robert Martin untuk TDD adalah:

  1. Anda tidak diperbolehkan untuk menulis kode produksi apa pun kecuali untuk membuat lulus uji unit yang gagal.
  2. Anda tidak diperbolehkan menulis lebih dari satu unit tes daripada yang cukup untuk gagal; dan kegagalan kompilasi adalah kegagalan.
  3. Anda tidak diperbolehkan menulis kode produksi lebih dari cukup untuk lulus satu unit test gagal.

Jika Anda mengikuti aturan ini, Anda tidak akan dapat masuk ke posisi untuk mengajukan pertanyaan ini. Jika getEmailmetode ini perlu diuji, itu akan diuji.

Daniel Kaplan
sumber
3

Ya, Anda harus menguji metode yang diwariskan karena di masa depan mereka mungkin akan diganti. Juga, metode yang diwariskan dapat memanggil metode virtual itu ditimpa , yang akan mengubah perilaku metode yang diwarisi tidak ditimpa.

Cara saya menguji ini adalah dengan membuat kelas abstrak untuk menguji kelas dasar atau antarmuka (mungkin abstrak), seperti ini (dalam C # menggunakan NUnit):

public abstract class EmployeeTests
{
    protected abstract Employee CreateInstance(string name, int age);

    [Test]
    public void GetEmail_ReturnsValidEmailAddress()
    {
        // Given
        var sut = CreateInstance("John Doe", 20);

        // When
        string email = sut.GetEmail();

        // Then
        Assert.IsTrue(Helper.IsValidEmail(email));
    }
}

Kemudian, saya memiliki kelas dengan tes khusus untuk Manager, dan mengintegrasikan tes karyawan seperti ini:

[TestFixture]
public class ManagerTests
{
    // Other tests.

    [TestFixture]
    public class ManagerEmployeeTests : EmployeeTests
    {
        protected override Employee CreateInstance(string name, int age);
        {
            return new Manager(name, age);
        }
    }
}

Alasan saya bisa jadi ini adalah prinsip substitusi Liskov: tes Employeeharus tetap lulus ketika melewati Managerobjek, karena itu berasal dari Employee. Jadi saya harus menulis tes saya hanya sekali, dan dapat memverifikasi mereka bekerja untuk semua kemungkinan implementasi antarmuka atau kelas dasar.

Daniel AA Pelsmaeker
sumber
Poin bagus adalah prinsip substitusi Liskov, Anda benar bahwa jika ini berlaku, kelas turunan akan lulus semua tes kelas dasar. Namun, LSP sering dilanggar, termasuk dengan setUp()metode xUnit sendiri! Dan hampir semua kerangka kerja MVC web yang melibatkan penggantian metode "indeks" juga merusak LSP, yang pada dasarnya semuanya.
mjs
Ini benar-benar jawaban yang benar - Saya pernah mendengarnya disebut "Pola Tes Abstrak". Karena penasaran, mengapa menggunakan kelas bersarang alih-alih hanya mencampur-in tes melalui class ManagerTests : ExployeeTests? (Yaitu Mencerminkan warisan kelas yang diuji.) Apakah ini terutama estetika, untuk membantu melihat hasilnya?
Luke Usherwood
2

Haruskah saya menguji bahwa perilaku metode getEmail () manajer sebenarnya sama dengan perilaku karyawan?

Saya akan mengatakan tidak karena itu akan menjadi tes berulang menurut pendapat saya, saya akan menguji sekali dalam tes Karyawan dan hanya itu.

Pada saat tes-tes ini ditulis, perilakunya akan sama, tetapi tentu saja di beberapa titik di masa depan seseorang mungkin menimpa metode ini, mengubah perilakunya.

Jika metode ini diganti maka Anda akan memerlukan tes baru untuk memeriksa perilaku yang diganti. Itu adalah tugas orang yang menerapkan getEmail()metode utama .


sumber
1

Tidak, Anda tidak perlu menguji metode yang diwariskan. Kelas dan kasus uji mereka yang bergantung pada metode ini akan tetap terpecah jika perilaku berubahManager .

Pikirkan skenario berikut: Alamat email dirakit sebagai [email protected]:

class Employee{
    String firstname, lastname;
    String getEmail() { 
        return firstname + "." + lastname + "@example.com";
    }
}

Anda telah menguji unit ini dan berfungsi dengan baik untuk Anda Employee. Anda juga telah membuat kelas Manager:

class Manager extends Employee { /* nothing different in email generation */ }

Sekarang Anda memiliki kelas ManagerSortyang mengurutkan manajer dalam daftar berdasarkan alamat email mereka. Dari Anda, Anda mengira bahwa generasi email sama dengan di Employee:

class ManagerSort {
    void sortManagers(Manager[] managerArrayToBeSorted)
        // sort based on email address omitted
    }
}

Anda menulis ujian untuk ManagerSort:

void testManagerSort() {
    Manager[] managers = ... // build random manager list
    ManagerSort.sortManagers(managers);

    Manager[] expected = ... // expected result
    assertEquals(expected, managers); // check the result
}

Semuanya bekerja dengan baik. Sekarang seseorang datang dan mengganti getEmail()metode:

class Manager extends Employee {
    String getEmail(){
        // managers should have their lastname and firstname order changed
        return lastname + "." + firstname + "@example.com";
    }
}

Sekarang, apa yang terjadi? Anda testManagerSort()akan gagal karenagetEmail() dari Manageritu ditimpa. Anda akan menyelidiki dalam masalah ini dan akan menemukan penyebabnya. Dan semua tanpa menulis testcase terpisah untuk metode yang diwarisi.

Karena itu, Anda tidak perlu menguji metode yang diwariskan.

Kalau tidak, misalnya di Jawa, Anda harus menguji semua metode yang diwarisi dari Objectsuka toString(), equals()dll. Di setiap kelas.

Uooo
sumber
Anda tidak seharusnya pintar dengan unit test. Anda mengatakan "Saya tidak perlu menguji X karena ketika X gagal, Y gagal". Tetapi unit test didasarkan pada asumsi bug dalam kode Anda. Dengan bug dalam kode Anda, mengapa Anda berpikir bahwa hubungan rumit antara tes bekerja seperti yang Anda harapkan?
gnasher729
@ gnasher729 Saya katakan "Saya tidak perlu menguji X di subkelas karena sudah diuji dalam unit test untuk superclass ". Tentu saja, jika Anda mengubah implementasi X, Anda harus menulis tes yang sesuai untuk itu.
Uooo