Bagaimana cara melakukan TDD untuk sesuatu dengan banyak permutasi?

15

Saat membuat sistem seperti AI, yang dapat mengambil banyak jalur berbeda dengan sangat cepat, atau benar-benar algoritma apa pun yang memiliki beberapa input berbeda, rangkaian hasil yang mungkin dapat berisi sejumlah besar permutasi.

Pendekatan apa yang harus dilakukan seseorang untuk menggunakan TDD ketika membuat sistem yang menghasilkan banyak, banyak permutasi hasil yang berbeda?

Nicole
sumber
1
Kebaikan keseluruhan sistem AI biasanya diukur dengan uji Precision-Recall dengan set input benchmark. Tes ini kira-kira setara dengan "tes integrasi". Seperti yang disebutkan orang lain, ini lebih seperti "riset algoritma yang digerakkan" daripada " desain yang digerakkan oleh tes ".
rwong
Silakan tentukan apa yang Anda maksud dengan "AI". Ini adalah bidang studi lebih dari jenis program tertentu. Untuk implementasi AI tertentu, Anda biasanya tidak dapat menguji beberapa jenis hal (yaitu: perilaku emergent) melalui TDD.
Steven Evers
@SnOrfus Maksud saya dalam arti yang paling umum, belum sempurna, mesin pembuat keputusan.
Nicole

Jawaban:

7

Mengambil pendekatan yang lebih praktis untuk jawaban pdr . TDD adalah semua tentang desain perangkat lunak daripada pengujian. Anda menggunakan tes unit untuk memverifikasi pekerjaan Anda saat Anda melanjutkan.

Jadi pada level unit test Anda perlu mendesain unit sehingga mereka dapat diuji dengan cara yang sepenuhnya deterministik. Anda dapat melakukan ini dengan mengambil apa pun yang membuat unit tidak deterministik (seperti generator angka acak) dan abstrak itu. Katakanlah kita memiliki contoh naif dari metode yang memutuskan apakah suatu langkah baik atau tidak:

class Decider {

  public boolean decide(float input, float risk) {

      float inputRand = Math.random();
      if (inputRand > input) {
         float riskRand = Math.random();
      }
      return false;

  }

}

// The usage:
Decider d = new Decider();
d.decide(0.1337f, 0.1337f);

Metode ini sangat sulit untuk diuji dan satu-satunya hal yang benar-benar dapat Anda verifikasi dalam unit test adalah batasnya ... tetapi itu membutuhkan banyak upaya untuk mencapai batas tersebut. Jadi alih-alih, mari abaikan bagian pengacakan dengan membuat antarmuka dan kelas konkret yang membungkus fungsi:

public interface IRandom {

   public float random();

}

public class ConcreteRandom implements IRandom {

   public float random() {
      return Math.random();
   }

}

The Deciderkelas sekarang perlu untuk menggunakan kelas beton melalui abstraksi, yaitu Interface. Cara melakukan hal ini disebut dependensi injeksi (contoh di bawah ini adalah contoh injeksi konstruktor, tetapi Anda juga bisa melakukannya dengan setter):

class Decider {

  IRandom irandom;

  public Decider(IRandom irandom) { // constructor injection
      this.irandom = irandom;
  }

  public boolean decide(float input, float risk) {

      float inputRand = irandom.random();
      if (inputRand > input) {
         float riskRand = irandom.random();
      }
      return false;

  }

}

// The usage:
Decider d = new Decider(new ConcreteRandom);
d.decide(0.1337f, 0.1337f);

Anda mungkin bertanya pada diri sendiri mengapa "kode mengasapi" ini diperlukan. Nah, untuk permulaan, Anda sekarang dapat mengejek perilaku bagian acak dari algoritma karena Decidersekarang memiliki ketergantungan yang mengikuti IRandom"kontrak". Anda dapat menggunakan kerangka kerja mengejek untuk ini, tetapi contoh ini cukup sederhana untuk membuat kode sendiri:

class MockedRandom() implements IRandom {

    public List<Float> floats = new ArrayList<Float>();
    int pos;

   public void addFloat(float f) {
     floats.add(f);
   }

   public float random() {
      float out = floats.get(pos);
      if (pos != floats.size()) {
         pos++;
      }
      return out;
   }

}

Bagian terbaiknya adalah ini sepenuhnya dapat menggantikan implementasi konkret "aktual". Kode menjadi mudah diuji seperti ini:

@Before void setUp() {
  MockedRandom mRandom = new MockedRandom();

  Decider decider = new Decider(mRandom);
}

@Test
public void testDecisionWithLowInput_ShouldGiveFalse() {

  mRandom.addFloat(0f);

  assertFalse(decider.decide(0.1337f, 0.1337f));
}

@Test
public void testDecisionWithHighInputRandButLowRiskRand_ShouldGiveFalse() {

  mRandom.addFloat(1f);
  mRandom.addFloat(0f);

  assertFalse(decider.decide(0.1337f, 0.1337f));
}

@Test
public void testDecisionWithHighInputRandAndHighRiskRand_ShouldGiveTrue() {

  mRandom.addFloat(1f);
  mRandom.addFloat(1f);

  assertTrue(decider.decide(0.1337f, 0.1337f));
}

Semoga ini memberi Anda ide tentang bagaimana merancang aplikasi Anda sehingga permutasi dapat dipaksa sehingga Anda dapat menguji semua kasus tepi dan yang lainnya.

Spoike
sumber
3

TDD yang ketat memang cenderung memecah sedikit untuk sistem yang lebih kompleks, tetapi itu tidak masalah terlalu banyak dalam hal praktis - setelah Anda melampaui kemampuan untuk mengisolasi input individu, cukup pilih beberapa kasus uji yang memberikan cakupan yang masuk akal dan menggunakannya.

Ini memang membutuhkan pengetahuan tentang implementasi apa yang akan dilakukan dengan baik, tetapi itu lebih merupakan perhatian teoritis - Anda sangat tidak mungkin membangun AI yang ditentukan secara rinci oleh pengguna non-teknis. Ini dalam kategori yang sama dengan melewati tes dengan hardcoding ke kasus uji - tes resmi adalah spec dan implementasinya benar dan solusi tercepat yang mungkin, tetapi tidak pernah benar-benar terjadi.

Tom Clarkson
sumber
2

TDD bukan tentang pengujian, ini tentang desain.

Jauh dari keruntuhan dengan kerumitan, ia unggul dalam keadaan ini. Ini akan mengarahkan Anda untuk mempertimbangkan masalah yang lebih besar dalam potongan-potongan kecil, yang akan mengarah pada desain yang lebih baik.

Jangan berangkat untuk mencoba menguji setiap permutasi algoritma Anda. Cukup buat tes demi tes, tulis kode paling sederhana untuk membuat tes bekerja, sampai Anda memiliki basis Anda tertutup. Anda harus melihat apa yang saya maksud dengan memecahkan masalah karena Anda akan didorong untuk memalsukan bagian dari masalah saat menguji bagian lain, untuk menyelamatkan diri Anda dari menulis 10 miliar tes untuk 10 miliar permutasi.

Sunting: Saya ingin menambahkan contoh, tetapi tidak punya waktu sebelumnya.

Mari kita pertimbangkan algoritma in-place-sort. Kita bisa melanjutkan dan menulis tes yang meliputi ujung atas array, ujung bawah array dan segala macam kombinasi aneh di tengah. Untuk masing-masing, kita harus membangun array lengkap dari beberapa jenis objek. Ini akan memakan waktu.

Atau kita bisa mengatasi masalah dalam empat bagian:

  1. Lintasi array.
  2. Bandingkan item yang dipilih.
  3. Ganti item.
  4. Mengkoordinasikan tiga di atas.

Yang pertama adalah satu-satunya bagian rumit dari masalah tetapi dengan mengabstraksikannya dari yang lain, Anda telah membuatnya jauh, lebih sederhana.

Yang kedua hampir pasti ditangani oleh objek itu sendiri, setidaknya secara opsional, dalam banyak kerangka kerja statis-ketik akan ada antarmuka untuk menunjukkan apakah fungsionalitas itu diimplementasikan. Jadi, Anda tidak perlu menguji ini.

Yang ketiga sangat mudah untuk diuji.

Yang keempat hanya menangani dua pointer, meminta kelas traversal untuk memindahkan pointer, meminta perbandingan dan berdasarkan hasil perbandingan itu, panggilan untuk item yang akan ditukar. Jika Anda telah memalsukan tiga masalah pertama, Anda dapat menguji ini dengan sangat mudah.

Bagaimana kami menghasilkan desain yang lebih baik di sini? Katakanlah Anda membuatnya sederhana dan menerapkan semacam gelembung. Ini bekerja tetapi, ketika Anda pergi ke produksi dan harus menangani sejuta objek, itu terlalu lambat. Yang harus Anda lakukan adalah menulis fungsionalitas traversal baru dan menukarnya. Anda tidak harus berurusan dengan kompleksitas penanganan tiga masalah lainnya.

Ini, Anda akan temukan, adalah perbedaan antara pengujian unit dan TDD. Penguji unit akan mengatakan bahwa ini telah membuat tes Anda rapuh, bahwa jika Anda telah menguji input dan output sederhana maka Anda tidak perlu lagi menulis tes untuk fungsionalitas baru Anda. TDDer akan mengatakan bahwa saya telah memisahkan masalah dengan tepat sehingga setiap kelas yang saya miliki melakukan satu hal dan satu hal dengan baik.

pdr
sumber
1

Tidaklah mungkin untuk menguji setiap permutasi suatu perhitungan dengan banyak variabel. Tapi itu bukan hal yang baru, selalu benar adanya program di atas kompleksitas mainan. Titik pengujian adalah untuk memverifikasi properti perhitungan. Misalnya, menyortir daftar dengan 1000 angka memerlukan sedikit usaha, tetapi setiap solusi individual dapat diverifikasi dengan sangat mudah. Sekarang, meskipun ada 1000! mungkin (kelas) input untuk progam itu dan Anda tidak dapat menguji semuanya, itu benar-benar cukup untuk hanya menghasilkan 1000 input secara acak dan memverifikasi bahwa outputnya, memang diurutkan. Mengapa? Karena hampir tidak mungkin untuk menulis sebuah program yang secara andal mengurutkan 1000 vektor yang dihasilkan secara acak tanpa juga mengoreksi secara umum (kecuali jika Anda dengan sengaja memasang rig untuk memanipulasi input sihir tertentu ...)

Sekarang, secara umum hal-hal sedikit lebih rumit. Benar-benar ada bug di mana pengirim tidak akan mengirimkan email kepada pengguna jika mereka memiliki 'f' di nama pengguna mereka dan hari dalam seminggu adalah hari Jumat. Tapi saya menganggap itu usaha sia-sia mencoba mengantisipasi keanehan semacam itu. Rangkaian pengujian Anda harus memberi Anda keyakinan mantap bahwa sistem melakukan apa yang Anda harapkan pada input yang Anda harapkan. Jika ia melakukan hal-hal yang funky dalam kasus-kasus funky tertentu, Anda akan segera mengetahui setelah Anda mencoba kasing funky pertama, dan kemudian Anda dapat menulis tes khusus terhadap kasing (yang biasanya juga akan mencakup seluruh kelas kasing yang serupa).

Kilian Foth
sumber
Mengingat Anda menghasilkan 1000 input secara acak, bagaimana Anda menguji output? Tentunya tes semacam itu akan melibatkan beberapa logika, yang dengan sendirinya tidak diuji. Jadi Anda menguji tesnya? Bagaimana? Intinya adalah, Anda harus menguji logika menggunakan transisi negara - diberikan input X output harus Y. Tes yang melibatkan logika rentan terhadap kesalahan sebanyak logika yang diuji. Dalam istilah logis, membenarkan argumen dengan argumen lain menempatkan Anda pada jalur regresi skeptis - Anda harus membuat beberapa pernyataan. Pernyataan ini adalah tes Anda.
Izhaki
0

Ambil case tepi ditambah beberapa input acak.

Untuk mengambil contoh penyortiran:

  • Mengurutkan beberapa daftar acak
  • Ambil daftar yang sudah diurutkan
  • Ambil daftar yang berada dalam urutan terbalik
  • Ambil daftar yang hampir diurutkan

Jika bekerja cepat untuk ini, Anda bisa yakin itu akan bekerja untuk semua input.

Carra
sumber