Bagikan melalui


Pengontrol Pengujian Unit di ASP.NET Web API 2

Topik ini menjelaskan beberapa teknik khusus untuk pengontrol pengujian unit di Web API 2. Sebelum membaca topik ini, Anda mungkin ingin membaca tutorial Pengujian Unit ASP.NET Web API 2, yang menunjukkan cara menambahkan proyek pengujian unit ke solusi Anda.

Versi perangkat lunak yang digunakan dalam tutorial

Catatan

Saya menggunakan Moq, tetapi ide yang sama berlaku untuk kerangka kerja tiruan apa pun. Moq 4.5.30 (dan yang lebih baru) mendukung Visual Studio 2017, Roslyn, dan .NET 4.5 dan versi yang lebih baru.

Pola umum dalam pengujian unit adalah "arrange-act-assert":

  • Susun: Siapkan prasyarat apa pun agar pengujian berjalan.
  • Act: Lakukan pengujian.
  • Pernyataan: Verifikasi bahwa pengujian berhasil.

Dalam langkah susun, Anda akan sering menggunakan objek tiruan atau stub. Itu meminimalkan jumlah dependensi, sehingga pengujian difokuskan pada pengujian satu hal.

Berikut adalah beberapa hal yang harus Anda lakukan pengujian unit di pengontrol API Web Anda:

  • Tindakan mengembalikan jenis respons yang benar.
  • Parameter yang tidak valid mengembalikan respons kesalahan yang benar.
  • Tindakan memanggil metode yang benar pada repositori atau lapisan layanan.
  • Jika respons menyertakan model domain, verifikasi jenis model.

Ini adalah beberapa hal umum untuk diuji, tetapi spesifikasinya tergantung pada implementasi pengontrol Anda. Secara khusus, ini membuat perbedaan besar apakah tindakan pengontrol Anda mengembalikan HttpResponseMessage atau IHttpActionResult. Untuk informasi selengkapnya tentang jenis hasil ini, lihat Hasil Tindakan di Web Api 2.

Tindakan Pengujian yang Mengembalikan HttpResponseMessage

Berikut adalah contoh pengontrol yang tindakannya mengembalikan HttpResponseMessage.

public class ProductsController : ApiController
{
    IProductRepository _repository;

    public ProductsController(IProductRepository repository)
    {
        _repository = repository;
    }

    public HttpResponseMessage Get(int id)
    {
        Product product = _repository.GetById(id);
        if (product == null)
        {
            return Request.CreateResponse(HttpStatusCode.NotFound);
        }
        return Request.CreateResponse(product);
    }

    public HttpResponseMessage Post(Product product)
    {
        _repository.Add(product);

        var response = Request.CreateResponse(HttpStatusCode.Created, product);
        string uri = Url.Link("DefaultApi", new { id = product.Id });
        response.Headers.Location = new Uri(uri);

        return response;
    }
}

Perhatikan pengontrol menggunakan injeksi dependensi untuk menyuntikkan IProductRepository. Itu membuat pengontrol lebih dapat diuji, karena Anda dapat menyuntikkan repositori tiruan. Pengujian unit berikut memverifikasi bahwa Get metode menulis Product ke isi respons. Asumsikan bahwa repository adalah tiruan IProductRepository.

[TestMethod]
public void GetReturnsProduct()
{
    // Arrange
    var controller = new ProductsController(repository);
    controller.Request = new HttpRequestMessage();
    controller.Configuration = new HttpConfiguration();

    // Act
    var response = controller.Get(10);

    // Assert
    Product product;
    Assert.IsTrue(response.TryGetContentValue<Product>(out product));
    Assert.AreEqual(10, product.Id);
}

Penting untuk mengatur Permintaan dan Konfigurasi pada pengontrol. Jika tidak, pengujian akan gagal dengan ArgumentNullException atau InvalidOperationException.

Metode memanggil PostUrlHelper.Link untuk membuat tautan dalam respons. Ini membutuhkan sedikit lebih banyak pengaturan dalam pengujian unit:

[TestMethod]
public void PostSetsLocationHeader()
{
    // Arrange
    ProductsController controller = new ProductsController(repository);

    controller.Request = new HttpRequestMessage { 
        RequestUri = new Uri("http://localhost/api/products") 
    };
    controller.Configuration = new HttpConfiguration();
    controller.Configuration.Routes.MapHttpRoute(
        name: "DefaultApi", 
        routeTemplate: "api/{controller}/{id}",
        defaults: new { id = RouteParameter.Optional });

    controller.RequestContext.RouteData = new HttpRouteData(
        route: new HttpRoute(),
        values: new HttpRouteValueDictionary { { "controller", "products" } });

    // Act
    Product product = new Product() { Id = 42, Name = "Product1" };
    var response = controller.Post(product);

    // Assert
    Assert.AreEqual("http://localhost/api/products/42", response.Headers.Location.AbsoluteUri);
}

Kelas UrlHelper memerlukan URL permintaan dan merutekan data, sehingga pengujian harus mengatur nilai untuk ini. Opsi lain adalah tiruan atau stub UrlHelper. Dengan pendekatan ini, Anda mengganti nilai default ApiController.Url dengan versi tiruan atau stub yang mengembalikan nilai tetap.

Mari kita tulis ulang tes menggunakan kerangka kerja Moq . Moq Instal paket NuGet dalam proyek pengujian.

[TestMethod]
public void PostSetsLocationHeader_MockVersion()
{
    // This version uses a mock UrlHelper.

    // Arrange
    ProductsController controller = new ProductsController(repository);
    controller.Request = new HttpRequestMessage();
    controller.Configuration = new HttpConfiguration();

    string locationUrl = "http://location/";

    // Create the mock and set up the Link method, which is used to create the Location header.
    // The mock version returns a fixed string.
    var mockUrlHelper = new Mock<UrlHelper>();
    mockUrlHelper.Setup(x => x.Link(It.IsAny<string>(), It.IsAny<object>())).Returns(locationUrl);
    controller.Url = mockUrlHelper.Object;

    // Act
    Product product = new Product() { Id = 42 };
    var response = controller.Post(product);

    // Assert
    Assert.AreEqual(locationUrl, response.Headers.Location.AbsoluteUri);
}

Dalam versi ini, Anda tidak perlu menyiapkan data rute apa pun, karena UrlHelper tiruan mengembalikan string konstanta.

Tindakan Pengujian yang Mengembalikan IHttpActionResult

Di Web API 2, tindakan pengontrol dapat mengembalikan IHttpActionResult, yang dianalogikan dengan ActionResult di ASP.NET MVC. Antarmuka IHttpActionResult menentukan pola perintah untuk membuat respons HTTP. Alih-alih membuat respons secara langsung, pengontrol mengembalikan IHttpActionResult. Kemudian, alur memanggil IHttpActionResult untuk membuat respons. Pendekatan ini memudahkan untuk menulis pengujian unit, karena Anda dapat melewati banyak pengaturan yang diperlukan untuk HttpResponseMessage.

Berikut adalah contoh pengontrol yang tindakannya mengembalikan IHttpActionResult.

public class Products2Controller : ApiController
{
    IProductRepository _repository;

    public Products2Controller(IProductRepository repository)
    {
        _repository = repository;
    }

    public IHttpActionResult Get(int id)
    {
        Product product = _repository.GetById(id);
        if (product == null)
        {
            return NotFound();
        }
        return Ok(product);
    }

    public IHttpActionResult Post(Product product)
    {
        _repository.Add(product);
        return CreatedAtRoute("DefaultApi", new { id = product.Id }, product);
    }

    public IHttpActionResult Delete(int id)
    {
        _repository.Delete(id);
        return Ok();
    }

    public IHttpActionResult Put(Product product)
    {
        // Do some work (not shown).
        return Content(HttpStatusCode.Accepted, product);
    }    
}

Contoh ini memperlihatkan beberapa pola umum menggunakan IHttpActionResult. Mari kita lihat cara unit mengujinya.

Tindakan mengembalikan 200 (OK) dengan isi respons

Metode ini Get memanggil Ok(product) jika produk ditemukan. Dalam pengujian unit, pastikan jenis pengembalian adalah OkNegotiatedContentResult dan produk yang dikembalikan memiliki ID yang tepat.

[TestMethod]
public void GetReturnsProductWithSameId()
{
    // Arrange
    var mockRepository = new Mock<IProductRepository>();
    mockRepository.Setup(x => x.GetById(42))
        .Returns(new Product { Id = 42 });

    var controller = new Products2Controller(mockRepository.Object);

    // Act
    IHttpActionResult actionResult = controller.Get(42);
    var contentResult = actionResult as OkNegotiatedContentResult<Product>;

    // Assert
    Assert.IsNotNull(contentResult);
    Assert.IsNotNull(contentResult.Content);
    Assert.AreEqual(42, contentResult.Content.Id);
}

Perhatikan bahwa pengujian unit tidak menjalankan hasil tindakan. Anda dapat mengasumsikan hasil tindakan membuat respons HTTP dengan benar. (Itulah sebabnya kerangka kerja API Web memiliki pengujian unit sendiri!)

Tindakan mengembalikan 404 (Tidak Ditemukan)

Metode Get memanggil NotFound() jika produk tidak ditemukan. Untuk kasus ini, pengujian unit hanya memeriksa apakah jenis pengembaliannya adalah NotFoundResult.

[TestMethod]
public void GetReturnsNotFound()
{
    // Arrange
    var mockRepository = new Mock<IProductRepository>();
    var controller = new Products2Controller(mockRepository.Object);

    // Act
    IHttpActionResult actionResult = controller.Get(10);

    // Assert
    Assert.IsInstanceOfType(actionResult, typeof(NotFoundResult));
}

Tindakan mengembalikan 200 (OK) tanpa isi respons

Metode ini Delete memanggil Ok() untuk mengembalikan respons HTTP 200 kosong. Seperti contoh sebelumnya, pengujian unit memeriksa jenis pengembalian, dalam hal ini OkResult.

[TestMethod]
public void DeleteReturnsOk()
{
    // Arrange
    var mockRepository = new Mock<IProductRepository>();
    var controller = new Products2Controller(mockRepository.Object);

    // Act
    IHttpActionResult actionResult = controller.Delete(10);

    // Assert
    Assert.IsInstanceOfType(actionResult, typeof(OkResult));
}

Tindakan mengembalikan 201 (Dibuat) dengan header Lokasi

Metode memanggil PostCreatedAtRoute untuk mengembalikan respons HTTP 201 dengan URI di header Lokasi. Dalam pengujian unit, verifikasi bahwa tindakan menetapkan nilai perutean yang benar.

[TestMethod]
public void PostMethodSetsLocationHeader()
{
    // Arrange
    var mockRepository = new Mock<IProductRepository>();
    var controller = new Products2Controller(mockRepository.Object);

    // Act
    IHttpActionResult actionResult = controller.Post(new Product { Id = 10, Name = "Product1" });
    var createdResult = actionResult as CreatedAtRouteNegotiatedContentResult<Product>;

    // Assert
    Assert.IsNotNull(createdResult);
    Assert.AreEqual("DefaultApi", createdResult.RouteName);
    Assert.AreEqual(10, createdResult.RouteValues["id"]);
}

Tindakan mengembalikan 2xx lain dengan isi respons

Metode ini Put memanggil Content untuk mengembalikan respons HTTP 202 (Diterima) dengan isi respons. Kasus ini mirip dengan mengembalikan 200 (OK), tetapi pengujian unit juga harus memeriksa kode status.

[TestMethod]
public void PutReturnsContentResult()
{
    // Arrange
    var mockRepository = new Mock<IProductRepository>();
    var controller = new Products2Controller(mockRepository.Object);

    // Act
    IHttpActionResult actionResult = controller.Put(new Product { Id = 10, Name = "Product" });
    var contentResult = actionResult as NegotiatedContentResult<Product>;

    // Assert
    Assert.IsNotNull(contentResult);
    Assert.AreEqual(HttpStatusCode.Accepted, contentResult.StatusCode);
    Assert.IsNotNull(contentResult.Content);
    Assert.AreEqual(10, contentResult.Content.Id);
}

Sumber Daya Tambahan