Haruskah hasil tes unit diharapkan di-hardcode?

29

Haruskah hasil yang diharapkan dari unit test hardcode, atau dapatkah mereka bergantung pada variabel yang diinisialisasi? Apakah hasil hardcoded atau dihitung meningkatkan risiko memperkenalkan kesalahan dalam unit test? Apakah ada faktor lain yang belum saya pertimbangkan?

Misalnya, yang mana dari dua ini adalah format yang lebih dapat diandalkan?

[TestMethod]
public void GetPath_Hardcoded()
{
    MyClass target = new MyClass("fields", "that later", "determine", "a folder");
    string expected = "C:\\Output Folder\\fields\\that later\\determine\\a folder";
    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}

[TestMethod]
public void GetPath_Softcoded()
{
    MyClass target = new MyClass("fields", "that later", "determine", "a folder");
    string expected = "C:\\Output Folder\\" + string.Join("\\", target.Field1, target.Field2, target.Field3, target.Field4);
    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}

EDIT 1: Menanggapi jawaban DXM, apakah opsi 3 solusi yang disukai?

[TestMethod]
public void GetPath_Option3()
{
    string field1 = "fields";
    string field2 = "that later";
    string field3 = "determine";
    string field4 = "a folder";
    MyClass target = new MyClass(field1, field2, field3, field4);
    string expected = "C:\\Output Folder\\" + string.Join("\\", field1, field2, field3, field4);
    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}
Makanan Tangan
sumber
2
Lakukan keduanya. Serius. Tes dapat dan harus tumpang tindih. Lihat juga beberapa jenis tes yang digerakkan oleh data jika Anda menemukan diri Anda berhadapan dengan nilai-nilai hard-coded.
Ayub
Saya setuju opsi ketiga adalah apa yang ingin saya gunakan. Saya tidak berpikir opsi 1 akan sakit karena Anda menghilangkan manipulasi pada kompilasi.
kwelch
Kedua opsi Anda menggunakan hardcoding dan akan pecah jika tes tidak berjalan pada C: \\
Qwertie

Jawaban:

27

Saya pikir hasil nilai yang dihitung menghasilkan kasus uji yang lebih kuat dan fleksibel. Juga dengan menggunakan nama variabel yang baik dalam ekspresi yang menghitung hasil yang diharapkan, jauh lebih jelas dari mana hasil yang diharapkan berasal dari tempat pertama.

Karena itu, dalam contoh spesifik Anda, saya TIDAK akan mempercayai metode "Softcoded" karena menggunakan SUT Anda (sistem yang diuji) sebagai input untuk perhitungan Anda. Jika ada bug di MyClass di mana bidang tidak disimpan dengan benar, pengujian Anda akan benar-benar lulus karena perhitungan nilai yang Anda harapkan akan menggunakan string yang salah seperti target.GetPath ().

Saran saya adalah untuk menghitung nilai yang diharapkan di tempat yang masuk akal, tetapi pastikan bahwa perhitungan tidak bergantung pada kode apa pun dari SUT itu sendiri.

Sebagai Tanggapan atas pembaruan OP terhadap respons saya:

Ya, berdasarkan pengetahuan saya tetapi pengalaman yang agak terbatas dalam melakukan TDD, saya akan memilih opsi # 3.

DXM
sumber
1
Poin bagus! Jangan mengandalkan objek yang tidak diverifikasi dalam tes.
Hand-E-Food
bukan duplikasi kode SUT?
Abyx
1
dengan cara itu, tapi itulah bagaimana Anda memverifikasi bahwa SUT berfungsi. Jika kami menggunakan kode yang sama dan rusak, Anda tidak akan pernah tahu. Tentu saja, jika untuk melakukan perhitungan, Anda perlu menduplikasi banyak SUT, maka mungkin opsi # 1 akan menjadi lebih baik, cukup hardcode nilainya.
DXM
16

Bagaimana jika kodenya adalah sebagai berikut:

MyTarget() // constructor
{
   Field1 = Field2 = Field3 = Field4 = "";
}

Contoh kedua Anda tidak akan menangkap bug, tetapi contoh pertama akan melakukannya.

Secara umum, saya akan merekomendasikan terhadap soft-coding karena dapat menyembunyikan bug. Sebagai contoh:

string expected = "C:\\Output Folder" + string.Join("\\", target.Field1, target.Field2, target.Field3, target.Field4);

Bisakah Anda menemukan masalahnya? Anda tidak akan membuat kesalahan yang sama dalam versi kode-keras. Lebih sulit untuk mendapatkan perhitungan yang benar daripada nilai-nilai yang dikodekan. Itu sebabnya saya lebih suka bekerja dengan nilai-nilai kode-keras daripada yang kode-lunak.

Tapi ada beberapa pengecualian. Bagaimana jika kode Anda harus dijalankan di Windows dan Linux? Path tidak hanya harus berbeda, tetapi juga harus menggunakan separator path yang berbeda! Menghitung jalur menggunakan fungsi yang abstrak perbedaan antara mungkin masuk akal dalam konteks itu.

Winston Ewert
sumber
Saya mendengar apa yang Anda katakan dan itu memberi saya sesuatu untuk dipertimbangkan. Softcoding bergantung pada test case saya yang lain (seperti ConstructorShouldCorrectlyInitialiseFields) yang lewat. Kegagalan yang Anda gambarkan akan dirujuk silang oleh tes unit lain yang gagal.
Hand-E-Food
@ Hand-E-Food, sepertinya Anda menulis tes pada metode individual objek Anda. Jangan. Anda harus menulis tes yang memeriksa kebenaran seluruh objek Anda bersama-sama bukan metode individual. Kalau tidak, tes Anda akan rapuh sehubungan dengan perubahan di dalam objek.
Winston Ewert
Saya tidak yakin saya mengikuti. Contoh yang saya berikan adalah murni hipotesis, skenario yang mudah dipahami. Saya menulis unit test untuk menguji anggota kelas dan objek publik. Apakah itu cara yang benar untuk menggunakannya?
Hand-E-Food
@ Hand-E-Food, jika saya mengerti Anda dengan benar, pengujian Anda ConstructShouldCorrectlyInitialiseFields akan memanggil konstruktor dan kemudian menyatakan bahwa bidang diatur dengan benar. Tetapi Anda tidak harus melakukan itu. Anda seharusnya tidak peduli apa yang dilakukan bidang internal. Anda hanya harus menyatakan bahwa perilaku eksternal objek sudah benar. Jika tidak, saatnya akan tiba ketika Anda perlu mengganti implementasi internal. Jika Anda telah membuat pernyataan tentang kondisi internal, semua tes Anda akan rusak. Tetapi jika Anda hanya membuat pernyataan tentang perilaku eksternal, semuanya akan tetap berfungsi.
Winston Ewert
@ Winston - Saya sebenarnya sedang dalam proses membaca buku Pola Uji xUnit dan sebelum itu menyelesaikan The Art of Unit Testing. Saya tidak akan berpura-pura tahu apa yang saya bicarakan, tetapi saya ingin berpikir saya mengambil sesuatu dari buku-buku itu. Kedua buku sangat merekomendasikan bahwa setiap metode pengujian harus menguji minimum absolut dan Anda harus memiliki banyak kasus uji untuk menguji seluruh objek Anda. Dengan begitu ketika antarmuka atau fungsi berubah, Anda seharusnya hanya memperbaiki beberapa metode pengujian, bukan sebagian besar. Dan karena mereka kecil, perubahan harus lebih mudah.
DXM
4

Menurut pendapat saya, kedua saran Anda kurang ideal. Cara ideal untuk melakukannya adalah yang ini:

[TestMethod]
public void GetPath_Hardcoded()
{
    const string f1 = "fields"; const string f2 = "that later"; 
    const string f3 = "determine"; const string f4 = "a folder";

    MyClass target = new MyClass( f1, f2, f3, f4 );
    string expected = "C:\\Output Folder\\" + string.Join("\\", f1, f2, f3, f4);
    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}

Dengan kata lain, tes harus bekerja secara eksklusif berdasarkan input dan output objek, dan tidak didasarkan pada keadaan internal objek. Objek harus diperlakukan sebagai kotak hitam. (Saya mengabaikan masalah lain, seperti ketidaksesuaian menggunakan string. Bergabunglah dengan bukan Path.Combine, karena ini hanya sebuah contoh.)

Mike Nakis
sumber
1
Tidak semua metode berfungsi - banyak yang benar memiliki efek samping yang mengubah keadaan beberapa objek atau objek. Tes unit untuk metode dengan efek samping mungkin perlu mengevaluasi keadaan objek yang dipengaruhi oleh metode tersebut.
Matthew Flynn
Maka keadaan itu akan dianggap sebagai output dari metode. Maksud dari pengujian sampel ini adalah untuk memeriksa metode GetPath (), bukan konstruktor MyClass. Baca jawaban @ DXM, ia memberikan alasan yang sangat bagus untuk mengambil pendekatan kotak hitam.
Mike Nakis
@ MatthewFlynn, maka Anda harus menguji metode yang dipengaruhi oleh keadaan itu. Keadaan internal yang tepat adalah detail implementasi dan bukan urusan bisnis tes.
Winston Ewert
@ MatthewFlynn, hanya untuk memperjelas, apakah itu terkait dengan contoh yang ditampilkan, atau sesuatu yang perlu dipertimbangkan untuk pengujian unit lainnya? Saya bisa melihat hal itu penting untuk sesuatu seperti target.Dispose(); Assert.IsTrue(target.IsDisposed);(contoh yang sangat sederhana.)
Hand-E-Food
Bahkan dalam kasus ini, properti IsDisposed adalah (atau harus) bagian yang sangat diperlukan dari antarmuka publik kelas, dan bukan detail implementasi. (Antarmuka IDispose tidak menyediakan properti seperti itu, tapi itu tidak menguntungkan.)
Mike Nakis
2

Ada dua aspek dalam diskusi:

1. Menggunakan target itu sendiri untuk test case
Pertanyaan pertama adalah harus / dapatkah Anda menggunakan kelas itu sendiri untuk mengandalkan dan menyelesaikan bagian dari pekerjaan dalam test stub? - Jawabannya adalah TIDAK karena, secara umum, Anda tidak boleh membuat asumsi tentang kode yang Anda uji. Jika ini tidak dilakukan dengan benar, seiring waktu bug menjadi kebal terhadap beberapa unit pengujian.

2. Hardcoding
haruskah Anda kode keras ? Sekali lagi jawabannya adalah Tidak . karena seperti perangkat lunak apa pun - pengkodean keras informasi menjadi sulit ketika berbagai hal berevolusi. Misalnya, ketika Anda ingin jalur di atas dimodifikasi lagi, Anda harus menulis unit tambahan atau terus memodifikasi. Metode yang lebih baik adalah menyimpan input dan tanggal evaluasi yang berasal dari konfigurasi terpisah yang dapat dengan mudah disesuaikan.

misalnya di sini adalah bagaimana saya akan memperbaiki rintisan tes.

[TestMethod]
public void GetPath_Tested(int CaseId)
{
    testParams = GetTestConfig(caseID,"testConfig.txt"); // some wrapper that does read line and chops the field. 
    MyClass target = new MyClass(testParams.field1, testParams.field2);
    string expected = testParams.field5;
    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}
Dipan Mehta
sumber
0

Ada banyak konsep yang mungkin, buat beberapa contoh untuk melihat perbedaannya

[TestMethod]
public void GetPath_Softcoded()
{
    //Hardcoded since you want to see what you expect is most simple and clear
    string expected = "C:\\Output Folder\\fields\\that later\\determine\\a folder";

    //If this test should also use a mocked filesystem it might be that you want to use
    //some base directory, which you could set in the setUp of your test class
    //that is usefull if you you need to run the same test on different environments
    string expected = this.outputPath + "fields\\that later\\determine\\a folder";


    //another readable way could be interesting if you have difficult variables needed to test
    string fields = "fields";
    string thatLater = "that later";
    string determine = "determine";
    string aFolder = "a folder";
    string expected = this.outputPath + fields + "\\" + thatLater + "\\" + determine + "\\" + aFolder;
    MyClass target = new MyClass(fields, thatLater, determine, aFolder);

    //in general testing with real words is not needed, so code could be shorter on that
    //for testing difficult folder names you write a separate test anyway
    string f1 = "f1";
    string f2 = "f2";
    string f3 = "f3";
    string f4 = "f4";
    string expected = this.outputPath + f1 + "\\" + f2 + "\\" + f3 + "\\" + f4;
    MyClass target = new MyClass(f1, f2, f3, f4);

    //so here we start to see a structure, it looks more like an array of fields
    //so what would make testing more interesting with lots of variables is the use of a data provider
    //the data provider will re-use your test with many different kinds of inputs. That will reduce the amount of duplication of code for testing
    //http://msdn.microsoft.com/en-us/library/ms182527.aspx


    The part where you compare already seems correct
    MyClass target = new MyClass(fields, thatLater, determine, aFolder);

    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}

Untuk meringkas: Secara umum tes hardcoded pertama Anda paling masuk akal bagi saya karena sederhana, langsung ke titik dll. Jika Anda memulai hardcoding jalan terlalu banyak hanya memasukkannya dalam metode setup.

Untuk pengujian terstruktur lebih lanjut di masa depan saya akan pergi untuk memeriksa sumber data sehingga Anda bisa menambahkan lebih banyak baris data jika Anda membutuhkan lebih banyak situasi pengujian.

Luc Franken
sumber
0

Kerangka kerja pengujian modern memungkinkan Anda memberikan parameter untuk metode Anda. Saya akan memanfaatkannya:

[TestCase("fields", "that later", "determine", "a folder", @"C:\Output Folder\fields\that later\determine\a folder")]
public void GetPathShouldReturnFullDirectoryPathBasedOnItsFields(
    string field1, string field2, string field3, string field,
    string expected)
{
    MyClass target = new MyClass(field1, field2, field3, field4);
    string actual = target.GetPath();
    Assert.AreEqual(expected, actual,
        "GetPath should return a full directory path based on its fields.");
}

Ada beberapa keuntungan untuk ini, dalam pandangan saya:

  1. Pengembang sering tergoda untuk menyalin bagian kode yang tampaknya sederhana dari SUT mereka ke dalam unit test mereka. Seperti yang ditunjukkan Winston , mereka masih bisa memiliki bug rumit yang tersembunyi di dalamnya. "Hard-coding" hasil yang diharapkan membantu menghindari situasi di mana kode pengujian Anda salah karena alasan yang sama bahwa kode asli Anda salah. Tetapi jika perubahan dalam persyaratan memaksa Anda untuk melacak string kode keras yang tertanam di dalam lusinan metode pengujian, itu bisa mengganggu. Memiliki semua nilai yang dikodekan di satu tempat, di luar logika pengujian Anda, memberi Anda yang terbaik dari kedua dunia.
  2. Anda dapat menambahkan tes untuk input berbeda dan output yang diharapkan dengan satu baris kode. Ini mendorong Anda untuk menulis lebih banyak tes, sambil menjaga kode tes Anda KERING dan mudah dirawat. Saya menemukan bahwa karena sangat murah untuk menambahkan tes, pikiran saya terbuka untuk kasus-kasus tes baru yang tidak akan saya pikirkan jika saya harus menulis metode yang sama sekali baru untuk mereka. Misalnya, perilaku apa yang akan saya harapkan jika salah satu input memiliki titik di dalamnya? Sebuah backslash? Bagaimana jika ada yang kosong? Atau ruang kosong? Atau mulai atau berakhir dengan spasi putih?
  3. Kerangka pengujian akan memperlakukan setiap TestCase sebagai pengujiannya sendiri, bahkan memasukkan input dan output yang disediakan ke dalam nama tes. Jika semua TestCases lulus tetapi satu, sangat mudah untuk melihat yang rusak dan bagaimana itu berbeda dari yang lainnya.
StriplingWarrior
sumber