Bagikan melalui


Aktifkan Pengujian Unit Otomatis

oleh Microsoft

Unduh PDF

Ini adalah langkah 12 dari tutorial aplikasi "NerdDinner" gratis yang memandu cara membangun aplikasi web kecil, tetapi lengkap menggunakan ASP.NET MVC 1.

Langkah 12 menunjukkan cara mengembangkan serangkaian pengujian unit otomatis yang memverifikasi fungsionalitas NerdDinner kami, dan yang akan memberi kami kepercayaan diri untuk membuat perubahan dan peningkatan pada aplikasi di masa depan.

Jika Anda menggunakan ASP.NET MVC 3, kami sarankan Anda mengikuti tutorial Memulai MVC 3 atau MVC Music Store .

NerdDinner Langkah 12: Pengujian Unit

Mari kita kembangkan serangkaian pengujian unit otomatis yang memverifikasi fungsionalitas NerdDinner kita, dan yang akan memberi kita keyakinan untuk membuat perubahan dan peningkatan pada aplikasi di masa depan.

Mengapa Unit Test?

Di drive ke tempat kerja suatu pagi Anda memiliki kilatan inspirasi mendadak tentang aplikasi yang sedang Anda kerjakan. Anda menyadari ada perubahan yang dapat Anda terapkan yang akan membuat aplikasi secara dramatis lebih baik. Ini mungkin refaktor yang membersihkan kode, menambahkan fitur baru, atau memperbaiki bug.

Pertanyaan yang berhadapan dengan Anda ketika Anda tiba di komputer Anda adalah - "seberapa aman untuk membuat peningkatan ini?" Bagaimana jika membuat perubahan memiliki efek samping atau merusak sesuatu? Perubahan mungkin sederhana dan hanya perlu beberapa menit untuk diterapkan, tetapi bagaimana jika diperlukan waktu berjam-jam untuk menguji semua skenario aplikasi secara manual? Bagaimana jika Anda lupa untuk menutupi skenario dan aplikasi yang rusak masuk ke produksi? Apakah membuat perbaikan ini benar-benar sepadan dengan semua upaya?

Pengujian unit otomatis dapat menyediakan jaring pengaman yang memungkinkan Anda untuk terus meningkatkan aplikasi Anda, dan menghindari takut pada kode yang sedang Anda kerjakan. Memiliki pengujian otomatis yang dengan cepat memverifikasi fungsionalitas memungkinkan Anda untuk membuat kode dengan percaya diri - dan memberdayakan Anda untuk melakukan peningkatan yang mungkin tidak Anda rasakan nyaman dilakukan. Mereka juga membantu menciptakan solusi yang lebih dapat dipertahankan dan memiliki masa pakai yang lebih lama - yang mengarah pada laba atas investasi yang jauh lebih tinggi.

ASP.NET MVC Framework memudahkan dan alami untuk fungsionalitas aplikasi pengujian unit. Ini juga memungkinkan alur kerja Test Driven Development (TDD) yang memungkinkan pengembangan berbasis uji pertama.

Proyek NerdDinner.Tests

Ketika kami membuat aplikasi NerdDinner di awal tutorial ini, kami diminta dengan dialog yang menanyakan apakah kami ingin membuat proyek pengujian unit untuk mengikuti proyek aplikasi:

Cuplikan layar dialog Buat Proyek Pengujian Unit. Ya, buat proyek pengujian unit dipilih. Nerd Dinner dot Tests ditulis sebagai nama proyek Uji.

Kami menyimpan tombol radio "Ya, buat proyek pengujian unit" dipilih - yang mengakibatkan proyek "NerdDinner.Tests" ditambahkan ke solusi kami:

Cuplikan layar pohon navigasi Penjelajah Solusi. Nerd Dinner dot Tests dipilih.

Proyek NerdDinner.Tests mereferensikan perakitan proyek aplikasi NerdDinner, dan memungkinkan kami untuk dengan mudah menambahkan pengujian otomatis ke dalamnya yang memverifikasi fungsionalitas aplikasi.

Membuat Pengujian Unit untuk Kelas Model Makan Malam kami

Mari kita tambahkan beberapa pengujian ke proyek NerdDinner.Tests kita yang memverifikasi kelas Dinner yang kita buat ketika kita membangun lapisan model kita.

Kita akan mulai dengan membuat folder baru dalam proyek pengujian kita yang disebut "Model" di mana kita akan menempatkan pengujian terkait model kita. Kami kemudian akan mengklik kanan folder dan memilih perintah menu Add-New> Test . Ini akan memunculkan dialog "Tambahkan Pengujian Baru".

Kita akan memilih untuk membuat "Pengujian Unit" dan menamainya "DinnerTest.cs":

Cuplikan layar kotak dialog Tambahkan Pengujian Baru. Pengujian Unit disorot. Uji Makan Malam dot c s ditulis sebagai Nama Uji.

Ketika kita mengeklik tombol "ok" Visual Studio akan menambahkan (dan membuka) file DinnerTest.cs ke proyek:

Cuplikan layar file Uji Makan Malam dot c s di Visual Studio.

Templat pengujian unit Visual Studio default memiliki banyak kode pelat boiler di dalamnya yang saya temukan sedikit berantakan. Mari kita bersihkan untuk hanya berisi kode di bawah ini:

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NerdDinner.Models;

namespace NerdDinner.Tests.Models {
 
    [TestClass]
    public class DinnerTest {

    }
}

Atribut [TestClass] pada kelas DinnerTest di atas mengidentifikasinya sebagai kelas yang akan berisi pengujian, serta inisialisasi pengujian opsional dan kode teardown. Kita dapat menentukan pengujian di dalamnya dengan menambahkan metode publik yang memiliki atribut [TestMethod] pada mereka.

Di bawah ini adalah tes pertama dari dua tes yang akan kami tambahkan latihan itu di kelas Makan Malam kami. Tes pertama memverifikasi bahwa Makan Malam kami tidak valid jika Makan Malam baru dibuat tanpa semua properti diatur dengan benar. Tes kedua memverifikasi bahwa Makan Malam kami valid ketika Makan Malam memiliki semua properti yang ditetapkan dengan nilai yang valid:

[TestClass]
public class DinnerTest {

    [TestMethod]
    public void Dinner_Should_Not_Be_Valid_When_Some_Properties_Incorrect() {

        //Arrange
        Dinner dinner = new Dinner() {
            Title = "Test title",
            Country = "USA",
            ContactPhone = "BOGUS"
        };

        // Act
        bool isValid = dinner.IsValid;

        //Assert
        Assert.IsFalse(isValid);
    }

    [TestMethod]
    public void Dinner_Should_Be_Valid_When_All_Properties_Correct() {
        
        //Arrange
        Dinner dinner = new Dinner {
            Title = "Test title",
            Description = "Some description",
            EventDate = DateTime.Now,
            HostedBy = "ScottGu",
            Address = "One Microsoft Way",
            Country = "USA",
            ContactPhone = "425-703-8072",
            Latitude = 93,
            Longitude = -92,
        };

        // Act
        bool isValid = dinner.IsValid;

        //Assert
        Assert.IsTrue(isValid);
    }
}

Anda akan melihat di atas bahwa nama pengujian kami sangat eksplisit (dan agak verbose). Kita melakukan ini karena kita mungkin akhirnya membuat ratusan atau ribuan tes kecil, dan kita ingin mempermudah untuk dengan cepat menentukan niat dan perilaku masing-masing (terutama ketika kita melihat melalui daftar kegagalan dalam test runner). Nama pengujian harus dinamai sesuai dengan fungsionalitas yang sedang mereka uji. Di atas kita menggunakan pola penamaan "Noun_Should_Verb".

Kami menyusun pengujian menggunakan pola pengujian "AAA" - yang merupakan singkatan dari "Arrange, Act, Assert":

  • Susun: Menyiapkan unit yang sedang diuji
  • Act: Menjalankan unit di bawah hasil pengujian dan pengambilan
  • Pernyataan: Memverifikasi perilaku

Ketika kita menulis tes, kita ingin menghindari tes individu melakukan terlalu banyak. Sebaliknya, setiap pengujian harus memverifikasi hanya satu konsep (yang akan membuatnya jauh lebih mudah untuk menentukan penyebab kegagalan). Pedoman yang baik adalah mencoba dan hanya memiliki satu pernyataan pernyataan untuk setiap pengujian. Jika Anda memiliki lebih dari satu pernyataan pernyataan dalam metode pengujian, pastikan semuanya digunakan untuk menguji konsep yang sama. Jika ragu, lakukan tes lain.

Menjalankan Pengujian

Visual Studio 2008 Professional (dan edisi yang lebih tinggi) mencakup runner pengujian bawaan yang dapat digunakan untuk menjalankan proyek Pengujian Unit Visual Studio dalam IDE. Kita dapat memilih perintah menu Test-Run-All>> Tests in Solution (atau ketik Ctrl R, A) untuk menjalankan semua pengujian unit kita. Atau, kita dapat memposisikan kursor kita dalam kelas pengujian atau metode pengujian tertentu dan menggunakan Test-Run-Tests>> di perintah menu Konteks Saat Ini (atau ketik Ctrl R, T) untuk menjalankan subset pengujian unit.

Mari kita posisikan kursor kita dalam kelas DinnerTest dan ketik "Ctrl R, T" untuk menjalankan dua tes yang baru saja kita tentukan. Ketika kita melakukan ini, jendela "Hasil Pengujian" akan muncul dalam Visual Studio dan kita akan melihat hasil uji coba yang tercantum di dalamnya:

Cuplikan layar jendela Hasil Pengujian di Visual Studio. Hasil uji coba tercantum di dalamnya.

Catatan: Jendela hasil pengujian VS tidak menampilkan kolom Nama Kelas secara default. Anda dapat menambahkan ini dengan mengklik kanan di dalam jendela Hasil Pengujian dan menggunakan perintah menu Tambahkan/Hapus Kolom.

Dua tes kami hanya membutuhkan waktu sedetik untuk dijalankan - dan seperti yang Anda lihat keduanya lulus. Kita sekarang dapat melanjutkan dan menambahnya dengan membuat pengujian tambahan yang memverifikasi validasi aturan tertentu, serta mencakup dua metode pembantu - IsUserHost() dan IsUserRegistered() - yang kami tambahkan ke kelas Makan Malam. Memiliki semua tes ini di tempat untuk kelas Makan Malam akan membuatnya jauh lebih mudah dan aman untuk menambahkan aturan bisnis baru dan validasi ke dalamnya di masa depan. Kita dapat menambahkan logika aturan baru kita ke Makan Malam, dan kemudian dalam hitungan detik memverifikasi bahwa itu belum merusak salah satu fungsionalitas logika kami sebelumnya.

Perhatikan cara menggunakan nama pengujian deskriptif memudahkan untuk dengan cepat memahami apa yang diverifikasi oleh setiap pengujian. Saya sarankan menggunakan perintah menu Tools-Options>, membuka layar konfigurasi Test Tools-Test> Execution, dan mencentang kotak centang "Mengklik dua kali hasil pengujian unit yang gagal atau tidak meyakinkan menampilkan titik kegagalan dalam pengujian". Ini akan memungkinkan Anda untuk mengeklik dua kali kegagalan di jendela hasil pengujian dan langsung melompat ke kegagalan pernyataan.

Membuat Tes Unit DinnersController

Sekarang mari kita buat beberapa pengujian unit yang memverifikasi fungsionalitas DinnersController kami. Kita akan mulai dengan mengeklik kanan pada folder "Pengontrol" dalam proyek Uji kita dan kemudian memilih perintah menu Uji Add-New>. Kami akan membuat "Pengujian Unit" dan menamainya "DinnersControllerTest.cs".

Kami akan membuat dua metode pengujian yang memverifikasi metode tindakan Details() pada DinnersController. Yang pertama akan memverifikasi bahwa Tampilan dikembalikan ketika Makan Malam yang ada diminta. Yang kedua akan memverifikasi bahwa tampilan "NotFound" dikembalikan ketika Makan Malam yang tidak ada diminta:

[TestClass]
public class DinnersControllerTest {

    [TestMethod]
    public void DetailsAction_Should_Return_View_For_ExistingDinner() {

        // Arrange
        var controller = new DinnersController();

        // Act
        var result = controller.Details(1) as ViewResult;

        // Assert
        Assert.IsNotNull(result, "Expected View");
    }

    [TestMethod]
    public void DetailsAction_Should_Return_NotFoundView_For_BogusDinner() {

        // Arrange
        var controller = new DinnersController();

        // Act
        var result = controller.Details(999) as ViewResult;

        // Assert
        Assert.AreEqual("NotFound", result.ViewName);
    } 
}

Kode di atas mengkompilasi bersih. Namun, ketika kita menjalankan tes, keduanya gagal:

Cuplikan layar kode. Kedua tes telah gagal.

Jika kita melihat pesan kesalahan, kita akan melihat bahwa alasan pengujian gagal adalah karena kelas DinnersRepository kita tidak dapat terhubung ke database. Aplikasi NerdDinner kami menggunakan string koneksi ke file SQL Server Express lokal yang berada di bawah direktori \App_Data proyek aplikasi NerdDinner. Karena proyek NerdDinner.Tests kami mengkompilasi dan berjalan di direktori yang berbeda maka proyek aplikasi, lokasi jalur relatif string koneksi kami salah.

Kami dapat memperbaikinya dengan menyalin file database SQL Express ke proyek pengujian kami, lalu menambahkan string koneksi pengujian yang sesuai ke dalam App.config proyek pengujian kami. Ini akan membuat pengujian di atas tidak diblokir dan berjalan.

Namun, kode pengujian unit menggunakan database nyata membawa sejumlah tantangan. Khususnya:

  • Ini secara signifikan memperlambat waktu eksekusi pengujian unit. Semakin lama waktu yang diperlukan untuk menjalankan pengujian, semakin kecil kemungkinan Anda untuk sering mengeksekusinya. Idealnya Anda ingin pengujian unit Anda dapat dijalankan dalam hitung detik - dan memilikinya menjadi sesuatu yang Anda lakukan secara alami seperti mengkompilasi proyek.
  • Ini mempersulit logika penyiapan dan pembersihan dalam pengujian. Anda ingin setiap pengujian unit diisolasi dan independen dari yang lain (tanpa efek samping atau dependensi). Saat bekerja melawan database nyata, Anda harus memperhatikan status dan mengatur ulang di antara pengujian.

Mari kita lihat pola desain yang disebut "injeksi dependensi" yang dapat membantu kita mengatasi masalah ini dan menghindari kebutuhan untuk menggunakan database nyata dengan pengujian kita.

Injeksi Dependensi

Saat ini DinnersController sangat "digabungkan" dengan kelas DinnerRepository. "Coupling" mengacu pada situasi di mana kelas secara eksplisit bergantung pada kelas lain untuk bekerja:

public class DinnersController : Controller {

    DinnerRepository dinnerRepository = new DinnerRepository();

    //
    // GET: /Dinners/Details/5

    public ActionResult Details(int id) {

        Dinner dinner = dinnerRepository.FindDinner(id);

        if (dinner == null)
            return View("NotFound");

        return View(dinner);
    }

Karena kelas DinnerRepository memerlukan akses ke database, dependensi yang digabungkan erat yang dimiliki kelas DinnersController pada DinnerRepository akhirnya mengharuskan kita untuk memiliki database agar metode tindakan DinnersController dapat diuji.

Kita bisa mengatasinya dengan menggunakan pola desain yang disebut "injeksi dependensi" - yang merupakan pendekatan di mana dependensi (seperti kelas repositori yang menyediakan akses data) tidak lagi dibuat secara implisit dalam kelas yang menggunakannya. Sebaliknya, dependensi dapat secara eksplisit diteruskan ke kelas yang menggunakannya menggunakan argumen konstruktor. Jika dependensi didefinisikan menggunakan antarmuka, kami kemudian memiliki fleksibilitas untuk meneruskan implementasi dependensi "palsu" untuk skenario pengujian unit. Ini memungkinkan kita untuk membuat implementasi dependensi khusus pengujian yang sebenarnya tidak memerlukan akses ke database.

Untuk melihat tindakan ini, mari kita terapkan injeksi dependensi dengan DinnersController kami.

Mengekstrak antarmuka IDinnerRepository

Langkah pertama kami adalah membuat antarmuka IDinnerRepository baru yang merangkum kontrak repositori yang diperlukan pengontrol kami untuk mengambil dan memperbarui Makan Malam.

Kita dapat menentukan kontrak antarmuka ini secara manual dengan mengklik kanan pada folder \Model, lalu memilih perintah menu Tambahkan Item> Baru dan membuat antarmuka baru bernama IDinnerRepository.cs.

Atau kita dapat menggunakan alat pemfaktoran ulang bawaan Visual Studio Professional (dan edisi yang lebih tinggi) untuk secara otomatis mengekstrak dan membuat antarmuka untuk kita dari kelas DinnerRepository yang ada. Untuk mengekstrak antarmuka ini menggunakan VS, cukup posisikan kursor di editor teks pada kelas DinnerRepository, lalu klik kanan dan pilih perintah menu Antarmuka Refaktor-Ekstrak>:

Cuplikan layar yang memperlihatkan Ekstrak Antarmuka dipilih di submenu Refaktor.

Ini akan meluncurkan dialog "Ekstrak Antarmuka" dan meminta nama antarmuka untuk dibuat. Ini akan default ke IDinnerRepository dan secara otomatis memilih semua metode publik pada kelas DinnerRepository yang ada untuk ditambahkan ke antarmuka:

Cuplikan layar jendela Hasil Pengujian di Visual Studio.

Ketika kita mengklik tombol "ok", Visual Studio akan menambahkan antarmuka IDinnerRepository baru ke aplikasi kita:

public interface IDinnerRepository {

    IQueryable<Dinner> FindAllDinners();
    IQueryable<Dinner> FindByLocation(float latitude, float longitude);
    IQueryable<Dinner> FindUpcomingDinners();
    Dinner             GetDinner(int id);

    void Add(Dinner dinner);
    void Delete(Dinner dinner);
    
    void Save();
}

Dan kelas DinnerRepository kami yang ada akan diperbarui sehingga mengimplementasikan antarmuka:

public class DinnerRepository : IDinnerRepository {
   ...
}

Memperbarui DinnersController untuk mendukung injeksi konstruktor

Kami sekarang akan memperbarui kelas DinnersController untuk menggunakan antarmuka baru.

Saat ini DinnersController dikodekan secara permanen sehingga bidang "dinnerRepository" selalu merupakan kelas DinnerRepository:

public class DinnersController : Controller {

    DinnerRepository dinnerRepository = new DinnerRepository();

    ...
}

Kami akan mengubahnya sehingga bidang "dinnerRepository" adalah jenis IDinnerRepository alih-alih DinnerRepository. Kami kemudian akan menambahkan dua konstruktor DinnersController publik. Salah satu konstruktor memungkinkan IDinnerRepository diteruskan sebagai argumen. Yang lainnya adalah konstruktor default yang menggunakan implementasi DinnerRepository kami yang ada:

public class DinnersController : Controller {

    IDinnerRepository dinnerRepository;

    public DinnersController()
        : this(new DinnerRepository()) {
    }

    public DinnersController(IDinnerRepository repository) {
        dinnerRepository = repository;
    }
    ...
}

Karena ASP.NET MVC secara default membuat kelas pengontrol menggunakan konstruktor default, DinnersController kami saat runtime akan terus menggunakan kelas DinnerRepository untuk melakukan akses data.

Namun, kita sekarang dapat memperbarui pengujian unit kita, untuk lulus dalam implementasi repositori makan malam "palsu" menggunakan konstruktor parameter. Repositori makan malam "palsu" ini tidak akan memerlukan akses ke database nyata, dan sebaliknya akan menggunakan data sampel dalam memori.

Membuat kelas FakeDinnerRepository

Mari kita buat kelas FakeDinnerRepository.

Kita akan mulai dengan membuat direktori "Palsu" dalam proyek NerdDinner.Tests kita dan kemudian menambahkan kelas FakeDinnerRepository baru ke dalamnya (klik kanan pada folder dan pilih Add-New> Class):

Cuplikan layar item menu Tambahkan Kelas Baru. Tambahkan Item Baru disorot.

Kami akan memperbarui kode sehingga kelas FakeDinnerRepository mengimplementasikan antarmuka IDinnerRepository. Kita kemudian dapat mengklik kanan padanya dan memilih perintah menu konteks "Terapkan antarmuka IDinnerRepository":

Cuplikan layar perintah menu konteks Terapkan antarmuka I Dinner Repository.

Ini akan menyebabkan Visual Studio secara otomatis menambahkan semua anggota antarmuka IDinnerRepository ke kelas FakeDinnerRepository kami dengan implementasi "stub out" default:

public class FakeDinnerRepository : IDinnerRepository {

    public IQueryable<Dinner> FindAllDinners() {
        throw new NotImplementedException();
    }

    public IQueryable<Dinner> FindByLocation(float lat, float long){
        throw new NotImplementedException();
    }

    public IQueryable<Dinner> FindUpcomingDinners() {
        throw new NotImplementedException();
    }

    public Dinner GetDinner(int id) {
        throw new NotImplementedException();
    }

    public void Add(Dinner dinner) {
        throw new NotImplementedException();
    }

    public void Delete(Dinner dinner) {
        throw new NotImplementedException();
    }

    public void Save() {
        throw new NotImplementedException();
    }
}

Kita kemudian dapat memperbarui implementasi FakeDinnerRepository untuk mengerjakan koleksi Makan Malam> Daftar<dalam memori yang diteruskan ke dalamnya sebagai argumen konstruktor:

public class FakeDinnerRepository : IDinnerRepository {

    private List<Dinner> dinnerList;

    public FakeDinnerRepository(List<Dinner> dinners) {
        dinnerList = dinners;
    }

    public IQueryable<Dinner> FindAllDinners() {
        return dinnerList.AsQueryable();
    }

    public IQueryable<Dinner> FindUpcomingDinners() {
        return (from dinner in dinnerList
                where dinner.EventDate > DateTime.Now
                select dinner).AsQueryable();
    }

    public IQueryable<Dinner> FindByLocation(float lat, float lon) {
        return (from dinner in dinnerList
                where dinner.Latitude == lat && dinner.Longitude == lon
                select dinner).AsQueryable();
    }

    public Dinner GetDinner(int id) {
        return dinnerList.SingleOrDefault(d => d.DinnerID == id);
    }

    public void Add(Dinner dinner) {
        dinnerList.Add(dinner);
    }

    public void Delete(Dinner dinner) {
        dinnerList.Remove(dinner);
    }

    public void Save() {
        foreach (Dinner dinner in dinnerList) {
            if (!dinner.IsValid)
                throw new ApplicationException("Rule violations");
        }
    }
}

Kita sekarang memiliki implementasi IDinnerRepository palsu yang tidak memerlukan database, dan sebaliknya dapat mengerjakan daftar objek Dinner dalam memori.

Menggunakan FakeDinnerRepository dengan Pengujian Unit

Mari kita kembali ke pengujian unit DinnersController yang gagal sebelumnya karena database tidak tersedia. Kita dapat memperbarui metode pengujian untuk menggunakan FakeDinnerRepository yang diisi dengan sampel data Makan Malam dalam memori ke DinnersController menggunakan kode di bawah ini:

[TestClass]
public class DinnersControllerTest {

    List<Dinner> CreateTestDinners() {

        List<Dinner> dinners = new List<Dinner>();

        for (int i = 0; i < 101; i++) {

            Dinner sampleDinner = new Dinner() {
                DinnerID = i,
                Title = "Sample Dinner",
                HostedBy = "SomeUser",
                Address = "Some Address",
                Country = "USA",
                ContactPhone = "425-555-1212",
                Description = "Some description",
                EventDate = DateTime.Now.AddDays(i),
                Latitude = 99,
                Longitude = -99
            };
            
            dinners.Add(sampleDinner);
        }
        
        return dinners;
    }

    DinnersController CreateDinnersController() {
        var repository = new FakeDinnerRepository(CreateTestDinners());
        return new DinnersController(repository);
    }

    [TestMethod]
    public void DetailsAction_Should_Return_View_For_Dinner() {

        // Arrange
        var controller = CreateDinnersController();

        // Act
        var result = controller.Details(1);

        // Assert
        Assert.IsInstanceOfType(result, typeof(ViewResult));
    }

    [TestMethod]
    public void DetailsAction_Should_Return_NotFoundView_For_BogusDinner() {

        // Arrange
        var controller = CreateDinnersController();

        // Act
        var result = controller.Details(999) as ViewResult;

        // Assert
        Assert.AreEqual("NotFound", result.ViewName);
    }
}

Dan sekarang ketika kita menjalankan tes ini mereka berdua lulus:

Cuplikan layar pengujian unit, kedua pengujian telah lulus.

Yang terbaik dari semuanya, mereka hanya membutuhkan waktu sedetik untuk dijalankan, dan tidak memerlukan logika penyiapan/pembersihan yang rumit. Kami sekarang dapat menguji semua kode metode tindakan DinnersController kami (termasuk daftar, penomoran, detail, buat, perbarui, dan hapus) tanpa perlu terhubung ke database nyata.

Topik Samping: Kerangka Kerja Injeksi Dependensi
Melakukan injeksi dependensi manual (seperti kami di atas) berfungsi dengan baik, tetapi menjadi lebih sulit untuk dipertahankan karena jumlah dependensi dan komponen dalam aplikasi meningkat. Beberapa kerangka kerja injeksi dependensi ada untuk .NET yang dapat membantu memberikan lebih banyak fleksibilitas manajemen dependensi. Kerangka kerja ini, juga kadang-kadang disebut kontainer "Inversion of Control" (IoC), menyediakan mekanisme yang memungkinkan tingkat dukungan konfigurasi tambahan untuk menentukan dan meneruskan dependensi ke objek pada runtime (paling sering menggunakan injeksi konstruktor). Beberapa kerangka kerja Injeksi Dependensi OSS / IOC yang lebih populer di .NET meliputi: AutoFac, Ninject, Spring.NET, StructureMap, dan Windsor. ASP.NET MVC memaparkan API ekstensibilitas yang memungkinkan pengembang untuk berpartisipasi dalam resolusi dan instansiasi pengontrol, dan yang memungkinkan kerangka kerja Injeksi Dependensi / IoC terintegrasi dengan bersih dalam proses ini. Menggunakan kerangka kerja DI/IOC juga akan memungkinkan kami untuk menghapus konstruktor default dari DinnersController kami - yang akan sepenuhnya menghapus konektor antara itu dan DinnerRepository. Kami tidak akan menggunakan injeksi dependensi / kerangka kerja IOC dengan aplikasi NerdDinner kami. Tapi itu adalah sesuatu yang dapat kita pertimbangkan untuk masa depan jika basis kode nerdDinner dan kemampuan tumbuh.

Membuat Pengujian Unit Tindakan Edit

Sekarang mari kita buat beberapa pengujian unit yang memverifikasi fungsionalitas Edit DinnersController. Kita akan mulai dengan menguji versi HTTP-GET dari tindakan Edit kita:

//
// GET: /Dinners/Edit/5

[Authorize]
public ActionResult Edit(int id) {

    Dinner dinner = dinnerRepository.GetDinner(id);

    if (!dinner.IsHostedBy(User.Identity.Name))
        return View("InvalidOwner");

    return View(new DinnerFormViewModel(dinner));
}

Kami akan membuat pengujian yang memverifikasi bahwa Tampilan yang didukung oleh objek DinnerFormViewModel dirender kembali saat makan malam yang valid diminta:

[TestMethod]
public void EditAction_Should_Return_View_For_ValidDinner() {

    // Arrange
    var controller = CreateDinnersController();

    // Act
    var result = controller.Edit(1) as ViewResult;

    // Assert
    Assert.IsInstanceOfType(result.ViewData.Model, typeof(DinnerFormViewModel));
}

Namun, ketika kita menjalankan pengujian, kita akan menemukan bahwa itu gagal karena pengecualian referensi null dilemparkan ketika metode Edit mengakses properti User.Identity.Name untuk melakukan pemeriksaan Dinner.IsHostedBy().

Objek Pengguna pada kelas dasar Pengontrol merangkum detail tentang pengguna yang masuk, dan diisi oleh ASP.NET MVC saat membuat pengontrol saat runtime. Karena kami menguji DinnersController di luar lingkungan server web, objek Pengguna tidak diatur (karenanya pengecualian referensi null).

Menirukan properti User.Identity.Name

Kerangka kerja tiruan mempermudah pengujian dengan memungkinkan kami membuat versi objek dependen palsu yang mendukung pengujian secara dinamis. Misalnya, kita dapat menggunakan kerangka kerja tiruan dalam pengujian tindakan Edit untuk membuat objek Pengguna yang dapat digunakan DinnersController secara dinamis untuk mencari nama pengguna yang disimulasikan. Ini akan menghindari referensi null agar tidak dilemparkan ketika kita menjalankan pengujian.

Ada banyak kerangka kerja tiruan .NET yang dapat digunakan dengan ASP.NET MVC (Anda dapat melihat daftarnya di sini: http://www.mockframeworks.com/).

Setelah diunduh, kami akan menambahkan referensi dalam proyek NerdDinner.Tests kami ke assembly Moq.dll:

Cuplikan layar pohon navigasi Nerd Dinner. Moq disorot.

Kami kemudian akan menambahkan metode pembantu "CreateDinnersControllerAs(nama pengguna)" ke kelas pengujian kami yang mengambil nama pengguna sebagai parameter, dan yang kemudian "meniru" properti User.Identity.Name pada instans DinnersController:

DinnersController CreateDinnersControllerAs(string userName) {

    var mock = new Mock<ControllerContext>();
    mock.SetupGet(p => p.HttpContext.User.Identity.Name).Returns(userName);
    mock.SetupGet(p => p.HttpContext.Request.IsAuthenticated).Returns(true);

    var controller = CreateDinnersController();
    controller.ControllerContext = mock.Object;

    return controller;
}

Di atas kita menggunakan Moq untuk membuat objek Mock yang memalsukan objek ControllerContext (yang ASP.NET MVC lolos ke kelas Pengontrol untuk mengekspos objek runtime seperti Pengguna, Permintaan, Respons, dan Sesi). Kami memanggil metode "SetupGet" pada Mock untuk menunjukkan bahwa properti HttpContext.User.Identity.Name di ControllerContext harus mengembalikan string nama pengguna yang kami berikan ke metode pembantu.

Kita dapat menipu sejumlah properti dan metode ControllerContext. Untuk mengilustrasikan hal ini, saya juga telah menambahkan panggilan SetupGet() untuk properti Request.IsAuthenticated (yang sebenarnya tidak diperlukan untuk pengujian di bawah ini - tetapi yang membantu menggambarkan bagaimana Anda dapat meniru properti Permintaan). Setelah selesai, kami menetapkan instans mock ControllerContext ke DinnersController, metode pembantu kami kembali.

Kita sekarang dapat menulis pengujian unit yang menggunakan metode pembantu ini untuk menguji skenario Edit yang melibatkan pengguna yang berbeda:

[TestMethod]
public void EditAction_Should_Return_EditView_When_ValidOwner() {

    // Arrange
    var controller = CreateDinnersControllerAs("SomeUser");

    // Act
    var result = controller.Edit(1) as ViewResult;

    // Assert
    Assert.IsInstanceOfType(result.ViewData.Model, typeof(DinnerFormViewModel));
}

[TestMethod]
public void EditAction_Should_Return_InvalidOwnerView_When_InvalidOwner() {

    // Arrange
    var controller = CreateDinnersControllerAs("NotOwnerUser");

    // Act
    var result = controller.Edit(1) as ViewResult;

    // Assert
    Assert.AreEqual(result.ViewName, "InvalidOwner");
}

Dan sekarang ketika kita menjalankan tes mereka lulus:

Cuplikan layar pengujian unit yang menggunakan metode pembantu. Tes telah berlalu.

Menguji skenario UpdateModel()

Kami telah membuat pengujian yang mencakup versi HTTP-GET dari tindakan Edit. Sekarang mari kita buat beberapa pengujian yang memverifikasi versi HTTP-POST dari tindakan Edit:

//
// POST: /Dinners/Edit/5

[AcceptVerbs(HttpVerbs.Post), Authorize]
public ActionResult Edit (int id, FormCollection collection) {

    Dinner dinner = dinnerRepository.GetDinner(id);

    if (!dinner.IsHostedBy(User.Identity.Name))
        return View("InvalidOwner");

    try {
        UpdateModel(dinner);

        dinnerRepository.Save();

        return RedirectToAction("Details", new { id=dinner.DinnerID });
    }
    catch {
        ModelState.AddModelErrors(dinner.GetRuleViolations());
        
        return View(new DinnerFormViewModel(dinner));
    }
}

Skenario pengujian baru yang menarik bagi kami untuk mendukung metode tindakan ini adalah penggunaan metode pembantu UpdateModel() pada kelas dasar Pengontrol. Kami menggunakan metode pembantu ini untuk mengikat nilai form-post ke instans objek Dinner kami.

Di bawah ini adalah dua pengujian yang menunjukkan bagaimana kita dapat menyediakan nilai yang diposting formulir untuk metode pembantu UpdateModel() untuk digunakan. Kita akan melakukan ini dengan membuat dan mengisi objek FormCollection, lalu menetapkannya ke properti "ValueProvider" pada Pengontrol.

Pengujian pertama memverifikasi bahwa pada penyimpanan browser yang berhasil dialihkan ke tindakan detail. Pengujian kedua memverifikasi bahwa ketika input yang tidak valid diposting, tindakan akan menampilkan kembali tampilan edit lagi dengan pesan kesalahan.

[TestMethod]
public void EditAction_Should_Redirect_When_Update_Successful() {

    // Arrange      
    var controller = CreateDinnersControllerAs("SomeUser");

    var formValues = new FormCollection() {
        { "Title", "Another value" },
        { "Description", "Another description" }
    };

    controller.ValueProvider = formValues.ToValueProvider();
    
    // Act
    var result = controller.Edit(1, formValues) as RedirectToRouteResult;

    // Assert
    Assert.AreEqual("Details", result.RouteValues["Action"]);
}

[TestMethod]
public void EditAction_Should_Redisplay_With_Errors_When_Update_Fails() {

    // Arrange
    var controller = CreateDinnersControllerAs("SomeUser");

    var formValues = new FormCollection() {
        { "EventDate", "Bogus date value!!!"}
    };

    controller.ValueProvider = formValues.ToValueProvider();

    // Act
    var result = controller.Edit(1, formValues) as ViewResult;

    // Assert
    Assert.IsNotNull(result, "Expected redisplay of view");
    Assert.IsTrue(result.ViewData.ModelState.Count > 0, "Expected errors");
}

Wrap-Up pengujian

Kami telah membahas konsep inti yang terlibat dalam kelas pengontrol pengujian unit. Kita dapat menggunakan teknik ini untuk dengan mudah membuat ratusan tes sederhana yang memverifikasi perilaku aplikasi kita.

Karena pengujian pengontrol dan model kami tidak memerlukan database nyata, mereka sangat cepat dan mudah dijalankan. Kita akan dapat menjalankan ratusan tes otomatis dalam hitungan detik, dan segera mendapatkan umpan balik tentang apakah perubahan yang kita buat melanggar sesuatu. Ini akan membantu memberi kita kepercayaan diri untuk terus meningkatkan, merefaktor, dan menyempurnakan aplikasi kita.

Kami membahas pengujian sebagai topik terakhir dalam bab ini - tetapi bukan karena pengujian adalah sesuatu yang harus Anda lakukan di akhir proses pengembangan! Sebaliknya, Anda harus menulis pengujian otomatis sedini mungkin dalam proses pengembangan Anda. Melakukannya memungkinkan Anda untuk mendapatkan umpan balik segera saat Anda mengembangkan, membantu Anda berpikir cermat tentang skenario kasus penggunaan aplikasi Anda, dan memandu Anda untuk merancang aplikasi Anda dengan lapisan bersih dan konektor dalam pikiran.

Bab selanjutnya dalam buku ini akan membahas Test Driven Development (TDD), dan cara menggunakannya dengan ASP.NET MVC. TDD adalah praktik pengkodean iteratif di mana Anda pertama kali menulis pengujian yang akan dipenuhi kode yang Anda hasilkan. Dengan TDD, Anda memulai setiap fitur dengan membuat pengujian yang memverifikasi fungsionalitas yang akan Anda terapkan. Menulis pengujian unit terlebih dahulu membantu memastikan bahwa Anda dengan jelas memahami fitur dan cara kerjanya. Hanya setelah pengujian ditulis (dan Anda telah memverifikasi bahwa pengujian gagal) apakah Anda kemudian menerapkan fungsionalitas aktual yang diverifikasi pengujian. Karena Anda telah menghabiskan waktu untuk memikirkan kasus penggunaan bagaimana fitur tersebut seharusnya berfungsi, Anda akan memiliki pemahaman yang lebih baik tentang persyaratan dan cara terbaik untuk menerapkannya. Ketika Anda selesai dengan implementasi, Anda dapat menjalankan kembali pengujian - dan mendapatkan umpan balik langsung tentang apakah fitur bekerja dengan benar. Kami akan membahas TDD lebih lanjut di Bab 10.

Langkah Selanjutnya

Beberapa komentar terakhir dibungkus.