Apakah ada nilai nyata dalam pengujian unit controller di ASP.NET MVC?

33

Saya harap pertanyaan ini memberikan beberapa jawaban menarik karena itu salah satu yang mengganggu saya untuk sementara waktu.

Apakah ada nilai nyata dalam pengujian unit controller di ASP.NET MVC?

Yang saya maksud dengan itu adalah, sebagian besar waktu, (dan saya bukan jenius), metode pengontrol saya, bahkan pada sesuatu yang paling kompleks seperti ini:

public ActionResult Create(MyModel model)
{
    // start error list
    var errors = new List<string>();

    // check model state based on data annotations
    if(ModelState.IsValid)
    {
        // call a service method
        if(this._myService.CreateNew(model, Request.UserHostAddress, ref errors))
        {
            // all is well, data is saved, 
            // so tell the user they are brilliant
            return View("_Success");
        }
    }

    // add errors to model state
    errors.ForEach(e => ModelState.AddModelError("", e));

    // return view
    return View(model);
}

Sebagian besar pekerjaan berat dilakukan oleh pipa MVC atau perpustakaan layanan saya.

Jadi mungkin pertanyaan yang akan diajukan:

  • berapakah nilai unit yang menguji metode ini?
  • tidakkah akan putus Request.UserHostAddressdan ModelStatedengan NullReferenceException? Haruskah saya mencoba mengejek ini?
  • jika saya refractor metode ini menjadi "helper" yang dapat digunakan kembali (yang mungkin saya harus, mengingat berapa kali saya melakukannya!), akan menguji yang bahkan berharga ketika semua saya benar-benar menguji sebagian besar "pipa" yang, mungkin, telah diuji dalam satu inci dari kehidupannya oleh Microsoft?

Saya pikir maksud saya sebenarnya , melakukan hal-hal berikut ini tampaknya sama sekali tidak ada gunanya dan salah

[TestMethod]
public void Test_Home_Index()
{
    var controller = new HomeController();
    var expected = "Index";
    var actual = ((ViewResult)controller.Index()).ViewName;
    Assert.AreEqual(expected, actual);
}

Jelas saya menjadi tumpul dengan contoh berlebihan yang tidak ada gunanya ini, tetapi apakah ada yang punya kebijaksanaan untuk menambahkan di sini?

Menantikan itu ... Terima kasih.

LiverpoolsNumber9
sumber
Saya pikir RoI (Pengembalian Investasi) pada tes tertentu tidak sepadan dengan usaha, kecuali jika Anda memiliki waktu dan uang yang tak terbatas. Saya akan menulis tes yang Kevin tunjukkan untuk memeriksa hal-hal yang lebih mungkin rusak atau akan membantu Anda dalam melakukan refactoring sesuatu dengan percaya diri atau memastikan penyebaran kesalahan terjadi seperti yang diharapkan. Tes pipa jika diperlukan dapat dilakukan pada tingkat yang lebih global / infrastruktur dan pada tingkat metode individual akan bernilai kecil. Bukan mengatakan mereka tidak ada nilainya, tetapi "kecil". Jadi, jika memberikan ROI yang baik dalam kasus Anda, lakukan untuk itu, jika tidak, tangkap ikan yang lebih besar terlebih dahulu!
Mrchief

Jawaban:

18

Bahkan untuk sesuatu yang sangat sederhana, unit test akan melayani berbagai keperluan

  1. Percaya diri, apa yang ditulis sesuai dengan hasil yang diharapkan. Tampaknya sepele untuk memverifikasi bahwa itu mengembalikan pandangan yang benar, tetapi hasilnya adalah bukti objektif bahwa persyaratan dipenuhi
  2. Pengujian regresi. Jika metode Buat perlu diubah, Anda masih memiliki tes unit untuk output yang diharapkan. Ya, output bisa berubah sepanjang dan itu menghasilkan tes rapuh tetapi masih merupakan pemeriksaan terhadap kontrol perubahan yang tidak dikelola

Untuk tindakan tertentu saya akan menguji untuk yang berikut

  1. Apa yang terjadi jika _myService adalah nol?
  2. Apa yang terjadi jika _myService.Create melempar Exception, apakah itu melemparkan yang spesifik untuk ditangani?
  3. Apakah _myService.Create yang berhasil mengembalikan tampilan _Success?
  4. Apakah kesalahan disebarkan hingga ModelState?

Anda menunjukkan memeriksa Permintaan dan Model untuk NullReferenceException dan saya pikir ModelState.IsValid akan menangani penanganan NullReference untuk Model.

Mengejek Permintaan memungkinkan Anda untuk menjaga terhadap Permintaan Null yang secara umum tidak mungkin dalam produksi saya pikir, tetapi dapat terjadi dalam Tes Unit. Dalam Tes Integrasi itu akan memungkinkan Anda untuk memberikan nilai UserHostAddress berbeda (Permintaan masih input pengguna sejauh kontrol yang bersangkutan dan harus diuji sesuai)

Kevin
sumber
Hai Kevin, terima kasih telah meluangkan waktu untuk menjawab. Saya akan meninggalkannya sebentar untuk melihat apakah ada orang lain yang datang dengan sesuatu tetapi sejauh ini milik Anda adalah yang paling logis / jelas.
LiverpoolsNumber9
Spifty. Senang itu membantu Anda.
Kevin
3

Kontroler saya juga sangat kecil. Sebagian besar "logika" di controller ditangani menggunakan atribut filter (built-in dan tulisan tangan). Jadi pengontrol saya biasanya hanya memiliki beberapa pekerjaan:

  • Buat model dari string kueri HTTP, nilai formulir, dll.
  • Lakukan beberapa validasi dasar
  • Panggil ke lapisan data atau bisnis saya
  • Hasilkan a ActionResult

Sebagian besar model mengikat dilakukan secara otomatis oleh ASP.NET MVC. DataAnnotations menangani sebagian besar validasi untuk saya juga.

Bahkan dengan begitu sedikit untuk diuji, saya masih biasanya menulisnya. Pada dasarnya, saya menguji apakah repositori saya dipanggil dan ActionResultjenis yang benar dikembalikan. Saya memiliki metode kenyamanan untuk ViewResultmemastikan jalur tampilan yang benar dikembalikan dan model tampilan terlihat seperti yang saya harapkan. Saya punya yang lain untuk memeriksa controller / action yang tepat diatur untuk RedirectToActionResult. Saya punya tes lain untuk JsonResult, dll. Dll

Hasil yang tidak menguntungkan dari sub- Controllerkelas kelas adalah bahwa ia menyediakan banyak metode kenyamanan yang menggunakan HttpContextinternal. Ini membuatnya sulit untuk menguji unit controller. Untuk alasan ini, saya biasanya meletakkan HttpContextpanggilan -dependen di belakang sebuah antarmuka dan meneruskan antarmuka itu ke konstruktor pengontrol (saya menggunakan ekstensi web Ninject untuk membuat pengontrol saya untuk saya). Antarmuka ini biasanya di mana saya menempel properti pembantu untuk mengakses sesi, pengaturan konfigurasi, IPrinciple dan URL helpers.

Ini membutuhkan banyak uji tuntas, tetapi saya pikir itu sangat berharga.

Taman Travis
sumber
Terima kasih telah meluangkan waktu untuk menjawab tetapi 2 masalah langsung. Pertama, "metode pembantu" dalam pengujian unit v. Berbahaya. Kedua, "tes bahwa repositori saya disebut" - maksud Anda melalui injeksi ketergantungan?
LiverpoolsNumber9
Mengapa metode kenyamanan berbahaya? Saya memiliki BaseControllerTestskelas di mana mereka semua tinggal. Saya mengejek repositori saya. Saya pasang mereka menggunakan Ninject.
Travis Parks
Apa yang terjadi jika Anda membuat kesalahan atau asumsi yang salah dalam pembantu Anda? Poin saya yang lain adalah bahwa, hanya tes integrasi (yaitu ujung ke ujung) yang dapat "menguji" apakah repositori Anda dipanggil. Dalam tes unit Anda akan "baru" atau mengejek repositori Anda secara manual.
LiverpoolsNumber9
Anda meneruskan repositori ke konstruktor. Anda mengejeknya saat ujian. Anda memastikan tiruan tersebut ditindaklanjuti seperti yang diharapkan. Pembantu hanya mendekonstruksi ActionResultuntuk memeriksa URL yang lewat, model, dll.
Travis Parks
Ok cukup adil - saya sedikit salah paham apa yang Anda maksud dengan "uji repositori saya dipanggil".
LiverpoolsNumber9
2

Jelas beberapa pengendali jauh lebih kompleks dari itu tetapi berdasarkan murni pada contoh Anda:

Apa yang terjadi jika myService melempar pengecualian?

Sebagai catatan.

Juga, saya akan mempertanyakan kebijaksanaan melewati daftar dengan referensi (toh tidak perlu karena c # lolos dengan referensi, tetapi meskipun tidak) - meneruskan tindakan errorAction (Aksi) yang kemudian dapat digunakan oleh layanan untuk memompa pesan kesalahan ke yang kemudian dapat ditangani sesuai keinginan Anda (mungkin Anda ingin menambahkannya ke daftar, mungkin Anda ingin menambahkan kesalahan model, mungkin Anda ingin mencatatnya).

Dalam contoh Anda:

bukannya kesalahan ref, lakukan (string s) => ModelState.AddModelError ("", s) misalnya.

Michael
sumber
Layak disebutkan, ini menganggap layanan Anda berada di aplikasi yang sama jika tidak masalah serialisasi akan ikut bermain.
Michael
Layanan akan berada di dll yang terpisah. Tapi bagaimanapun, Anda mungkin benar "ref". Di titik Anda yang lain, tidak masalah jika myService melempar pengecualian. Saya tidak menguji myService - saya akan menguji metode di dalamnya secara terpisah. Saya sedang berbicara tentang murni menguji "unit" ActionResult dengan (mungkin) myService diejek.
LiverpoolsNumber9
Apakah Anda memiliki pemetaan 1: 1 antara layanan Anda dan pengontrol Anda? Jika tidak, apakah beberapa pengendali menggunakan beberapa panggilan layanan? Jika demikian, Anda dapat menguji interaksi itu?
Michael
Tidak. Pada akhirnya, metode layanan mengambil input (biasanya model tampilan atau bahkan hanya string / int), mereka "melakukan hal-hal", kemudian mengembalikan bool / kesalahan jika salah. Tidak ada tautan "langsung" antara pengontrol dan lapisan layanan. Benar-benar dipisahkan.
LiverpoolsNumber9
Ya, saya mengerti itu, saya mencoba memahami model relasional antara pengontrol dan lapisan layanan - dengan asumsi bahwa setiap pengontrol tidak memiliki metode layanan yang sesuai maka akan masuk akal bahwa beberapa pengontrol mungkin perlu memanfaatkan lebih dari satu metode layanan?
Michael