Apakah saya tetap bisa memalsukan bagian dari kelas yang sedang diuji?

22

Misalkan saya memiliki kelas (maafkan contoh yang dibuat-buat dan desain buruknya):

class MyProfit
{
  public decimal GetNewYorkRevenue();
  public decimal GetNewYorkExpenses();
  public decimal GetNewYorkProfit();

  public decimal GetMiamiRevenue();
  public decimal GetMiamiExpenses();
  public decimal GetMiamiProfit();

  public bool BothCitiesProfitable();

}

(Perhatikan metode GetxxxRevenue () dan GetxxxExpenses () memiliki dependensi yang terhapus)

Sekarang saya menguji unit BothCitiesProfitable () yang tergantung pada GetNewYorkProfit () dan GetMiamiProfit (). Apakah saya tetap bisa mematikan GetNewYorkProfit () dan GetMiamiProfit ()?

Sepertinya jika saya tidak melakukannya, saya secara bersamaan menguji GetNewYorkProfit () dan GetMiamiProfit () bersama dengan BothCitiesProfitable (). Saya harus memastikan bahwa saya mensetup stubbing untuk GetxxxRevenue () dan GetxxxExpenses () sehingga metode GetxxxProfit () mengembalikan nilai yang benar.

Sejauh ini saya hanya melihat contoh ketergantungan stubbing pada kelas eksternal bukan metode internal.

Dan jika tidak apa-apa, apakah ada pola tertentu yang harus saya gunakan untuk melakukan ini?

MEMPERBARUI

Saya khawatir bahwa kita mungkin kehilangan masalah inti dan itu mungkin kesalahan dari contoh buruk saya. Pertanyaan mendasarnya adalah: jika suatu metode di kelas memiliki ketergantungan pada metode lain yang terbuka untuk umum di kelas yang sama, apakah boleh (atau bahkan disarankan) untuk mematikan metode yang lain?

Mungkin saya kehilangan sesuatu, tetapi saya tidak yakin berpisah dengan kelas selalu masuk akal. Mungkin contoh lain yang lebih baik adalah:

class Person
{
 public string FirstName()
 public string LastName()
 public string FullName()
}

di mana nama lengkap didefinisikan sebagai:

public string FullName()
{
  return FirstName() + " " + LastName();
}

Apakah saya boleh membubarkan FirstName () dan LastName () saat menguji FullName ()?

Pengguna
sumber
Jika Anda menguji beberapa kode secara bersamaan, bagaimana ini bisa menjadi buruk? Apa masalahnya? Biasanya, akan lebih baik untuk menguji kode lebih sering dan lebih intensif.
pengguna tidak diketahui
Baru tahu tentang pembaruan Anda, saya telah memperbarui jawaban saya
Winston Ewert

Jawaban:

27

Anda harus memecah kelas dalam pertanyaan.

Setiap kelas harus melakukan beberapa tugas sederhana. Jika tugas Anda terlalu rumit untuk diuji, maka tugas yang dilakukan kelas terlalu besar.

Mengabaikan kesalahan desain ini:

class NewYork
{
    decimal GetRevenue();
    decimal GetExpenses();
    decimal GetProfit();
}


class Miami
{
    decimal GetRevenue();
    decimal GetExpenses();
    decimal GetProfit();
}

class MyProfit
{
     MyProfit(NewYork new_york, Miami miami);
     boolean bothProfitable();
}

MEMPERBARUI

Masalah dengan metode stubbing di kelas adalah bahwa Anda melanggar enkapsulasi. Tes Anda harus memeriksa untuk melihat apakah perilaku eksternal objek cocok dengan spesifikasi. Apa pun yang terjadi di dalam objek bukan urusannya.

Fakta bahwa FullName menggunakan FirstName dan LastName adalah detail implementasi. Tidak ada yang di luar kelas yang seharusnya peduli bahwa itu benar. Dengan mengejek metode publik untuk menguji objek, Anda membuat asumsi tentang objek yang diimplementasikan.

Di beberapa titik di masa depan, asumsi itu mungkin tidak lagi benar. Mungkin semua logika nama akan dipindahkan ke objek Nama yang orang hanya memanggil. Mungkin FullName akan langsung mengakses variabel anggota first_name dan last_name daripada memanggil FirstName dan LastName.

Pertanyaan kedua adalah mengapa Anda merasa perlu melakukannya. Setelah semua kelas orang Anda dapat diuji sesuatu seperti:

Person person = new Person("John", "Doe");
Test.AssertEquals(person.FullName(), "John Doe");

Anda seharusnya tidak merasakan kebutuhan rintisan apa pun untuk contoh ini. Jika Anda melakukannya maka Anda bahagia dan baik-baik saja ... hentikan! Tidak ada gunanya mengejek metode di sana karena Anda punya kendali atas apa yang ada di dalamnya.

Satu-satunya kasus di mana tampaknya masuk akal untuk metode yang digunakan FullName untuk diejek adalah jika entah bagaimana FirstName () dan LastName () adalah operasi non-sepele. Mungkin Anda sedang menulis salah satu generator nama acak itu, atau FirstName dan LastName meminta database untuk menjawab, atau sesuatu. Tetapi jika itu yang terjadi itu menunjukkan bahwa objek melakukan sesuatu yang bukan milik kelas Person.

Dengan kata lain, mengejek metode adalah mengambil objek dan memecah menjadi dua bagian. Satu bagian sedang diejek sementara bagian lainnya sedang diuji. Apa yang Anda lakukan pada dasarnya adalah pemecahan ad-hoc objek. Jika itu masalahnya, potong saja objeknya.

Jika kelas Anda sederhana, Anda tidak perlu merasa perlu untuk mengejeknya selama ujian. Jika kelas Anda cukup kompleks sehingga Anda merasa perlu mengejek, maka Anda harus memecah kelas menjadi potongan-potongan yang lebih sederhana.

PEMBARUAN LAGI

Cara saya melihatnya, sebuah objek memiliki perilaku eksternal dan internal. Perilaku eksternal termasuk mengembalikan nilai panggilan ke objek lain dll. Jelas, apa pun dalam kategori itu harus diuji. (Kalau tidak, apa yang akan Anda uji?) Tetapi perilaku internal seharusnya tidak benar-benar diuji.

Sekarang perilaku internal diuji, karena itulah yang menghasilkan perilaku eksternal. Tetapi saya tidak menulis tes langsung pada perilaku internal, hanya secara tidak langsung melalui perilaku eksternal.

Jika saya ingin menguji sesuatu, saya pikir itu harus dipindahkan sehingga menjadi perilaku eksternal. Itu sebabnya saya pikir jika Anda ingin mengejek sesuatu, Anda harus membagi objek sehingga hal yang ingin Anda tiru sekarang dalam perilaku eksternal objek yang dimaksud.

Tapi, apa bedanya? Jika FirstName () dan LastName () adalah anggota dari objek lain apakah itu benar-benar mengubah masalah FullName ()? Jika kita memutuskan bahwa perlu mengejek FirstName dan LastName apakah sebenarnya membantu mereka untuk berada di objek lain?

Saya pikir jika Anda menggunakan pendekatan mengejek Anda, maka Anda membuat jahitan di objek. Anda memiliki fungsi seperti FirstName () dan LastName () yang langsung berkomunikasi dengan sumber data eksternal. Anda juga memiliki FullName () yang tidak. Tapi karena mereka semua berada di kelas yang sama sehingga tidak terlihat. Beberapa bagian tidak seharusnya langsung mengakses sumber data dan lainnya. Kode Anda akan lebih jelas jika hanya membagi dua kelompok itu.

EDIT

Mari kita mundur selangkah dan bertanya: mengapa kita mengejek benda ketika kita menguji?

  1. Buat tes berjalan secara konsisten (hindari mengakses hal-hal yang berubah dari lari ke lari)
  2. Hindari mengakses sumber daya yang mahal (jangan tekan layanan pihak ketiga, dll.)
  3. Sederhanakan sistem yang sedang diuji
  4. Buat lebih mudah untuk menguji semua skenario yang mungkin (yaitu hal-hal seperti mensimulasikan kegagalan, dll)
  5. Hindari bergantung pada detail potongan kode lain sehingga perubahan pada potongan kode lainnya tidak akan merusak tes ini.

Sekarang, saya pikir alasan 1-4 tidak berlaku untuk skenario ini. Mengejek sumber eksternal ketika menguji nama lengkap menangani semua alasan tersebut untuk mengejek. Satu-satunya bagian yang tidak ditangani adalah kesederhanaan, tetapi tampaknya objeknya cukup sederhana yang tidak menjadi perhatian.

Saya pikir kekhawatiran Anda adalah alasan nomor 5. Kekhawatirannya adalah bahwa pada suatu saat di masa depan mengubah implementasi FirstName dan LastName akan memecahkan ujian. Di masa depan FirstName dan LastName dapat memperoleh nama-nama dari lokasi atau sumber yang berbeda. Tapi FullName mungkin akan selalu begitu FirstName() + " " + LastName(). Itu sebabnya Anda ingin menguji FullName dengan mengejek FirstName dan LastName.

Apa yang Anda miliki, adalah beberapa bagian dari objek orang yang lebih mungkin berubah daripada yang lain. Sisa objek menggunakan subset ini. Subset tersebut saat ini mengambil datanya menggunakan satu sumber, tetapi dapat mengambil data itu dengan cara yang sama sekali berbeda di kemudian hari. Tapi bagiku itu terdengar seperti subset yang merupakan objek berbeda yang berusaha keluar.

Tampak bagi saya bahwa jika Anda mengejek metode objek Anda membagi objek. Tetapi Anda melakukannya secara ad-hoc. Kode Anda tidak menjelaskan bahwa ada dua bagian berbeda di dalam objek Person Anda. Jadi cukup bagi objek itu dalam kode Anda yang sebenarnya, sehingga jelas dari membaca kode Anda apa yang terjadi. Pilih pemisahan sebenarnya dari objek yang masuk akal dan jangan mencoba untuk membagi objek secara berbeda untuk setiap tes.

Saya menduga Anda mungkin keberatan untuk memisahkan objek Anda, tetapi mengapa?

EDIT

Saya salah.

Anda harus membagi objek daripada memperkenalkan pemisahan ad-hoc dengan mengejek metode individual. Namun, saya terlalu fokus pada satu metode pemisahan objek. Namun, OO menyediakan beberapa metode pemisahan objek.

Apa yang saya usulkan:

class PersonBase
{
      abstract sring FirstName();
      abstract string LastName();

      string FullName()
      {
            return FirstName() + " " + LastName();
      }
 }

 class Person extends PersonBase
 {
      string FirstName(); 
      string LastName();
 }

 class FakePerson extends PersonBase
 {
      void setFirstName(string);
      void setLastName(string);
      string getFirstName();
      string getLastName();
 }

Mungkin itu yang Anda lakukan selama ini. Tetapi saya tidak berpikir metode ini akan memiliki masalah yang saya lihat dengan metode mengejek karena kami telah dengan jelas menggambarkan sisi mana setiap metode aktif. Dan dengan menggunakan warisan, kita menghindari kecanggungan yang akan muncul jika kita menggunakan objek pembungkus tambahan.

Ini memang mengenalkan beberapa kompleksitas, dan hanya untuk beberapa fungsi utilitas saya mungkin baru saja mengujinya dengan mengejek sumber pihak ke-3 yang mendasarinya. Tentu, mereka berada dalam bahaya yang semakin besar untuk dipatahkan tetapi itu tidak layak disusun ulang. Jika Anda punya objek yang cukup kompleks sehingga Anda perlu membaginya, maka saya pikir sesuatu seperti ini adalah ide yang bagus.

Winston Ewert
sumber
8
Kata baik. Jika Anda mencoba mencari solusi dalam pengujian, Anda mungkin perlu mempertimbangkan kembali desain Anda (sebagai catatan, sekarang suara Frank Sinatra tertahan di kepala saya).
Bermasalah
2
"Dengan mengejek metode publik untuk menguji objek, Anda membuat asumsi tentang [bagaimana] objek itu diimplementasikan." Tapi bukankah itu masalahnya setiap kali Anda mematikan objek? Sebagai contoh, misalkan saya tahu metode saya xyz () memanggil beberapa objek lain. Untuk menguji xyz () secara terpisah, saya harus mematikan objek lainnya. Oleh karena itu pengujian saya harus tahu tentang detail implementasi metode xyz () saya.
Pengguna
Dalam kasus saya, metode "FirstName ()" dan "LastName ()" sederhana tetapi mereka melakukan query api pihak ketiga untuk hasilnya.
Pengguna
@ Pengguna, diperbarui, pasti Anda mengejek api pihak ke-3 untuk menguji FirstName dan LastName. Apa yang salah dengan melakukan ejekan yang sama saat menguji nama lengkap?
Winston Ewert
@ Winston, itu semacam inti saya, saat ini saya mengejek api pihak ke-3 yang digunakan dalam nama depan dan nama belakang untuk menguji nama lengkap (), tapi saya lebih suka tidak peduli tentang bagaimana nama depan dan nama belakang diterapkan ketika menguji nama lengkap (tentu saja tidak apa-apa saat saya menguji nama depan dan nama belakang). Maka pertanyaan saya tentang mengejek nama depan dan nama belakang.
Pengguna
10

Sementara saya setuju dengan jawaban Winston Ewert, kadang-kadang tidak mungkin untuk memecah kelas, apakah karena kendala waktu, API sudah digunakan di tempat lain, atau apa pun.

Dalam kasus seperti itu, alih-alih metode mengejek, saya akan menulis tes saya untuk menutupi kelas secara bertahap. Uji getExpenses()dan getRevenue()metode, lalu uji getProfit()metode, lalu uji metode bersama. Memang benar bahwa Anda akan memiliki lebih dari satu tes yang mencakup metode tertentu, tetapi karena Anda menulis tes yang lulus untuk mencakup metode secara individual, Anda dapat yakin bahwa output Anda dapat diandalkan diberikan input yang diuji.

Bermasalah
sumber
1
Sepakat. Tetapi hanya untuk menekankan poin bagi siapa saja yang membaca ini, ini solusi yang Anda gunakan jika Anda tidak dapat melakukan solusi yang lebih baik.
Winston Ewert
5
@ Winston, dan ini adalah solusi yang Anda gunakan sebelum sampai ke solusi yang lebih baik. Yaitu memiliki bola besar kode warisan, Anda harus menutupinya dengan unit test sebelum Anda bisa refactor.
Péter Török
@ Péter Török, poin bagus. Meskipun saya pikir itu tercakup dalam "tidak dapat melakukan solusi yang lebih baik." :)
Winston Ewert
@all Menguji metode getProfit () akan sangat sulit karena skenario yang berbeda untuk getExpenses () dan getRevenue () sebenarnya akan melipatgandakan skenario yang diperlukan untuk getProfit (). jika kita menguji getExpenses () dan mendapatkan Revenue () secara independen Apakah bukan ide yang baik untuk mengejek metode ini untuk menguji getProfit ()?
Mohd Farid
7

Untuk menyederhanakan contoh Anda lebih lanjut, katakan Anda sedang menguji C(), yang tergantung pada A()dan B(), masing-masing memiliki dependensi sendiri. IMO tergantung pada apa yang coba Anda capai.

Jika Anda menguji perilaku perilaku C()diketahui yang diketahui A()dan B()kemudian mungkin paling mudah dan terbaik untuk mematikan A()dan B(). Ini mungkin akan disebut unit test oleh puritan.

Jika Anda menguji perilaku seluruh sistem (dari C()perspektif), saya akan pergi A()dan B()menerapkan dan mematikan dependensi mereka (jika memungkinkan untuk menguji) atau mengatur lingkungan kotak pasir, seperti tes basis data. Saya akan menyebutnya tes integrasi.

Kedua pendekatan ini mungkin valid, jadi jika itu dirancang sehingga Anda dapat mengujinya di muka mungkin akan lebih baik dalam jangka panjang.

Rebecca Scott
sumber