Duplikasi konstanta antara tes dan kode produksi?

20

Apakah baik atau buruk untuk menggandakan data antara tes dan kode nyata? Sebagai contoh, misalkan saya memiliki kelas Python FooSaveryang menyimpan file dengan nama tertentu ke direktori yang diberikan:

class FooSaver(object):
  def __init__(self, out_dir):
    self.out_dir = out_dir

  def _save_foo_named(self, type_, name):
    to_save = None
    if type_ == FOOTYPE_A:
      to_save = make_footype_a()
    elif type == FOOTYPE_B:
      to_save = make_footype_b()
    # etc, repeated
    with open(self.out_dir + name, "w") as f:
      f.write(str(to_save))

  def save_type_a(self):
    self._save_foo_named(a, "a.foo_file")

  def save_type_b(self):
    self._save_foo_named(b, "b.foo_file")

Sekarang dalam pengujian saya, saya ingin memastikan bahwa semua file ini dibuat, jadi saya ingin mengatakan sesuatu seperti ini:

foo = FooSaver("/tmp/special_name")
foo.save_type_a()
foo.save_type_b()

self.assertTrue(os.path.isfile("/tmp/special_name/a.foo_file"))
self.assertTrue(os.path.isfile("/tmp/special_name/b.foo_file"))

Meskipun ini menduplikasi nama file di dua tempat, saya pikir itu bagus: itu memaksa saya untuk menuliskan dengan tepat apa yang saya harapkan untuk keluar dari ujung yang lain, itu menambahkan lapisan perlindungan terhadap kesalahan ketik, dan umumnya membuat saya merasa yakin bahwa segala sesuatu bekerja dengan baik persis seperti yang saya harapkan. Saya tahu bahwa jika saya beralih a.foo_fileke type_a.foo_filemasa depan saya harus melakukan beberapa pencarian dan penggantian dalam tes saya, tapi saya pikir itu bukan masalah besar. Saya lebih suka memiliki beberapa hal positif yang salah jika saya lupa memperbarui tes sebagai imbalan untuk memastikan bahwa pemahaman saya tentang kode dan tes-tes tersebut selaras.

Seorang rekan kerja berpendapat bahwa duplikasi ini buruk, dan merekomendasikan agar saya memperbaiki kedua belah pihak dengan hal seperti ini:

class FooSaver(object):
  A_FILENAME = "a.foo_file"
  B_FILENAME = "b.foo_file"

  # as before...

  def save_type_a(self):
    self._save_foo_named(a, self.A_FILENAME)

  def save_type_b(self):
    self._save_foo_named(b, self.B_FILENAME)

dan dalam ujian:

self.assertTrue(os.path.isfile("/tmp/special_name/" + FooSaver.A_FILENAME))
self.assertTrue(os.path.isfile("/tmp/special_name/" + FooSaver.B_FILENAME))

Saya tidak suka ini karena itu tidak membuat saya yakin bahwa kode melakukan apa yang saya harapkan --- Saya baru saja menggandakan out_dir + namelangkah pada sisi produksi dan sisi pengujian. Itu tidak akan mengungkap kesalahan dalam pemahaman saya tentang cara +kerjanya pada string, dan itu tidak akan menangkap kesalahan ketik.

Di sisi lain, itu jelas kurang rapuh daripada menulis string itu dua kali, dan tampaknya sedikit salah bagi saya untuk menggandakan data di dua file seperti itu.

Apakah ada preseden yang jelas di sini? Apakah boleh menggandakan konstanta di seluruh tes dan kode produksi, atau terlalu rapuh?

Patrick Collins
sumber

Jawaban:

16

Saya pikir itu tergantung pada apa yang Anda coba uji, yang sesuai dengan apa kontrak kelas itu.

Jika kontrak kelas persis FooSavermenghasilkan a.foo_filedan b.foo_filedi lokasi tertentu, maka Anda harus mengujinya secara langsung, yaitu menduplikasi konstanta dalam tes.

Namun, jika kontrak kelas adalah bahwa ia menghasilkan dua file menjadi area sementara, nama masing-masing yang mudah diubah, terutama saat runtime, maka Anda harus menguji lebih umum, mungkin menggunakan konstanta yang difaktorkan keluar dari tes.

Jadi Anda harus berdebat dengan rekan kerja Anda tentang sifat sebenarnya dan kontrak kelas dari perspektif desain domain tingkat yang lebih tinggi. Jika Anda tidak bisa setuju maka saya akan mengatakan bahwa ini adalah masalah tingkat pemahaman dan abstraksi dari kelas itu sendiri, daripada mengujinya.

Juga masuk akal untuk menemukan kontrak kelas berubah selama refactoring, misalnya, agar tingkat abstraksinya dinaikkan seiring waktu. Pada awalnya, ini tentang dua file tertentu di lokasi temp tertentu, tetapi seiring waktu, Anda mungkin menemukan abstraksi tambahan dijamin. Pada saat itu, ubah tes agar tetap sinkron dengan kontrak kelas. Tidak perlu terlalu membangun kontrak kelas segera hanya karena Anda sedang mengujinya (YAGNI).

Ketika suatu kontrak kelas tidak didefinisikan dengan baik, pengujiannya dapat membuat kita mempertanyakan sifat kelas, tetapi akan menggunakannya. Saya akan mengatakan bahwa Anda tidak harus meningkatkan kontrak kelas hanya karena Anda mengujinya; Anda harus memutakhirkan kontrak kelas karena alasan lain, seperti, itu adalah abstraksi yang lemah untuk domain tersebut, dan jika tidak maka coba tes apa adanya.

Erik Eidt
sumber
4

Apa yang disarankan oleh @Erik - dalam hal memastikan Anda jelas tentang apa yang Anda uji - tentu harus menjadi pertimbangan utama Anda.

Tetapi haruskah keputusan Anda membawa Anda ke arah memfaktorkan konstanta, yang meninggalkan bagian menarik dari pertanyaan Anda (parafrase) "Mengapa saya harus menukar konstanta duplikat untuk kode duplikasi?". (Mengacu pada tempat Anda berbicara tentang "menduplikat langkah nama + out_dir".)

Saya percaya bahwa (modulo komentar Erik) kebanyakan situasi lakukan manfaat dari menghapus duplikat konstanta. Tetapi Anda perlu melakukan ini dengan cara yang tidak menggandakan kode. Dalam contoh khusus Anda, ini mudah. Alih-alih berurusan dengan jalur sebagai string "mentah" dalam kode produksi Anda, perlakukan jalur sebagai jalur. Ini adalah cara yang lebih kuat untuk bergabung dengan komponen jalur daripada penggabungan string:

os.path.join(self.out_dir, name)

Di kode tes Anda, di sisi lain, saya akan merekomendasikan sesuatu seperti ini. Di sini, penekanannya menunjukkan bahwa Anda memiliki jalur dan Anda "memasukkan" nama file daun:

self.assertTrue(os.path.isfile("/tmp/special_name/{0}".format(FooSaver.A_FILENAME)))

Artinya, dengan pemilihan elemen bahasa yang lebih bijaksana, Anda dapat secara otomatis menghindari duplikasi kode. (Tidak sepanjang waktu, tetapi sangat sering dalam pengalaman saya.)

Michael Sorens
sumber
1

Saya setuju dengan jawaban Erik Eidt, tetapi ada opsi ketiga: matikan konstanta dalam tes, jadi tes akan lulus bahkan jika Anda mengubah nilai konstanta dalam kode produksi.

(Lihat mematikan konstanta di python unittest )

foo = FooSaver("/tmp/special_name")
foo.save_type_a()
foo.save_type_b()

with mock.patch.object(FooSaver, 'A_FILENAME', 'unique_to_your_test_a'):
  self.assertTrue(os.path.isfile("/tmp/special_name/unique_to_your_test_a"))
with mock.patch.object(FooSaver, 'B_FILENAME', 'unique_to_your_test_b'):
  self.assertTrue(os.path.isfile("/tmp/special_name/unique_to_your_test_b"))

Dan ketika melakukan hal-hal seperti ini saya biasanya akan memastikan untuk melakukan tes kewarasan di mana saya menjalankan tes tanpa withpernyataan dan pastikan saya melihat "'a.foo_file'! = 'Unique_to_your_test_a'", lalu masukkan withpernyataan itu kembali dalam tes jadi itu lewat lagi.

alexanderbird
sumber