Bagaimana cara menguji kelas abstrak: memperpanjang dengan bertopik?


446

Saya bertanya-tanya bagaimana cara menguji kelas abstrak, dan kelas yang memperluas kelas abstrak.

Haruskah saya menguji kelas abstrak dengan memperluasnya, mematikan metode abstrak, dan kemudian menguji semua metode konkret? Maka hanya menguji metode yang saya timpa, dan menguji metode abstrak dalam tes unit untuk objek yang memperpanjang kelas abstrak saya?

Haruskah saya memiliki test case abstrak yang dapat digunakan untuk menguji metode kelas abstrak, dan memperluas kelas ini dalam test case saya untuk objek yang memperluas kelas abstrak?

Perhatikan bahwa kelas abstrak saya memiliki beberapa metode konkret.

Jawaban:


268

Tulis objek Mock dan gunakan hanya untuk pengujian. Mereka biasanya sangat sangat sangat minim (mewarisi dari kelas abstrak) dan tidak lebih. Kemudian, dalam Unit Test Anda, Anda dapat memanggil metode abstrak yang ingin Anda uji.

Anda harus menguji kelas abstrak yang berisi beberapa logika seperti semua kelas lain yang Anda miliki.


9
Sial, aku harus mengatakan ini adalah pertama kalinya aku setuju dengan ide menggunakan tiruan.
Jonathan Allen

5
Anda perlu dua kelas, satu mock dan tes. Kelas tiruan hanya memperluas metode abstrak dari kelas Abstrak yang diuji. Metode-metode itu bisa no-op, return null, dll. Karena tidak akan diuji. Kelas uji hanya menguji API publik non-abstrak (yaitu Antarmuka yang diterapkan oleh kelas Abstrak). Untuk setiap kelas yang memperluas kelas Abstrak Anda akan memerlukan kelas tes tambahan karena metode abstrak tidak tercakup.
cyber-monk

10
Sangat mungkin untuk melakukan ini .. tetapi untuk benar-benar menguji setiap kelas turunan Anda akan menguji fungsi dasar ini berulang-ulang .. yang membuat Anda memiliki fixture tes abstrak sehingga Anda dapat memperhitungkan duplikasi ini dalam pengujian. Ini semua baunya! Saya sangat merekomendasikan untuk melihat mengapa Anda menggunakan kelas abstrak di tempat pertama dan melihat apakah sesuatu yang lain akan bekerja lebih baik.
Nigel Thorne

5
jawaban berikutnya yang berlebihan jauh lebih baik.
Martin Spamer

22
@MartiSpamer: Saya tidak akan mengatakan jawaban ini dinilai terlalu tinggi karena ditulis jauh lebih awal (2 tahun) daripada jawaban yang Anda anggap lebih baik di bawah ini. Mari kita mendorong Patrick karena dalam konteks ketika dia mengirim jawaban ini, itu hebat. Mari kita saling mendukung. Cheers
Marvin Thobejane

449

Ada dua cara di mana kelas dasar abstrak digunakan.

  1. Anda mengkhususkan objek abstrak Anda, tetapi semua klien akan menggunakan kelas turunan melalui antarmuka dasarnya.

  2. Anda menggunakan kelas dasar abstrak untuk memfaktorkan duplikasi dalam objek dalam desain Anda, dan klien menggunakan implementasi konkret melalui antarmuka mereka sendiri.!


Solusi Untuk 1 - Pola Strategi

Pilihan 1

Jika Anda memiliki situasi pertama, maka Anda sebenarnya memiliki antarmuka yang ditentukan oleh metode virtual di kelas abstrak yang diterapkan oleh kelas turunan Anda.

Anda harus mempertimbangkan membuat ini antarmuka nyata, mengubah kelas abstrak Anda menjadi konkret, dan mengambil contoh antarmuka ini dalam konstruktornya. Kelas turunan Anda kemudian menjadi implementasi dari antarmuka baru ini.

IMotor

Ini berarti Anda sekarang dapat menguji kelas abstrak Anda sebelumnya menggunakan contoh tiruan dari antarmuka baru, dan setiap implementasi baru melalui antarmuka sekarang publik. Semuanya sederhana dan dapat diuji.


Solusi untuk 2

Jika Anda memiliki situasi kedua, maka kelas abstrak Anda berfungsi sebagai kelas pembantu.

AbstractHelper

Lihatlah fungsionalitas yang dikandungnya. Lihat apakah ada yang bisa didorong ke objek yang sedang dimanipulasi untuk meminimalkan duplikasi ini. Jika Anda masih memiliki sesuatu yang tersisa, lihat menjadikannya sebagai kelas pembantu yang implementasi konkret Anda ambil dalam konstruktor mereka dan menghapus kelas dasar mereka.

Pembantu Motor

Ini lagi mengarah ke kelas konkret yang sederhana dan mudah diuji.


Sebagai aturan

Mendukung jaringan kompleks objek sederhana melalui jaringan objek kompleks sederhana.

Kunci kode yang dapat diuji yang dapat diperluas adalah blok bangunan kecil dan kabel independen.


Diperbarui: Bagaimana cara menangani campuran keduanya?

Dimungkinkan untuk memiliki kelas dasar yang melakukan kedua peran ini ... yaitu: ia memiliki antarmuka publik, dan telah melindungi metode pembantu. Jika ini masalahnya, maka Anda dapat memfaktorkan metode helper menjadi satu kelas (skenario2) dan mengubah pohon warisan menjadi pola strategi.

Jika Anda menemukan Anda memiliki beberapa metode yang diterapkan kelas dasar Anda secara langsung dan yang lainnya adalah virtual, maka Anda masih dapat mengubah pohon warisan menjadi pola strategi, tetapi saya juga akan menganggapnya sebagai indikator yang baik bahwa tanggung jawab tidak selaras dengan benar, dan mungkin perlu refactoring.


Pembaruan 2: Kelas Abstrak sebagai batu loncatan (2014/06/12)

Saya mengalami situasi di hari sebelumnya ketika saya menggunakan abstrak, jadi saya ingin mencari tahu mengapa.

Kami memiliki format standar untuk file konfigurasi kami. Alat khusus ini memiliki 3 file konfigurasi semua dalam format itu. Saya ingin kelas yang sangat diketik untuk setiap file pengaturan sehingga, melalui injeksi ketergantungan, kelas bisa meminta pengaturan yang diperhatikannya.

Saya menerapkan ini dengan memiliki kelas dasar abstrak yang tahu bagaimana mem-parsing format file pengaturan dan kelas turunan yang mengekspos metode yang sama, tetapi merangkum lokasi file pengaturan.

Saya bisa menulis "SettingsFileParser" yang dibungkus oleh 3 kelas, dan kemudian didelegasikan ke kelas dasar untuk mengekspos metode akses data. Saya memilih untuk tidak melakukan hal ini belum karena akan mengakibatkan 3 kelas turunan dengan lebih delegasi kode di dalamnya dari apa pun.

Namun ... saat kode ini berkembang dan konsumen dari masing-masing kelas pengaturan ini menjadi lebih jelas. Setiap pengaturan pengguna akan meminta beberapa pengaturan dan mengubahnya dalam beberapa cara (karena pengaturan adalah teks mereka dapat membungkusnya dalam objek untuk mengubahnya menjadi angka dll.). Ketika ini terjadi, saya akan mulai mengekstraksi logika ini ke dalam metode manipulasi data dan mendorongnya kembali ke kelas pengaturan yang diketik dengan kuat. Ini akan mengarah ke antarmuka tingkat yang lebih tinggi untuk setiap set pengaturan, yang pada akhirnya tidak lagi sadar berurusan dengan 'pengaturan'.

Pada titik ini, kelas pengaturan yang sangat diketik tidak lagi memerlukan metode "pengambil" yang mengekspos implementasi 'pengaturan' yang mendasarinya.

Pada saat itu saya tidak ingin lagi antarmuka publik mereka memasukkan metode accessor pengaturan; jadi saya akan mengubah kelas ini untuk merangkum kelas parser pengaturan bukannya berasal dari itu.

Kelas Abstrak karena itu: cara bagi saya untuk menghindari kode delegasi saat ini, dan penanda dalam kode untuk mengingatkan saya untuk mengubah desain nanti. Saya mungkin tidak pernah mencapainya, jadi mungkin tinggal sebentar ... hanya kode yang dapat memberi tahu.

Saya menemukan ini benar dengan aturan apa pun ... seperti "tanpa metode statis" atau "tanpa metode pribadi". Mereka mengindikasikan bau dalam kode ... dan itu bagus. Itu membuat Anda mencari abstraksi yang Anda lewatkan ... dan memungkinkan Anda terus memberikan nilai kepada pelanggan Anda dalam waktu yang bersamaan.

Saya membayangkan aturan seperti ini mendefinisikan lanskap, di mana kode yang dapat dipelihara tinggal di lembah. Saat Anda menambahkan perilaku baru, itu seperti hujan mendarat di kode Anda. Awalnya Anda meletakkannya di mana pun ia mendarat .. lalu Anda refactor untuk memungkinkan kekuatan desain yang baik untuk mendorong perilaku di sekitar sampai semuanya berakhir di lembah.


18
Ini jawaban yang bagus. Jauh lebih baik daripada peringkat teratas. Tapi saya kira hanya mereka yang benar - benar ingin menulis kode yang dapat diuji yang akan menghargainya .. :)
MalcomTucker

22
Saya tidak bisa melupakan betapa baiknya jawaban ini sebenarnya. Ini benar-benar mengubah cara berpikir saya tentang kelas abstrak. Terima kasih Nigel.
MalcomTucker

4
Oh tidak .. prinsip lain yang harus saya pikirkan kembali adalah gong! Terima kasih (baik secara sarkastik untuk saat ini, dan non-sarkastik untuk sekali saya telah mengasimilasi dan merasa seperti programmer yang lebih baik)
Martin Lyne

11
Jawaban bagus. Pasti sesuatu untuk dipikirkan ... tetapi bukankah apa yang Anda katakan pada dasarnya mendidih untuk tidak menggunakan kelas abstrak?
brianestey

32
+1 untuk Aturan saja, "Mendukung jaringan kompleks objek sederhana daripada jaringan objek kompleks sederhana."
David Glass

12

Apa yang saya lakukan untuk kelas dan antarmuka abstrak adalah sebagai berikut: Saya menulis tes, yang menggunakan objek seperti beton. Tetapi variabel tipe X (X adalah kelas abstrak) tidak diatur dalam tes. Kelas uji ini tidak ditambahkan ke suite tes, tetapi subkelasnya, yang memiliki metode pengaturan yang mengatur variabel ke implementasi konkret X. Dengan begitu saya tidak menduplikasi kode uji. Subkelas dari tes yang tidak digunakan dapat menambahkan lebih banyak metode uji jika diperlukan.


bukankah ini menyebabkan masalah casting di subclass? jika X memiliki metode a dan Y mewarisi X tetapi memiliki metode b juga. Ketika Anda mensubkelas kelas uji Anda, bukankah Anda harus melemparkan variabel abstrak ke Y untuk melakukan tes pada b?
Johnno Nolan

8

Untuk membuat tes unit khusus pada kelas abstrak, Anda harus menurunkannya untuk tujuan pengujian, test base.method () hasil dan perilaku yang diinginkan saat mewarisi.

Anda menguji suatu metode dengan memanggilnya jadi uji kelas abstrak dengan mengimplementasikannya ...


8

Jika kelas abstrak Anda berisi fungsionalitas konkret yang memiliki nilai bisnis, maka saya biasanya akan mengujinya secara langsung dengan membuat tes ganda yang mematikan data abstrak, atau dengan menggunakan kerangka kerja mengejek untuk melakukan ini untuk saya. Yang mana saya pilih sangat tergantung pada apakah saya perlu menulis implementasi khusus metode abstrak atau tidak.

Skenario yang paling umum di mana saya perlu melakukan ini adalah ketika saya menggunakan pola Metode Templat , seperti ketika saya sedang membangun semacam kerangka kerja yang dapat diperluas yang akan digunakan oleh pihak ke-3. Dalam hal ini, kelas abstrak adalah apa yang mendefinisikan algoritma yang ingin saya uji, sehingga lebih masuk akal untuk menguji basis abstrak daripada implementasi tertentu.

Namun, saya pikir penting bahwa tes ini harus fokus pada implementasi nyata dari logika bisnis nyata ; Anda tidak boleh menguji unit implementasi detail dari kelas abstrak karena Anda akan berakhir dengan tes rapuh.


6

salah satu caranya adalah dengan menulis test case abstrak yang sesuai dengan kelas abstrak Anda, kemudian menulis test case konkret yang mensubklasifikasikan case test abstrak Anda. lakukan ini untuk setiap subkelas konkret dari kelas abstrak asli Anda (yaitu hirarki kasus uji Anda mencerminkan hierarki kelas Anda). lihat Menguji antarmuka dalam buku penerima junit: http://safari.informit.com/9781932394238/ch02lev1sec6 .

juga lihat Testcase Superclass dalam pola xUnit: http://xunitpatterns.com/Testcase%20Superclass.html


4

Saya akan menentang tes "abstrak". Saya pikir tes adalah ide konkret dan tidak memiliki abstraksi. Jika Anda memiliki elemen umum, letakkan dalam metode pembantu atau kelas untuk digunakan semua orang.

Sedangkan untuk menguji kelas tes abstrak, pastikan Anda bertanya pada diri sendiri apa yang Anda uji. Ada beberapa pendekatan, dan Anda harus mencari tahu apa yang berhasil dalam skenario Anda. Apakah Anda mencoba menguji metode baru di subkelas Anda? Maka tes Anda hanya berinteraksi dengan metode itu. Apakah Anda menguji metode di kelas dasar Anda? Kemudian mungkin memiliki fixture terpisah hanya untuk kelas itu, dan uji setiap metode secara individual dengan tes sebanyak yang diperlukan.


Saya tidak ingin menguji ulang kode yang sudah saya uji itu sebabnya saya turun jalan abstrak tesis. Saya mencoba menguji semua metode konkret di kelas abstrak saya di satu tempat.
Paul Whelan

7
Saya tidak setuju dengan mengekstraksi elemen umum ke kelas pembantu, setidaknya dalam beberapa (banyak?) Kasus. Jika kelas abstrak berisi beberapa fungsi konkret, saya pikir sangat dapat diterima untuk menguji fungsi tersebut secara langsung.
Seth Petry-Johnson

4

Ini adalah pola yang biasanya saya ikuti ketika menyiapkan harness untuk menguji kelas abstrak:

public abstract class MyBase{
  /*...*/
  public abstract void VoidMethod(object param1);
  public abstract object MethodWithReturn(object param1);
  /*,,,*/
}

Dan versi yang saya gunakan sedang diuji:

public class MyBaseHarness : MyBase{
  /*...*/
  public Action<object> VoidMethodFunction;
  public override void VoidMethod(object param1){
    VoidMethodFunction(param1);
  }
  public Func<object, object> MethodWithReturnFunction;
  public override object MethodWithReturn(object param1){
    return MethodWihtReturnFunction(param1);
  }
  /*,,,*/
}

Jika metode abstrak dipanggil ketika saya tidak mengharapkannya, tes gagal. Ketika mengatur tes, saya dapat dengan mudah mematikan metode abstrak dengan lambdas yang melakukan penegasan, melempar pengecualian, mengembalikan nilai yang berbeda, dll.


3

Jika metode konkret memanggil salah satu metode abstrak yang strategi tidak akan berhasil, dan Anda ingin menguji perilaku setiap kelas anak secara terpisah. Kalau tidak, memperluasnya dan mematikan metode abstrak seperti yang telah Anda jelaskan seharusnya baik-baik saja, asalkan metode abstrak kelas beton dipisahkan dari kelas anak.


2

Saya kira Anda mungkin ingin menguji fungsionalitas dasar dari kelas abstrak ... Tapi Anda mungkin akan lebih baik dengan memperluas kelas tanpa mengesampingkan metode apa pun, dan membuat upaya minimum mengejek metode abstrak.


2

Salah satu motivasi utama untuk menggunakan kelas abstrak adalah untuk mengaktifkan polimorfisme dalam aplikasi Anda - yaitu: Anda dapat mengganti versi yang berbeda saat runtime. Bahkan, ini sangat banyak hal yang sama dengan menggunakan antarmuka kecuali kelas abstrak menyediakan beberapa pipa ledeng umum, sering disebut sebagai pola Templat .

Dari perspektif pengujian unit, ada dua hal yang perlu dipertimbangkan:

  1. Interaksi kelas abstrak Anda dengan kelas terkait . Menggunakan kerangka pengujian tiruan sangat ideal untuk skenario ini karena ini menunjukkan bahwa kelas abstrak Anda bermain baik dengan orang lain.

  2. Fungsi kelas turunan . Jika Anda memiliki logika khusus yang Anda tulis untuk kelas turunan Anda, Anda harus menguji kelas-kelas itu secara terpisah.

sunting: RhinoMocks adalah kerangka pengujian tiruan yang mengagumkan yang dapat menghasilkan objek tiruan saat runtime dengan secara dinamis berasal dari kelas Anda. Pendekatan ini dapat menghemat banyak jam dari kelas turunan kode tangan.


2

Pertama jika kelas abstrak berisi beberapa metode konkret saya pikir Anda harus melakukan ini dengan mempertimbangkan contoh ini

 public abstract class A 

 {

    public boolean method 1
    {
        // concrete method which we have to test.

    }


 }


 class B extends class A

 {

      @override
      public boolean method 1
      {
        // override same method as above.

      }


 } 


  class Test_A 

  {

    private static B b;  // reference object of the class B

    @Before
    public void init()

      {

      b = new B ();    

      }

     @Test
     public void Test_method 1

       {

       b.method 1; // use some assertion statements.

       }

   }

1

Jika kelas abstrak sesuai untuk implementasi Anda, uji (seperti yang disarankan di atas) kelas beton turunan. Asumsi Anda benar.

Untuk menghindari kebingungan di masa mendatang, ketahuilah bahwa kelas ujian konkret ini bukan tiruan, tetapi palsu .

Dalam istilah yang ketat, tiruan didefinisikan oleh karakteristik berikut:

  • Mock digunakan sebagai ganti dari setiap dan setiap ketergantungan kelas subjek yang diuji.
  • Mock adalah implementasi semu dari sebuah antarmuka (Anda mungkin ingat bahwa sebagai aturan umum, dependensi harus dinyatakan sebagai antarmuka; testabilitas adalah salah satu alasan utama untuk ini)
  • Perilaku anggota antarmuka tiruan - apakah metode atau properti - disediakan pada saat pengujian (sekali lagi, dengan menggunakan kerangka kerja mengejek). Dengan cara ini, Anda menghindari penggabungan implementasi yang sedang diuji dengan implementasi dependensinya (yang semuanya harus memiliki tes tersendiri).

1

Mengikuti jawaban @ patrick-desjardins, saya menerapkan abstrak dan kelas implementasinya bersama dengan @Testsebagai berikut:

Kelas abstrak - ABC.java

import java.util.ArrayList;
import java.util.List;

public abstract class ABC {

    abstract String sayHello();

    public List<String> getList() {
        final List<String> defaultList = new ArrayList<>();
        defaultList.add("abstract class");
        return defaultList;
    }
}

Sebagai kelas abstrak tidak bisa dipakai, tetapi mereka bisa subclass , kelas beton DEF.java , adalah sebagai berikut:

public class DEF extends ABC {

    @Override
    public String sayHello() {
        return "Hello!";
    }
}

@Test kelas untuk menguji metode abstrak maupun non-abstrak:

import org.junit.Before;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.contains;
import java.util.Collection;
import java.util.List;
import static org.hamcrest.Matchers.equalTo;

import org.junit.Test;

public class DEFTest {

    private DEF def;

    @Before
    public void setup() {
        def = new DEF();
    }

    @Test
    public void add(){
        String result = def.sayHello();
        assertThat(result, is(equalTo("Hello!")));
    }

    @Test
    public void getList(){
        List<String> result = def.getList();
        assertThat((Collection<String>) result, is(not(empty())));
        assertThat(result, contains("abstract class"));
    }
}
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.