Catatan
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba masuk atau mengubah direktori.
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba mengubah direktori.
Oleh Jos van der Til, Martin Costello, dan Javier Calvarro Nelson.
Pengujian integrasi memastikan bahwa komponen aplikasi berfungsi dengan benar pada tingkat yang mencakup infrastruktur pendukung aplikasi, seperti database, sistem file, dan jaringan. ASP.NET Core mendukung pengujian integrasi menggunakan kerangka kerja pengujian unit dengan host web pengujian dan server pengujian dalam memori.
Artikel ini mengasumsikan pemahaman dasar tentang pengujian unit. Jika tidak terbiasa dengan konsep pengujian, lihat artikel Pengujian di .NET dan konten tertautnya.
Melihat atau mengunduh kode sampel (cara mengunduh)
Aplikasi sampel adalah Razor aplikasi Pages dan mengasumsikan pengetahuan dasar mengenai Razor Pages. Jika Anda tidak terbiasa dengan Razor Pages, lihat artikel berikut ini:
Untuk menguji SPAs, kami merekomendasikan alat seperti Playwright untuk .NET, yang dapat mengotomatiskan browser.
Pengantar pengujian integrasi
Pengujian integrasi mengevaluasi komponen aplikasi pada tingkat yang lebih luas daripada pengujian unit. Pengujian unit digunakan untuk menguji komponen perangkat lunak terisolasi, seperti metode kelas individual. Pengujian integrasi mengonfirmasi bahwa dua komponen aplikasi atau lebih bekerja sama untuk menghasilkan hasil yang diharapkan, mungkin termasuk setiap komponen yang diperlukan untuk memproses permintaan sepenuhnya.
Pengujian yang lebih luas ini digunakan untuk menguji infrastruktur aplikasi dan seluruh kerangka kerja, sering kali termasuk komponen berikut:
- Basis data
- Sistem file
- Appliance jaringan
- Jalur permintaan-tanggapan
Pengujian unit menggunakan komponen fabrikasi, yang dikenal sebagai objek palsu atau tiruan, sebagai pengganti komponen infrastruktur.
Berbeda dengan pengujian unit, pengujian integrasi:
- Gunakan komponen aktual yang digunakan aplikasi dalam produksi.
- Memerlukan lebih banyak kode dan pemrosesan data.
- Membutuhkan waktu lebih lama untuk berjalan.
Oleh karena itu, batasi penggunaan pengujian integrasi ke skenario infrastruktur yang paling penting. Jika perilaku dapat diuji menggunakan pengujian unit atau pengujian integrasi, pilih pengujian unit.
Dalam diskusi pengujian integrasi, proyek yang diuji sering disebut System Under Test, atau "SUT" singkatnya. "SUT" digunakan di seluruh artikel ini untuk merujuk ke aplikasi ASP.NET Core yang sedang diuji.
Jangan menulis pengujian integrasi untuk setiap permutasi akses data dan file yang melibatkan database dan sistem file. Terlepas dari berapa banyak tempat di seluruh aplikasi berinteraksi dengan database dan sistem file, serangkaian pengujian integrasi baca, tulis, perbarui, dan hapus yang berfokus biasanya mampu menguji database dan komponen sistem file dengan memadai. Gunakan pengujian unit untuk pengujian rutin logika metode yang berinteraksi dengan komponen-komponen ini. Dalam pengujian unit, penggunaan infrastruktur palsu atau tiruan menghasilkan eksekusi pengujian yang lebih cepat.
pengujian integrasi ASP.NET Core
Pengujian integrasi di ASP.NET Core memerlukan hal berikut:
- Proyek pengujian digunakan untuk memuat dan menjalankan pengujian. Proyek pengujian memiliki referensi ke SUT.
- Proyek pengujian membuat host web pengujian untuk SUT dan menggunakan klien server pengujian untuk menangani permintaan dan respons dengan SUT.
- Pelari uji digunakan untuk menjalankan pengujian dan melaporkan hasil pengujian.
Pengujian integrasi mengikuti urutan langkah pengujian yang mencakup tahapan Arrange, Act, dan Assert yang biasa:
- Server web SUT telah dikonfigurasi.
- Klien server pengujian dibuat untuk mengirimkan permintaan ke aplikasi.
- Langkah Atur pengujian dijalankan: Aplikasi pengujian menyiapkan permintaan.
- Langkah pengujian Act dijalankan: Klien mengirimkan permintaan dan menerima respons.
- Langkah pengujian Assert dijalankan: Respons aktual divalidasi sebagai lulus atau gagal berdasarkan respons yang diharapkan .
- Proses berlanjut sampai semua pengujian dijalankan.
- Hasil pengujian dilaporkan.
Biasanya, host web pengujian dikonfigurasi secara berbeda dari host web normal aplikasi untuk eksekusi pengujian. Misalnya, database yang berbeda atau pengaturan aplikasi yang berbeda dapat digunakan untuk pengujian.
Komponen infrastruktur, seperti host web pengujian dan server pengujian dalam memori (TestServer), disediakan atau dikelola oleh paket Microsoft.AspNetCore.Mvc.Testing . Penggunaan paket ini menyederhanakan pembuatan dan eksekusi pengujian.
Paket Microsoft.AspNetCore.Mvc.Testing
menangani tugas-tugas berikut:
- Menyalin file dependensi (
.deps
) dari SUT ke direktori proyekbin
pengujian. - Menetapkan akar konten ke akar proyek SUT agar file statis serta halaman dan tampilan dapat ditemukan saat pengujian berlangsung.
-
Menyediakan kelas WebApplicationFactory untuk mempermudah proses awal pengoperasian SUT dengan
TestServer
.
Dokumentasi tes unit menjelaskan cara menyiapkan proyek pengujian dan pengelola pengujian, bersama dengan instruksi terperinci tentang cara menjalankan tes serta rekomendasi tentang cara memberi nama tes dan kelas tes.
Memisahkan pengujian unit dari pengujian integrasi ke dalam proyek yang berbeda. Memisahkan pengujian:
- Membantu memastikan bahwa komponen pengujian infrastruktur tidak secara tidak sengaja disertakan dalam pengujian unit.
- Memungkinkan kontrol atas set pengujian yang akan dijalankan.
Hampir tidak ada perbedaan antara konfigurasi untuk pengujian Razor aplikasi Pages dan aplikasi MVC. Satu-satunya perbedaan adalah bagaimana tes diberi nama.
Razor Di aplikasi Pages, pengujian titik akhir halaman biasanya dinamai sesuai dengan kelas model halaman (misalnya, IndexPageTests
untuk menguji integrasi komponen untuk halaman Indeks). Dalam aplikasi MVC, pengujian biasanya diatur oleh kelas pengontrol dan dinamai sesuai pengontrol yang mereka uji (misalnya, HomeControllerTests
untuk menguji integrasi komponen untuk Home pengontrol).
Menguji prasyarat aplikasi
Proyek pengujian harus:
- Merujuk ke paket
Microsoft.AspNetCore.Mvc.Testing
. - Tentukan Web SDK dalam file proyek (
<Project Sdk="Microsoft.NET.Sdk.Web">
).
Prasyarat ini dapat dilihat di aplikasi sampel. Periksa file tests/RazorPagesProject.Tests/RazorPagesProject.Tests.csproj
. Aplikasi sampel menggunakan kerangka kerja pengujian xUnit dan pustaka pengurai AngleSharp , sehingga aplikasi sampel juga mereferensikan:
Di aplikasi yang menggunakan xunit.runner.visualstudio
versi 2.4.2 atau yang lebih baru, proyek pengujian harus mereferensikan Microsoft.NET.Test.Sdk
paket.
Entity Framework Core juga digunakan dalam pengujian. Lihat file proyek di GitHub.
Lingkungan SUT
Jika lingkungan SUT tidak diatur, lingkungan akan default ke Pengembangan.
Pengujian dasar dengan WebApplicationFactory default
WebApplicationFactory<TEntryPoint> digunakan untuk membuat TestServer untuk pengujian integrasi.
TEntryPoint
adalah kelas titik masuk SUT, biasanya Program.cs
.
Kelas uji menerapkan antarmuka fikstur kelas untuk menunjukkan bahwa kelas tersebut berisi pengujian dan menyediakan instans objek yang dapat digunakan bersama di seluruh pengujian dalam kelas.
Kelas pengujian berikut, BasicTests
, menggunakan WebApplicationFactory
untuk bootstrap SUT dan menyediakan HttpClient untuk metode pengujian, Get_EndpointsReturnSuccessAndCorrectContentType
. Metode memverifikasi kode status respons berhasil (200-299) dan Content-Type
header adalah text/html; charset=utf-8
untuk beberapa halaman aplikasi.
CreateClient() membuat instans HttpClient
yang secara otomatis mengikuti pengalihan dan menangani cookie.
public class BasicTests
: IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;
public BasicTests(WebApplicationFactory<Program> factory)
{
_factory = factory;
}
[Theory]
[InlineData("/")]
[InlineData("/Index")]
[InlineData("/About")]
[InlineData("/Privacy")]
[InlineData("/Contact")]
public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
{
// Arrange
var client = _factory.CreateClient();
// Act
var response = await client.GetAsync(url);
// Assert
response.EnsureSuccessStatusCode(); // Status Code 200-299
Assert.Equal("text/html; charset=utf-8",
response.Content.Headers.ContentType.ToString());
}
}
[TestClass]
public class BasicTests
{
private static CustomWebApplicationFactory<Program> _factory;
[ClassInitialize]
public static void AssemblyInitialize(TestContext _)
{
_factory = new CustomWebApplicationFactory<Program>();
}
[ClassCleanup(ClassCleanupBehavior.EndOfClass)]
public static void AssemblyCleanup(TestContext _)
{
_factory.Dispose();
}
[TestMethod]
[DataRow("/")]
[DataRow("/Index")]
[DataRow("/About")]
[DataRow("/Privacy")]
[DataRow("/Contact")]
public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
{
// Arrange
var client = _factory.CreateClient();
// Act
var response = await client.GetAsync(url);
// Assert
response.EnsureSuccessStatusCode(); // Status Code 200-299
Assert.AreEqual("text/html; charset=utf-8",
response.Content.Headers.ContentType.ToString());
}
}
public class BasicTests
{
private CustomWebApplicationFactory<Program>
_factory;
[SetUp]
public void SetUp()
{
_factory = new CustomWebApplicationFactory<Program>();
}
[TearDown]
public void TearDown()
{
_factory.Dispose();
}
[DatapointSource]
public string[] values = ["/", "/Index", "/About", "/Privacy", "/Contact"];
[Theory]
public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
{
// Arrange
var client = _factory.CreateClient();
// Act
var response = await client.GetAsync(url);
// Assert
response.EnsureSuccessStatusCode(); // Status Code 200-299
Assert.That(response.Content.Headers.ContentType.ToString(), Is.EqualTo("text/html; charset=utf-8"));
}
}
Secara default, cookie yang tidak penting tidak dipertahankan di seluruh permintaan saat kebijakan persetujuan Peraturan Perlindungan Data Umum diaktifkan. Untuk mempertahankan cookie yang tidak penting, seperti yang digunakan oleh penyedia TempData, tandai sebagai esensial dalam pengujian Anda. Untuk petunjuk tentang menandai cookie sebagai penting, lihat Cookie penting.
AngleSharp vs Application Parts
untuk pemeriksaan antipemalsuan
Artikel ini menggunakan AngleSharp parser untuk menangani pemeriksaan antiforgery, dengan cara memuat halaman dan mengurai HTML. Untuk menguji titik akhir dari pengontrol dan tampilan Halaman pada tingkat yang lebih rendah tanpa mempedulikan bagaimana tampilan tersebut dirender di browser, pertimbangkan untuk menggunakan Razor. Pendekatan Bagian Aplikasi menyuntikkan pengontrol atau Razor Halaman yang dapat digunakan untuk membuat permintaan JSON ke dalam aplikasi untuk mendapatkan nilai yang diperlukan. Untuk informasi selengkapnya, lihat blog Pengujian Integrasi Sumber Daya ASP.NET Core yang Dilindungi dengan Antiforgery Menggunakan Bagian Aplikasi dan repo GitHub terkait oleh Martin Costello.
Menyesuaikan WebApplicationFactory
Konfigurasi host web dapat dibuat secara independen dari kelas pengujian dengan mewarisi dari WebApplicationFactory<TEntryPoint> untuk membuat satu atau beberapa pabrik kustom:
Warisi dari
WebApplicationFactory
dan ambil alih ConfigureWebHost. IWebHostBuilder memungkinkan konfigurasi kumpulan layanan denganIWebHostBuilder.ConfigureServices
public class CustomWebApplicationFactory<TProgram> : WebApplicationFactory<TProgram> where TProgram : class { protected override void ConfigureWebHost(IWebHostBuilder builder) { builder.ConfigureServices(services => { var dbContextDescriptor = services.SingleOrDefault( d => d.ServiceType == typeof(IDbContextOptionsConfiguration<ApplicationDbContext>)); services.Remove(dbContextDescriptor); var dbConnectionDescriptor = services.SingleOrDefault( d => d.ServiceType == typeof(DbConnection)); services.Remove(dbConnectionDescriptor); // Create open SqliteConnection so EF won't automatically close it. services.AddSingleton<DbConnection>(container => { var connection = new SqliteConnection("DataSource=:memory:"); connection.Open(); return connection; }); services.AddDbContext<ApplicationDbContext>((container, options) => { var connection = container.GetRequiredService<DbConnection>(); options.UseSqlite(connection); }); }); builder.UseEnvironment("Development"); } }
public class CustomWebApplicationFactory<TProgram> : WebApplicationFactory<TProgram> where TProgram : class { protected override void ConfigureWebHost(IWebHostBuilder builder) { builder.ConfigureServices(services => { var dbContextDescriptor = services.SingleOrDefault( d => d.ServiceType == typeof(IDbContextOptionsConfiguration<ApplicationDbContext>)); services.Remove(dbContextDescriptor); var dbConnectionDescriptor = services.SingleOrDefault( d => d.ServiceType == typeof(DbConnection)); services.Remove(dbConnectionDescriptor); // Create open SqliteConnection so EF won't automatically close it. services.AddSingleton<DbConnection>(container => { var connection = new SqliteConnection("DataSource=:memory:"); connection.Open(); return connection; }); services.AddDbContext<ApplicationDbContext>((container, options) => { var connection = container.GetRequiredService<DbConnection>(); options.UseSqlite(connection); }); }); builder.UseEnvironment("Development"); } }
public class CustomWebApplicationFactory<TProgram> : WebApplicationFactory<TProgram> where TProgram : class { protected override void ConfigureWebHost(IWebHostBuilder builder) { builder.ConfigureServices(services => { var dbContextDescriptor = services.SingleOrDefault( d => d.ServiceType == typeof(IDbContextOptionsConfiguration<ApplicationDbContext>)); services.Remove(dbContextDescriptor); var dbConnectionDescriptor = services.SingleOrDefault( d => d.ServiceType == typeof(DbConnection)); services.Remove(dbConnectionDescriptor); // Create open SqliteConnection so EF won't automatically close it. services.AddSingleton<DbConnection>(container => { var connection = new SqliteConnection("DataSource=:memory:"); connection.Open(); return connection; }); services.AddDbContext<ApplicationDbContext>((container, options) => { var connection = container.GetRequiredService<DbConnection>(); options.UseSqlite(connection); }); }); builder.UseEnvironment("Development"); } }
Penyemaian database di aplikasi sampel dilakukan dengan
InitializeDbForTests
metode . Metode ini dijelaskan di bagian Sampel pengujian integrasi: Menguji organisasi aplikasi.Konteks database SUT terdaftar di
Program.cs
. Panggilan balikbuilder.ConfigureServices
dari aplikasi pengujian dijalankan setelah kode dari aplikasiProgram.cs
dijalankan. Untuk menggunakan database yang berbeda untuk pengujian daripada database aplikasi, konteks database aplikasi harus diganti dibuilder.ConfigureServices
.Aplikasi sampel menemukan pendeskripsi layanan untuk konteks database dan menggunakan deskriptor untuk menghapus pendaftaran layanan. Pabrik kemudian menambahkan sebuah
ApplicationDbContext
baru yang menggunakan database berbasis memori untuk pengujian.Untuk menyambungkan ke database lain, ubah
DbConnection
. Untuk menggunakan database pengujian SQL Server:- Rujuk paket
Microsoft.EntityFrameworkCore.SqlServer
NuGet dalam file proyek. - Panggil
UseInMemoryDatabase
.
- Rujuk paket
Gunakan kustom
CustomWebApplicationFactory
dalam kelas pengujian. Contoh berikut menggunakan pabrik di kelasIndexPageTests
:public class IndexPageTests : IClassFixture<CustomWebApplicationFactory<Program>> { private readonly HttpClient _client; private readonly CustomWebApplicationFactory<Program> _factory; public IndexPageTests( CustomWebApplicationFactory<Program> factory) { _factory = factory; _client = factory.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false }); }
[TestClass] public class IndexPageTests { private static HttpClient _client; private static CustomWebApplicationFactory<Program> _factory; [ClassInitialize] public static void AssemblyInitialize(TestContext _) { _factory = new CustomWebApplicationFactory<Program>(); _client = _factory.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false }); } [ClassCleanup(ClassCleanupBehavior.EndOfClass)] public static void AssemblyCleanup(TestContext _) { _factory.Dispose(); }
public class IndexPageTests { private HttpClient _client; private CustomWebApplicationFactory<Program> _factory; [SetUp] public void SetUp() { _factory = new CustomWebApplicationFactory<Program>(); _client = _factory.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false }); } [TearDown] public void TearDown() { _factory.Dispose(); _client.Dispose(); }
Klien aplikasi sampel dikonfigurasi untuk mencegah
HttpClient
pengalihan berikut. Seperti yang dijelaskan kemudian di bagian Mock autentikasi, ini memungkinkan pengujian untuk memeriksa hasil dari respons pertama aplikasi. Respons pertama adalah pengalihan di banyak pengujian ini denganLocation
header.Pengujian umum menggunakan metode dan pembantu
HttpClient
untuk memproses permintaan dan respons:[Fact] public async Task Post_DeleteAllMessagesHandler_ReturnsRedirectToRoot() { // Arrange var defaultPage = await _client.GetAsync("/"); var content = await HtmlHelpers.GetDocumentAsync(defaultPage); //Act var response = await _client.SendAsync( (IHtmlFormElement)content.QuerySelector("form[id='messages']"), (IHtmlButtonElement)content.QuerySelector("button[id='deleteAllBtn']")); // Assert Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode); Assert.Equal(HttpStatusCode.Redirect, response.StatusCode); Assert.Equal("/", response.Headers.Location.OriginalString); }
[TestMethod] public async Task Post_DeleteAllMessagesHandler_ReturnsRedirectToRoot() { // Arrange var defaultPage = await _client.GetAsync("/"); var content = await HtmlHelpers.GetDocumentAsync(defaultPage); //Act var response = await _client.SendAsync( (IHtmlFormElement)content.QuerySelector("form[id='messages']"), (IHtmlButtonElement)content.QuerySelector("button[id='deleteAllBtn']")); // Assert Assert.AreEqual(HttpStatusCode.OK, defaultPage.StatusCode); Assert.AreEqual(HttpStatusCode.Redirect, response.StatusCode); Assert.AreEqual("/", response.Headers.Location.OriginalString); }
[Test] public async Task Post_DeleteAllMessagesHandler_ReturnsRedirectToRoot() { // Arrange var defaultPage = await _client.GetAsync("/"); var content = await HtmlHelpers.GetDocumentAsync(defaultPage); //Act var response = await _client.SendAsync( (IHtmlFormElement)content.QuerySelector("form[id='messages']"), (IHtmlButtonElement)content.QuerySelector("button[id='deleteAllBtn']")); // Assert Assert.That(defaultPage.StatusCode, Is.EqualTo(HttpStatusCode.OK)); Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Redirect)); Assert.That(response.Headers.Location.OriginalString, Is.EqualTo("/")); }
Setiap permintaan POST ke SUT harus memenuhi pemeriksaan anti-pemalsuan yang secara otomatis dilakukan oleh sistem perlindungan data aplikasi. Untuk mengatur permintaan POST pengujian, aplikasi pengujian harus:
- Buat permintaan untuk halaman tersebut.
- Uraikan antiforgery cookie dan minta token validasi dari respons.
- Lakukan permintaan POST dengan antiforgery cookie dan pastikan token validasi permintaan tersedia.
Metode ekstensi SendAsync
pembantu (Helpers/HttpClientExtensions.cs
) dan metode pembantu GetDocumentAsync
(Helpers/HtmlHelpers.cs
) dalam aplikasi contoh menggunakan pengurai AngleSharp untuk menangani pemeriksaan antiforgery dengan metode berikut:
-
GetDocumentAsync
: Menerima HttpResponseMessage dan mengembalikanIHtmlDocument
.GetDocumentAsync
menggunakan pengaturan yang menyiapkan respons virtual berdasarkanHttpResponseMessage
yang asli. Untuk informasi selengkapnya, lihat dokumentasi AngleSharp. -
SendAsync
metode ekstensi untukHttpClient
menyusun sebuah HttpRequestMessage dan memanggil SendAsync(HttpRequestMessage) untuk mengirimkan permintaan ke SUT. Kelebihan beban untukSendAsync
menerima formulir HTML (IHtmlFormElement
) dan yang berikut:- Tombol Kirim pada formulir (
IHtmlElement
) - Kumpulan nilai formulir (
IEnumerable<KeyValuePair<string, string>>
) - Tombol Kirim (
IHtmlElement
) dan nilai formulir (IEnumerable<KeyValuePair<string, string>>
)
- Tombol Kirim pada formulir (
AngleSharp adalah pustaka penguraian pihak ketiga yang digunakan untuk tujuan demonstrasi dalam artikel ini dan aplikasi sampel. AngleSharp tidak didukung atau diperlukan untuk pengujian integrasi aplikasi ASP.NET Core. Pengurai lain dapat digunakan, seperti Html Agility Pack (HAP). Pendekatan lain adalah menulis kode untuk menangani token verifikasi permintaan sistem antiforgery dan antiforgery cookie secara langsung. Lihat AngleSharp vs Application Parts
untuk pemeriksaan antiforgery di artikel ini untuk informasi selengkapnya.
Penyedia database dalam memori EF-Core dapat digunakan untuk pengujian terbatas dan dasar, namun penyedia SQLite adalah pilihan yang direkomendasikan untuk pengujian dalam memori.
Lihat Perluas Startup dengan filter startup yang menunjukkan cara mengonfigurasi middleware menggunakan IStartupFilter, yang berguna ketika pengujian membutuhkan layanan atau middleware kustom.
Menyesuaikan klien dengan WithWebHostBuilder
Ketika konfigurasi tambahan diperlukan dalam metode pengujian, WithWebHostBuilder membuat WebApplicationFactory
baru dengan IWebHostBuilder yang disesuaikan lebih lanjut oleh konfigurasi.
Kode sampel memanggil WithWebHostBuilder
untuk menggantikan layanan yang telah dikonfigurasi dengan stub pengujian. Untuk informasi selengkapnya dan contoh penggunaan, lihat Inject mock services di artikel ini.
Metode pengujian Post_DeleteMessageHandler_ReturnsRedirectToRoot
aplikasi sampel menunjukkan penggunaan WithWebHostBuilder
. Pengujian ini melakukan penghapusan rekaman dalam database dengan memicu pengiriman formulir di SUT.
Karena pengujian lain di IndexPageTests
melakukan operasi di kelas yang menghapus semua rekaman dalam database dan mungkin dijalankan sebelum metode Post_DeleteMessageHandler_ReturnsRedirectToRoot
, database diinisialisasi ulang dalam metode pengujian ini untuk memastikan adanya rekaman yang dapat dihapus oleh Sistem yang Diuji (SUT). Memilih tombol pertama hapus dari formulir messages
di SUT disimulasikan dalam permintaan ke SUT.
[Fact]
public async Task Post_DeleteMessageHandler_ReturnsRedirectToRoot()
{
// Arrange
using (var scope = _factory.Services.CreateScope())
{
var scopedServices = scope.ServiceProvider;
var db = scopedServices.GetRequiredService<ApplicationDbContext>();
Utilities.ReinitializeDbForTests(db);
}
var defaultPage = await _client.GetAsync("/");
var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
//Act
var response = await _client.SendAsync(
(IHtmlFormElement)content.QuerySelector("form[id='messages']"),
(IHtmlButtonElement)content.QuerySelector("form[id='messages']")
.QuerySelector("div[class='panel-body']")
.QuerySelector("button"));
// Assert
Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode);
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
Assert.Equal("/", response.Headers.Location.OriginalString);
}
[TestMethod]
public async Task Post_DeleteMessageHandler_ReturnsRedirectToRoot()
{
// Arrange
using (var scope = _factory.Services.CreateScope())
{
var scopedServices = scope.ServiceProvider;
var db = scopedServices.GetRequiredService<ApplicationDbContext>();
Utilities.ReinitializeDbForTests(db);
}
var defaultPage = await _client.GetAsync("/");
var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
//Act
var response = await _client.SendAsync(
(IHtmlFormElement)content.QuerySelector("form[id='messages']"),
(IHtmlButtonElement)content.QuerySelector("form[id='messages']")
.QuerySelector("div[class='panel-body']")
.QuerySelector("button"));
// Assert
Assert.AreEqual(HttpStatusCode.OK, defaultPage.StatusCode);
Assert.AreEqual(HttpStatusCode.Redirect, response.StatusCode);
Assert.AreEqual("/", response.Headers.Location.OriginalString);
}
[Test]
public async Task Post_DeleteMessageHandler_ReturnsRedirectToRoot()
{
// Arrange
using (var scope = _factory.Services.CreateScope())
{
var scopedServices = scope.ServiceProvider;
var db = scopedServices.GetRequiredService<ApplicationDbContext>();
Utilities.ReinitializeDbForTests(db);
}
var defaultPage = await _client.GetAsync("/");
var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
//Act
var response = await _client.SendAsync(
(IHtmlFormElement)content.QuerySelector("form[id='messages']"),
(IHtmlButtonElement)content.QuerySelector("form[id='messages']")
.QuerySelector("div[class='panel-body']")
.QuerySelector("button"));
// Assert
Assert.That(defaultPage.StatusCode, Is.EqualTo(HttpStatusCode.OK));
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Redirect));
Assert.That(response.Headers.Location.OriginalString, Is.EqualTo("/"));
}
Pengaturan klien
Lihat halaman WebApplicationFactoryClientOptions untuk default dan opsi yang tersedia saat membuat instance HttpClient
.
Buat kelas WebApplicationFactoryClientOptions
dan passing ke metode CreateClient().
public class IndexPageTests :
IClassFixture<CustomWebApplicationFactory<Program>>
{
private readonly HttpClient _client;
private readonly CustomWebApplicationFactory<Program>
_factory;
public IndexPageTests(
CustomWebApplicationFactory<Program> factory)
{
_factory = factory;
_client = factory.CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false
});
}
[TestClass]
public class IndexPageTests
{
private static HttpClient _client;
private static CustomWebApplicationFactory<Program>
_factory;
[ClassInitialize]
public static void AssemblyInitialize(TestContext _)
{
_factory = new CustomWebApplicationFactory<Program>();
_client = _factory.CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false
});
}
[ClassCleanup(ClassCleanupBehavior.EndOfClass)]
public static void AssemblyCleanup(TestContext _)
{
_factory.Dispose();
}
public class IndexPageTests
{
private HttpClient _client;
private CustomWebApplicationFactory<Program>
_factory;
[SetUp]
public void SetUp()
{
_factory = new CustomWebApplicationFactory<Program>();
_client = _factory.CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false
});
}
[TearDown]
public void TearDown()
{
_factory.Dispose();
_client.Dispose();
}
CATATAN: Untuk menghindari peringatan pengalihan HTTPS dalam log saat menggunakan Middleware Pengalihan HTTPS, atur BaseAddress = new Uri("https://localhost")
Menyuntikkan layanan contoh
Layanan dapat ditimpa dalam pengujian dengan panggilan ke ConfigureTestServices pada pembuat host. Untuk membatasi layanan yang ditimpa khusus untuk pengujian itu sendiri, metode WithWebHostBuilder ini digunakan untuk mendapatkan pembangun host. Ini dapat dilihat dalam pengujian berikut:
- Dapatkan_LayananKutipan_MenyediakanKutipanDiHalaman
- Get_GithubProfilePageCanGetAGithubUser
- Dapatkan_HalamanAmanDikembalikanUntukPenggunaTerautentikasi
Sampel SUT menyertakan layanan terlingkup yang mengembalikan kuotasi. Kutipan tersebut disematkan di bidang tersembunyi pada halaman Indeks ketika diminta.
Services/IQuoteService.cs
:
public interface IQuoteService
{
Task<string> GenerateQuote();
}
Services/QuoteService.cs
:
// Quote ©1975 BBC: The Doctor (Tom Baker); Dr. Who: Planet of Evil
// https://www.bbc.co.uk/programmes/p00pyrx6
public class QuoteService : IQuoteService
{
public Task<string> GenerateQuote()
{
return Task.FromResult<string>(
"Come on, Sarah. We've an appointment in London, " +
"and we're already 30,000 years late.");
}
}
Program.cs
:
services.AddScoped<IQuoteService, QuoteService>();
Pages/Index.cshtml.cs
:
public class IndexModel : PageModel
{
private readonly ApplicationDbContext _db;
private readonly IQuoteService _quoteService;
public IndexModel(ApplicationDbContext db, IQuoteService quoteService)
{
_db = db;
_quoteService = quoteService;
}
[BindProperty]
public Message Message { get; set; }
public IList<Message> Messages { get; private set; }
[TempData]
public string MessageAnalysisResult { get; set; }
public string Quote { get; private set; }
public async Task OnGetAsync()
{
Messages = await _db.GetMessagesAsync();
Quote = await _quoteService.GenerateQuote();
}
Pages/Index.cs
:
<input id="quote" type="hidden" value="@Model.Quote">
Markup berikut dihasilkan saat aplikasi SUT dijalankan:
<input id="quote" type="hidden" value="Come on, Sarah. We've an appointment in
London, and we're already 30,000 years late.">
Untuk menguji layanan dan menyuntikkan mock service dalam pengujian integrasi, layanan tiruan disuntikkan ke SUT oleh tes. Layanan tiruan mengganti aplikasi QuoteService
dengan layanan yang disediakan oleh aplikasi pengujian, yang disebut TestQuoteService
:
IntegrationTests.IndexPageTests.cs
:
// Quote ©1975 BBC: The Doctor (Tom Baker); Pyramids of Mars
// https://www.bbc.co.uk/programmes/p00pys55
public class TestQuoteService : IQuoteService
{
public Task<string> GenerateQuote()
{
return Task.FromResult(
"Something's interfering with time, Mr. Scarman, " +
"and time is my business.");
}
}
// Quote ©1975 BBC: The Doctor (Tom Baker); Pyramids of Mars
// https://www.bbc.co.uk/programmes/p00pys55
public class TestQuoteService : IQuoteService
{
public Task<string> GenerateQuote()
{
return Task.FromResult(
"Something's interfering with time, Mr. Scarman, " +
"and time is my business.");
}
}
// Quote ©1975 BBC: The Doctor (Tom Baker); Pyramids of Mars
// https://www.bbc.co.uk/programmes/p00pys55
public class TestQuoteService : IQuoteService
{
public Task<string> GenerateQuote()
{
return Task.FromResult(
"Something's interfering with time, Mr. Scarman, " +
"and time is my business.");
}
}
ConfigureTestServices
dipanggil, dan layanan terlingkup terdaftar:
[Fact]
public async Task Get_QuoteService_ProvidesQuoteInPage()
{
// Arrange
var client = _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.AddScoped<IQuoteService, TestQuoteService>();
});
})
.CreateClient();
//Act
var defaultPage = await client.GetAsync("/");
var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
var quoteElement = content.QuerySelector("#quote");
// Assert
Assert.Equal("Something's interfering with time, Mr. Scarman, " +
"and time is my business.", quoteElement.Attributes["value"].Value);
}
[TestMethod]
public async Task Get_QuoteService_ProvidesQuoteInPage()
{
// Arrange
var client = _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.AddScoped<IQuoteService, TestQuoteService>();
});
})
.CreateClient();
//Act
var defaultPage = await client.GetAsync("/");
var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
var quoteElement = content.QuerySelector("#quote");
// Assert
Assert.AreEqual("Something's interfering with time, Mr. Scarman, " +
"and time is my business.", quoteElement.Attributes["value"].Value);
}
[Test]
public async Task Get_QuoteService_ProvidesQuoteInPage()
{
// Arrange
var client = _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.AddScoped<IQuoteService, TestQuoteService>();
});
})
.CreateClient();
//Act
var defaultPage = await client.GetAsync("/");
var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
var quoteElement = content.QuerySelector("#quote");
// Assert
Assert.That(quoteElement.Attributes["value"].Value, Is.EqualTo(
"Something's interfering with time, Mr. Scarman, " +
"and time is my business."));
}
Markup yang dihasilkan selama eksekusi pengujian mencerminkan teks kutipan yang disediakan oleh TestQuoteService
, sehingga pernyataan lolos:
<input id="quote" type="hidden" value="Something's interfering with time,
Mr. Scarman, and time is my business.">
Autentikasi tiruan
Pengujian pada kelas AuthTests
memeriksa bahwa suatu titik akhir aman:
- Mengalihkan pengguna yang tidak diautentikasi ke halaman masuk aplikasi.
- Mengembalikan konten untuk pengguna yang diautentikasi.
Dalam SUT, /SecurePage
halaman menggunakan AuthorizePage konvensi untuk menerapkan AuthorizeFilter pada halaman. Untuk informasi selengkapnya, lihat Razor Konvensi otorisasi halaman.
services.AddRazorPages(options =>
{
options.Conventions.AuthorizePage("/SecurePage");
});
Dalam Get_SecurePageRedirectsAnUnauthenticatedUser
pengujian, WebApplicationFactoryClientOptions diatur untuk melarang pengalihan dengan menetapkan AllowAutoRedirect ke false
:
[Fact]
public async Task Get_SecurePageRedirectsAnUnauthenticatedUser()
{
// Arrange
var client = _factory.CreateClient(
new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false
});
// Act
var response = await client.GetAsync("/SecurePage");
// Assert
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
Assert.StartsWith("http://localhost/Identity/Account/Login",
response.Headers.Location.OriginalString);
}
[TestMethod]
public async Task Get_SecurePageRedirectsAnUnauthenticatedUser()
{
// Arrange
var client = _factory.CreateClient(
new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false
});
// Act
var response = await client.GetAsync("/SecurePage");
// Assert
Assert.AreEqual(HttpStatusCode.Redirect, response.StatusCode);
StringAssert.StartsWith(response.Headers.Location.OriginalString, "http://localhost/Identity/Account/Login");
}
[Test]
public async Task Get_SecurePageRedirectsAnUnauthenticatedUser()
{
// Arrange
var client = _factory.CreateClient(
new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false
});
// Act
var response = await client.GetAsync("/SecurePage");
// Assert
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Redirect));
Assert.That(response.Headers.Location.OriginalString, Does.StartWith("http://localhost/Identity/Account/Login"));
}
Dengan melarang klien untuk mengikuti pengalihan, pemeriksaan berikut dapat dilakukan:
- Kode status yang dikembalikan oleh SUT dapat diperiksa terhadap hasil yang diharapkan HttpStatusCode.Redirect , bukan kode status akhir setelah pengalihan ke halaman masuk, yang akan menjadi HttpStatusCode.OK.
- Nilai header
Location
di header respons diperiksa untuk mengonfirmasi bahwa nilai tersebut dimulai denganhttp://localhost/Identity/Account/Login
, bukan respons halaman akhir masuk, di manaLocation
header tidak hadir.
Aplikasi pengujian dapat meniru AuthenticationHandler<TOptions>ConfigureTestServices untuk menguji aspek autentikasi dan otorisasi. Skenario yang minimal menghasilkan AuthenticateResult.Success.
public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
public TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger, UrlEncoder encoder)
: base(options, logger, encoder)
{
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var claims = new[] { new Claim(ClaimTypes.Name, "Test user") };
var identity = new ClaimsIdentity(claims, "Test");
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, "TestScheme");
var result = AuthenticateResult.Success(ticket);
return Task.FromResult(result);
}
}
public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
public TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger, UrlEncoder encoder)
: base(options, logger, encoder)
{
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var claims = new[] { new Claim(ClaimTypes.Name, "Test user") };
var identity = new ClaimsIdentity(claims, "Test");
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, "TestScheme");
var result = AuthenticateResult.Success(ticket);
return Task.FromResult(result);
}
}
public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
public TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger, UrlEncoder encoder)
: base(options, logger, encoder)
{
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var claims = new[] { new Claim(ClaimTypes.Name, "Test user") };
var identity = new ClaimsIdentity(claims, "Test");
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, "TestScheme");
var result = AuthenticateResult.Success(ticket);
return Task.FromResult(result);
}
}
TestAuthHandler
dipanggil untuk mengautentikasi pengguna ketika skema autentikasi diatur ke TestScheme
di mana AddAuthentication
terdaftar untuk ConfigureTestServices
. Penting agar skema TestScheme
sesuai dengan skema yang diharapkan oleh aplikasi Anda. Jika tidak, autentikasi tidak akan berfungsi.
[Fact]
public async Task Get_SecurePageIsReturnedForAnAuthenticatedUser()
{
// Arrange
var client = _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.AddAuthentication(defaultScheme: "TestScheme")
.AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(
"TestScheme", options => { });
});
})
.CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false,
});
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue(scheme: "TestScheme");
//Act
var response = await client.GetAsync("/SecurePage");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
[TestMethod]
public async Task Get_SecurePageIsReturnedForAnAuthenticatedUser()
{
// Arrange
var client = _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.AddAuthentication(defaultScheme: "TestScheme")
.AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(
"TestScheme", options => { });
});
})
.CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false,
});
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue(scheme: "TestScheme");
//Act
var response = await client.GetAsync("/SecurePage");
// Assert
Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
}
[Test]
public async Task Get_SecurePageIsReturnedForAnAuthenticatedUser()
{
// Arrange
var client = _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.AddAuthentication(defaultScheme: "TestScheme")
.AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(
"TestScheme", options => { });
});
})
.CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false,
});
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue(scheme: "TestScheme");
//Act
var response = await client.GetAsync("/SecurePage");
// Assert
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
}
Untuk informasi selengkapnya tentang WebApplicationFactoryClientOptions
, lihat bagian Opsi klien.
Pengujian dasar untuk middleware autentikasi
Lihat repositori GitHub ini untuk pengujian dasar middleware autentikasi. Ini berisi server pengujian yang khusus untuk skenario pengujian.
Mengatur lingkungan
Atur lingkungan di pabrik aplikasi kustom:
public class CustomWebApplicationFactory<TProgram>
: WebApplicationFactory<TProgram> where TProgram : class
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
var dbContextDescriptor = services.SingleOrDefault(
d => d.ServiceType ==
typeof(IDbContextOptionsConfiguration<ApplicationDbContext>));
services.Remove(dbContextDescriptor);
var dbConnectionDescriptor = services.SingleOrDefault(
d => d.ServiceType ==
typeof(DbConnection));
services.Remove(dbConnectionDescriptor);
// Create open SqliteConnection so EF won't automatically close it.
services.AddSingleton<DbConnection>(container =>
{
var connection = new SqliteConnection("DataSource=:memory:");
connection.Open();
return connection;
});
services.AddDbContext<ApplicationDbContext>((container, options) =>
{
var connection = container.GetRequiredService<DbConnection>();
options.UseSqlite(connection);
});
});
builder.UseEnvironment("Development");
}
}
public class CustomWebApplicationFactory<TProgram>
: WebApplicationFactory<TProgram> where TProgram : class
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
var dbContextDescriptor = services.SingleOrDefault(
d => d.ServiceType ==
typeof(IDbContextOptionsConfiguration<ApplicationDbContext>));
services.Remove(dbContextDescriptor);
var dbConnectionDescriptor = services.SingleOrDefault(
d => d.ServiceType ==
typeof(DbConnection));
services.Remove(dbConnectionDescriptor);
// Create open SqliteConnection so EF won't automatically close it.
services.AddSingleton<DbConnection>(container =>
{
var connection = new SqliteConnection("DataSource=:memory:");
connection.Open();
return connection;
});
services.AddDbContext<ApplicationDbContext>((container, options) =>
{
var connection = container.GetRequiredService<DbConnection>();
options.UseSqlite(connection);
});
});
builder.UseEnvironment("Development");
}
}
public class CustomWebApplicationFactory<TProgram>
: WebApplicationFactory<TProgram> where TProgram : class
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
var dbContextDescriptor = services.SingleOrDefault(
d => d.ServiceType ==
typeof(IDbContextOptionsConfiguration<ApplicationDbContext>));
services.Remove(dbContextDescriptor);
var dbConnectionDescriptor = services.SingleOrDefault(
d => d.ServiceType ==
typeof(DbConnection));
services.Remove(dbConnectionDescriptor);
// Create open SqliteConnection so EF won't automatically close it.
services.AddSingleton<DbConnection>(container =>
{
var connection = new SqliteConnection("DataSource=:memory:");
connection.Open();
return connection;
});
services.AddDbContext<ApplicationDbContext>((container, options) =>
{
var connection = container.GetRequiredService<DbConnection>();
options.UseSqlite(connection);
});
});
builder.UseEnvironment("Development");
}
}
Cara infrastruktur pengujian menyimpulkan jalur akar konten aplikasi
WebApplicationFactory
Konstruktor menyimpulkan jalur akar konten aplikasi dengan mencari pada rakitan yang WebApplicationFactoryContentRootAttribute berisi pengujian integrasi dengan kunci yang sama dengan TEntryPoint
rakitan System.Reflection.Assembly.FullName
. Jika atribut dengan kunci yang benar tidak ditemukan, WebApplicationFactory
kembali mencari file solusi (.sln) dan menambahkan nama perakitan TEntryPoint
ke direktori solusi. Direktori akar aplikasi (jalur akar konten) digunakan untuk menemukan tampilan dan file konten.
Menonaktifkan penyalinan bayangan
Penyalinan bayangan menyebabkan pengujian dijalankan di direktori yang berbeda dari direktori output. Jika pengujian Anda mengandalkan pemuatan file yang relatif terhadap Assembly.Location
dan Anda mengalami masalah, Anda mungkin harus menonaktifkan penyalinan bayangan.
Untuk menonaktifkan penyalinan bayangan saat menggunakan xUnit, buat xunit.runner.json
file di direktori proyek pengujian Anda, dengan pengaturan konfigurasi yang benar:
{
"shadowCopy": false
}
Pembuangan objek
Setelah pengujian IClassFixture
implementasi dijalankan, TestServer dan HttpClient dibuang ketika xUnit membuang WebApplicationFactory
. Jika objek yang dibuat oleh pengembang memerlukan pembuangan, buang dalam implementasi IClassFixture
. Untuk informasi selengkapnya, lihat Menerapkan metode Dispose.
Setelah pengujian TestClass
dijalankan, TestServer dan HttpClient dibuang ketika MSTest membuang WebApplicationFactory
di metode ClassCleanup
. Jika objek yang dibuat oleh pengembang memerlukan pembuangan, lakukan pembuangan tersebut di dalam metode ClassCleanup
. Untuk informasi selengkapnya, lihat Menerapkan metode Dispose.
Setelah pengujian kelas uji dijalankan, TestServer dan HttpClient dibuang ketika NUnit membuang WebApplicationFactory
dalam metode TearDown
. Jika objek yang dibuat oleh pengembang memerlukan pembuangan, lakukan pembuangan tersebut di dalam metode TearDown
. Untuk informasi selengkapnya, lihat Menerapkan metode Dispose.
Contoh pengujian integrasi
Aplikasi sampel terdiri dari dua aplikasi:
Aplikasi | Direktori proyek | Deskripsi |
---|---|---|
Aplikasi pesan (SUT) | src/RazorPagesProject |
Memungkinkan pengguna untuk menambahkan, menghapus satu, menghapus semua, dan menganalisis pesan. |
Menguji aplikasi | tests/RazorPagesProject.Tests |
Digunakan untuk menguji integrasi SUT. |
Pengujian dapat dijalankan menggunakan fitur pengujian bawaan IDE, seperti Visual Studio. Jika menggunakan Visual Studio Code atau baris perintah, jalankan perintah berikut pada prompt perintah di tests/RazorPagesProject.Tests
direktori:
dotnet test
Organisasi aplikasi pesan (SUT)
SUT adalah Razor sistem pesan Pages dengan karakteristik berikut:
- Halaman Indeks aplikasi (
Pages/Index.cshtml
danPages/Index.cshtml.cs
) menyediakan metode UI dan model halaman untuk mengontrol penambahan, penghapusan, dan analisis pesan (kata rata-rata per pesan). - Pesan dijelaskan oleh
Message
kelas (Data/Message.cs
) dengan dua properti:Id
(kunci) danText
(pesan). PropertiText
diperlukan dan dibatasi hingga 200 karakter. - Pesan disimpan menggunakan database dalam memori Entity Framework†.
- Aplikasi ini berisi lapisan akses data (DAL) di kelas konteks databasenya,
AppDbContext
(Data/AppDbContext.cs
). - Jika database kosong pada startup aplikasi, penyimpanan pesan diinisialisasi dengan tiga pesan.
- Aplikasi ini menyertakan
/SecurePage
yang hanya dapat diakses oleh pengguna yang diautentikasi.
† Artikel EF, Uji dengan InMemory, menjelaskan cara menggunakan database dalam memori untuk pengujian dengan MSTest. Topik ini menggunakan kerangka kerja pengujian xUnit . Konsep pengujian dan implementasi pengujian di berbagai kerangka kerja pengujian serupa tetapi tidak identik.
Meskipun aplikasi tidak menggunakan pola repositori dan bukan contoh pola Unit kerja (UoW) yang efektif, Razor Pages mendukung pola pengembangan ini. Untuk informasi selengkapnya, lihat Merancang lapisan persistensi infrastruktur dan Logika pengontrol Pengujian (sampel mengimplementasikan pola repositori).
Menguji organisasi aplikasi
Aplikasi pengujian adalah aplikasi konsol di direktori tests/RazorPagesProject.Tests
.
Menguji direktori aplikasi | Deskripsi |
---|---|
AuthTests |
Berisi metode pengujian untuk:
|
BasicTests |
Berisi metode pengujian untuk perutean dan jenis konten. |
IntegrationTests |
Berisi pengujian integrasi untuk halaman Indeks menggunakan kelas kustom WebApplicationFactory . |
Helpers/Utilities |
|
Kerangka kerja pengujian adalah xUnit. Pengujian integrasi dilakukan menggunakan Microsoft.AspNetCore.TestHost, yang mencakup TestServer. Karena paket Microsoft.AspNetCore.Mvc.Testing
digunakan untuk mengonfigurasi host dan server pengujian, paket TestHost
dan TestServer
tidak memerlukan referensi paket langsung dalam file proyek aplikasi pengujian atau konfigurasi pengembang di aplikasi pengujian.
Pengujian integrasi biasanya memerlukan himpunan data kecil dalam database sebelum eksekusi pengujian. Misalnya, pengujian penghapusan memerlukan penghapusan catatan dalam database, sehingga database harus memiliki setidaknya satu catatan agar penghapusan berhasil dilakukan.
Aplikasi contoh memuat database dengan tiga pesan dalam Utilities.cs
yang dapat digunakan dalam pengujian saat dijalankan.
public static void InitializeDbForTests(ApplicationDbContext db)
{
db.Messages.AddRange(GetSeedingMessages());
db.SaveChanges();
}
public static void ReinitializeDbForTests(ApplicationDbContext db)
{
db.Messages.RemoveRange(db.Messages);
InitializeDbForTests(db);
}
public static List<Message> GetSeedingMessages()
{
return new List<Message>()
{
new Message(){ Text = "TEST RECORD: You're standing on my scarf." },
new Message(){ Text = "TEST RECORD: Would you like a jelly baby?" },
new Message(){ Text = "TEST RECORD: To the rational mind, " +
"nothing is inexplicable; only unexplained." }
};
}
Konteks database SUT terdaftar di Program.cs
. Panggilan balik builder.ConfigureServices
dari aplikasi pengujian dijalankan setelah kode dari aplikasi Program.cs
dijalankan. Untuk menggunakan database yang berbeda untuk pengujian, konteks database aplikasi harus diganti di builder.ConfigureServices
. Untuk informasi selengkapnya, lihat bagian Kustomisasi WebApplicationFactory .
Sumber Daya Tambahan:
Topik ini mengasumsikan pemahaman dasar tentang pengujian unit. Jika tidak terbiasa dengan konsep pengujian, lihat Pengujian Unit dalam topik .NET Core dan .NET Standard dan konten tertautnya.
Melihat atau mengunduh kode sampel (cara mengunduh)
Aplikasi sampel adalah Razor aplikasi Pages dan mengasumsikan pengetahuan dasar mengenai Razor Pages. Jika tidak terbiasa dengan Razor Pages, lihat topik berikut:
Catatan
Untuk menguji SPAs, kami merekomendasikan alat seperti Playwright untuk .NET, yang dapat mengotomatiskan browser.
Pengantar pengujian integrasi
Pengujian integrasi mengevaluasi komponen aplikasi pada tingkat yang lebih luas daripada pengujian unit. Pengujian unit digunakan untuk menguji komponen perangkat lunak terisolasi, seperti metode kelas individual. Pengujian integrasi mengonfirmasi bahwa dua komponen aplikasi atau lebih bekerja sama untuk menghasilkan hasil yang diharapkan, mungkin termasuk setiap komponen yang diperlukan untuk memproses permintaan sepenuhnya.
Pengujian yang lebih luas ini digunakan untuk menguji infrastruktur aplikasi dan seluruh kerangka kerja, sering kali termasuk komponen berikut:
- Basis data
- Sistem file
- Appliance jaringan
- Jalur permintaan-tanggapan
Pengujian unit menggunakan komponen fabrikasi, yang dikenal sebagai objek palsu atau tiruan, sebagai pengganti komponen infrastruktur.
Berbeda dengan pengujian unit, pengujian integrasi:
- Gunakan komponen aktual yang digunakan aplikasi dalam produksi.
- Memerlukan lebih banyak kode dan pemrosesan data.
- Membutuhkan waktu lebih lama untuk berjalan.
Oleh karena itu, batasi penggunaan pengujian integrasi ke skenario infrastruktur yang paling penting. Jika perilaku dapat diuji menggunakan pengujian unit atau pengujian integrasi, pilih pengujian unit.
Dalam diskusi pengujian integrasi, proyek yang diuji sering disebut System Under Test, atau "SUT" singkatnya. "SUT" digunakan di seluruh artikel ini untuk merujuk ke aplikasi ASP.NET Core yang sedang diuji.
Jangan menulis pengujian integrasi untuk setiap permutasi akses data dan file yang melibatkan database dan sistem file. Terlepas dari berapa banyak tempat di seluruh aplikasi berinteraksi dengan database dan sistem file, serangkaian pengujian integrasi baca, tulis, perbarui, dan hapus yang berfokus biasanya mampu menguji database dan komponen sistem file dengan memadai. Gunakan pengujian unit untuk pengujian rutin logika metode yang berinteraksi dengan komponen-komponen ini. Dalam pengujian unit, penggunaan infrastruktur palsu atau tiruan menghasilkan eksekusi pengujian yang lebih cepat.
pengujian integrasi ASP.NET Core
Pengujian integrasi di ASP.NET Core memerlukan hal berikut:
- Proyek pengujian digunakan untuk memuat dan menjalankan pengujian. Proyek pengujian memiliki referensi ke SUT.
- Proyek pengujian membuat host web pengujian untuk SUT dan menggunakan klien server pengujian untuk menangani permintaan dan respons dengan SUT.
- Pelari uji digunakan untuk menjalankan pengujian dan melaporkan hasil pengujian.
Pengujian integrasi mengikuti urutan langkah pengujian yang mencakup tahapan Arrange, Act, dan Assert yang biasa:
- Server web SUT telah dikonfigurasi.
- Klien server pengujian dibuat untuk mengirimkan permintaan ke aplikasi.
- Langkah Atur pengujian dijalankan: Aplikasi pengujian menyiapkan permintaan.
- Langkah pengujian Act dijalankan: Klien mengirimkan permintaan dan menerima respons.
- Langkah pengujian Assert dijalankan: Respons aktual divalidasi sebagai lulus atau gagal berdasarkan respons yang diharapkan .
- Proses berlanjut sampai semua pengujian dijalankan.
- Hasil pengujian dilaporkan.
Biasanya, host web pengujian dikonfigurasi secara berbeda dari host web normal aplikasi untuk eksekusi pengujian. Misalnya, database yang berbeda atau pengaturan aplikasi yang berbeda dapat digunakan untuk pengujian.
Komponen infrastruktur, seperti host web pengujian dan server pengujian dalam memori (TestServer), disediakan atau dikelola oleh paket Microsoft.AspNetCore.Mvc.Testing . Penggunaan paket ini menyederhanakan pembuatan dan eksekusi pengujian.
Paket Microsoft.AspNetCore.Mvc.Testing
menangani tugas-tugas berikut:
- Menyalin file dependensi (
.deps
) dari SUT ke direktori proyekbin
pengujian. - Menetapkan akar konten ke akar proyek SUT agar file statis serta halaman dan tampilan dapat ditemukan saat pengujian berlangsung.
-
Menyediakan kelas WebApplicationFactory untuk mempermudah proses awal pengoperasian SUT dengan
TestServer
.
Dokumentasi tes unit menjelaskan cara menyiapkan proyek pengujian dan pengelola pengujian, bersama dengan instruksi terperinci tentang cara menjalankan tes serta rekomendasi tentang cara memberi nama tes dan kelas tes.
Memisahkan pengujian unit dari pengujian integrasi ke dalam proyek yang berbeda. Memisahkan pengujian:
- Membantu memastikan bahwa komponen pengujian infrastruktur tidak secara tidak sengaja disertakan dalam pengujian unit.
- Memungkinkan kontrol atas set pengujian yang akan dijalankan.
Hampir tidak ada perbedaan antara konfigurasi untuk pengujian Razor aplikasi Pages dan aplikasi MVC. Satu-satunya perbedaan adalah bagaimana tes diberi nama.
Razor Di aplikasi Pages, pengujian titik akhir halaman biasanya dinamai sesuai dengan kelas model halaman (misalnya, IndexPageTests
untuk menguji integrasi komponen untuk halaman Indeks). Dalam aplikasi MVC, pengujian biasanya diatur oleh kelas pengontrol dan dinamai sesuai pengontrol yang mereka uji (misalnya, HomeControllerTests
untuk menguji integrasi komponen untuk Home pengontrol).
Menguji prasyarat aplikasi
Proyek pengujian harus:
- Merujuk ke paket
Microsoft.AspNetCore.Mvc.Testing
. - Tentukan Web SDK dalam file proyek (
<Project Sdk="Microsoft.NET.Sdk.Web">
).
Prasyarat ini dapat dilihat di aplikasi sampel. Periksa file tests/RazorPagesProject.Tests/RazorPagesProject.Tests.csproj
. Aplikasi sampel menggunakan kerangka kerja pengujian xUnit dan pustaka pengurai AngleSharp , sehingga aplikasi sampel juga mereferensikan:
Di aplikasi yang menggunakan xunit.runner.visualstudio
versi 2.4.2 atau yang lebih baru, proyek pengujian harus mereferensikan Microsoft.NET.Test.Sdk
paket.
Entity Framework Core juga digunakan dalam pengujian. Referensi aplikasi:
Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore
Microsoft.AspNetCore.Identity.EntityFrameworkCore
Microsoft.EntityFrameworkCore
Microsoft.EntityFrameworkCore.InMemory
Microsoft.EntityFrameworkCore.Tools
Lingkungan SUT
Jika lingkungan SUT tidak diatur, lingkungan akan default ke Pengembangan.
Pengujian dasar dengan WebApplicationFactory default
WebApplicationFactory<TEntryPoint> digunakan untuk membuat TestServer untuk pengujian integrasi.
TEntryPoint
adalah kelas titik masuk SUT, biasanya kelas Startup
.
Kelas uji menerapkan antarmuka fikstur kelas untuk menunjukkan bahwa kelas tersebut berisi pengujian dan menyediakan instans objek yang dapat digunakan bersama di seluruh pengujian dalam kelas.
Kelas pengujian berikut, BasicTests
, menggunakan WebApplicationFactory
untuk bootstrap SUT dan menyediakan HttpClient untuk metode pengujian, Get_EndpointsReturnSuccessAndCorrectContentType
. Metode ini memeriksa apakah kode status respons berhasil (kode status dalam rentang 200-299) dan Content-Type
header adalah text/html; charset=utf-8
untuk beberapa halaman aplikasi.
CreateClient() membuat instans HttpClient
yang secara otomatis mengikuti pengalihan dan menangani cookie.
public class BasicTests
: IClassFixture<WebApplicationFactory<RazorPagesProject.Startup>>
{
private readonly WebApplicationFactory<RazorPagesProject.Startup> _factory;
public BasicTests(WebApplicationFactory<RazorPagesProject.Startup> factory)
{
_factory = factory;
}
[Theory]
[InlineData("/")]
[InlineData("/Index")]
[InlineData("/About")]
[InlineData("/Privacy")]
[InlineData("/Contact")]
public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
{
// Arrange
var client = _factory.CreateClient();
// Act
var response = await client.GetAsync(url);
// Assert
response.EnsureSuccessStatusCode(); // Status Code 200-299
Assert.Equal("text/html; charset=utf-8",
response.Content.Headers.ContentType.ToString());
}
}
Secara default, cookie non-esensial tidak dipertahankan di seluruh permintaan saat kebijakan persetujuan GDPR diaktifkan. Untuk mempertahankan cookie yang tidak penting, seperti yang digunakan oleh penyedia TempData, tandai sebagai esensial dalam pengujian Anda. Untuk petunjuk tentang menandai cookie sebagai penting, lihat Cookie penting.
Menyesuaikan WebApplicationFactory
Konfigurasi host web dapat dibuat secara independen dari kelas pengujian dengan mewarisi dari WebApplicationFactory
untuk membuat satu atau beberapa pabrik kustom:
Warisi dari
WebApplicationFactory
dan ambil alih ConfigureWebHost. IWebHostBuilder memungkinkan konfigurasi pengumpulan layanan dengan ConfigureServices:public class CustomWebApplicationFactory<TStartup> : WebApplicationFactory<TStartup> where TStartup: class { protected override void ConfigureWebHost(IWebHostBuilder builder) { builder.ConfigureServices(services => { var descriptor = services.SingleOrDefault( d => d.ServiceType == typeof(DbContextOptions<ApplicationDbContext>)); services.Remove(descriptor); services.AddDbContext<ApplicationDbContext>(options => { options.UseInMemoryDatabase("InMemoryDbForTesting"); }); var sp = services.BuildServiceProvider(); using (var scope = sp.CreateScope()) { var scopedServices = scope.ServiceProvider; var db = scopedServices.GetRequiredService<ApplicationDbContext>(); var logger = scopedServices .GetRequiredService<ILogger<CustomWebApplicationFactory<TStartup>>>(); db.Database.EnsureCreated(); try { Utilities.InitializeDbForTests(db); } catch (Exception ex) { logger.LogError(ex, "An error occurred seeding the " + "database with test messages. Error: {Message}", ex.Message); } } }); } }
Penyemaian database di aplikasi sampel dilakukan dengan
InitializeDbForTests
metode . Metode ini dijelaskan di bagian Sampel pengujian integrasi: Menguji organisasi aplikasi.Konteks database SUT terdaftar dalam metode
Startup.ConfigureServices
. Panggilan balikbuilder.ConfigureServices
dari aplikasi pengujian dijalankan setelah kode dari aplikasiStartup.ConfigureServices
dijalankan. Urutan eksekusi adalah perubahan signifikan untuk Host Generik dengan rilis ASP.NET Core 3.0. Untuk menggunakan database yang berbeda untuk pengujian daripada database aplikasi, konteks database aplikasi harus diganti dibuilder.ConfigureServices
.Untuk SUT yang masih menggunakan Web Host, panggilan balik aplikasi
builder.ConfigureServices
pengujian dijalankan sebelum kode SUTStartup.ConfigureServices
. Panggilan balik aplikasi ujianbuilder.ConfigureTestServices
dieksekusi setelahnya.Aplikasi sampel menemukan pendeskripsi layanan untuk konteks database dan menggunakan deskriptor untuk menghapus pendaftaran layanan. Selanjutnya, pabrik menambahkan satu
ApplicationDbContext
baru yang menggunakan database memori untuk pengujian.Untuk menyambungkan ke database yang berbeda dari database dalam memori, ubah fungsi
UseInMemoryDatabase
untuk mengaitkan konteks ke database lain. Untuk menggunakan database pengujian SQL Server:- Rujuk paket
Microsoft.EntityFrameworkCore.SqlServer
NuGet dalam file proyek. - Panggil
UseSqlServer
dengan string koneksi ke database.
services.AddDbContext<ApplicationDbContext>((options, context) => { context.UseSqlServer( Configuration.GetConnectionString("TestingDbConnectionString")); });
- Rujuk paket
Gunakan kustom
CustomWebApplicationFactory
dalam kelas pengujian. Contoh berikut menggunakan pabrik di kelasIndexPageTests
:public class IndexPageTests : IClassFixture<CustomWebApplicationFactory<RazorPagesProject.Startup>> { private readonly HttpClient _client; private readonly CustomWebApplicationFactory<RazorPagesProject.Startup> _factory; public IndexPageTests( CustomWebApplicationFactory<RazorPagesProject.Startup> factory) { _factory = factory; _client = factory.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false }); }
Klien aplikasi sampel dikonfigurasi untuk mencegah
HttpClient
pengalihan berikut. Seperti yang dijelaskan kemudian di bagian Mock autentikasi, ini memungkinkan pengujian untuk memeriksa hasil dari respons pertama aplikasi. Respons pertama adalah pengalihan di banyak pengujian ini denganLocation
header.Pengujian umum menggunakan metode dan pembantu
HttpClient
untuk memproses permintaan dan respons:[Fact] public async Task Post_DeleteAllMessagesHandler_ReturnsRedirectToRoot() { // Arrange var defaultPage = await _client.GetAsync("/"); var content = await HtmlHelpers.GetDocumentAsync(defaultPage); //Act var response = await _client.SendAsync( (IHtmlFormElement)content.QuerySelector("form[id='messages']"), (IHtmlButtonElement)content.QuerySelector("button[id='deleteAllBtn']")); // Assert Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode); Assert.Equal(HttpStatusCode.Redirect, response.StatusCode); Assert.Equal("/", response.Headers.Location.OriginalString); }
Setiap permintaan POST ke SUT harus memenuhi pemeriksaan anti-pemalsuan yang secara otomatis dilakukan oleh sistem perlindungan data aplikasi. Untuk mengatur permintaan POST pengujian, aplikasi pengujian harus:
- Buat permintaan untuk halaman tersebut.
- Uraikan antiforgery cookie dan minta token validasi dari respons.
- Lakukan permintaan POST dengan antiforgery cookie dan pastikan token validasi permintaan tersedia.
Metode ekstensi SendAsync
pembantu (Helpers/HttpClientExtensions.cs
) dan metode pembantu GetDocumentAsync
(Helpers/HtmlHelpers.cs
) dalam aplikasi contoh menggunakan pengurai AngleSharp untuk menangani pemeriksaan antiforgery dengan metode berikut:
-
GetDocumentAsync
: Menerima HttpResponseMessage dan mengembalikanIHtmlDocument
.GetDocumentAsync
menggunakan pengaturan yang menyiapkan respons virtual berdasarkanHttpResponseMessage
yang asli. Untuk informasi selengkapnya, lihat dokumentasi AngleSharp. -
SendAsync
metode ekstensi untukHttpClient
menyusun sebuah HttpRequestMessage dan memanggil SendAsync(HttpRequestMessage) untuk mengirimkan permintaan ke SUT. Kelebihan beban untukSendAsync
menerima formulir HTML (IHtmlFormElement
) dan yang berikut:- Tombol Kirim pada formulir (
IHtmlElement
) - Kumpulan nilai formulir (
IEnumerable<KeyValuePair<string, string>>
) - Tombol Kirim (
IHtmlElement
) dan nilai formulir (IEnumerable<KeyValuePair<string, string>>
)
- Tombol Kirim pada formulir (
Catatan
AngleSharp adalah pustaka penguraian pihak ketiga yang digunakan untuk tujuan demonstrasi dalam topik ini dan aplikasi sampel. AngleSharp tidak didukung atau diperlukan untuk pengujian integrasi aplikasi ASP.NET Core. Pengurai lain dapat digunakan, seperti Html Agility Pack (HAP). Pendekatan lain adalah menulis kode untuk menangani token verifikasi permintaan sistem antiforgery dan antiforgery cookie secara langsung.
Catatan
Penyedia database dalam memori EF-Core dapat digunakan untuk pengujian terbatas dan dasar, namun penyedia SQLite adalah pilihan yang direkomendasikan untuk pengujian dalam memori.
Menyesuaikan klien dengan WithWebHostBuilder
Ketika konfigurasi tambahan diperlukan dalam metode pengujian, WithWebHostBuilder membuat WebApplicationFactory
baru dengan IWebHostBuilder yang disesuaikan lebih lanjut oleh konfigurasi.
Metode pengujian Post_DeleteMessageHandler_ReturnsRedirectToRoot
aplikasi sampel menunjukkan penggunaan WithWebHostBuilder
. Pengujian ini melakukan penghapusan rekaman dalam database dengan memicu pengiriman formulir di SUT.
Karena pengujian lain di IndexPageTests
melakukan operasi di kelas yang menghapus semua rekaman dalam database dan mungkin dijalankan sebelum metode Post_DeleteMessageHandler_ReturnsRedirectToRoot
, database diinisialisasi ulang dalam metode pengujian ini untuk memastikan adanya rekaman yang dapat dihapus oleh Sistem yang Diuji (SUT). Memilih tombol pertama hapus dari formulir messages
di SUT disimulasikan dalam permintaan ke SUT.
[Fact]
public async Task Post_DeleteMessageHandler_ReturnsRedirectToRoot()
{
// Arrange
var client = _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureServices(services =>
{
var serviceProvider = services.BuildServiceProvider();
using (var scope = serviceProvider.CreateScope())
{
var scopedServices = scope.ServiceProvider;
var db = scopedServices
.GetRequiredService<ApplicationDbContext>();
var logger = scopedServices
.GetRequiredService<ILogger<IndexPageTests>>();
try
{
Utilities.ReinitializeDbForTests(db);
}
catch (Exception ex)
{
logger.LogError(ex, "An error occurred seeding " +
"the database with test messages. Error: {Message}",
ex.Message);
}
}
});
})
.CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false
});
var defaultPage = await client.GetAsync("/");
var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
//Act
var response = await client.SendAsync(
(IHtmlFormElement)content.QuerySelector("form[id='messages']"),
(IHtmlButtonElement)content.QuerySelector("form[id='messages']")
.QuerySelector("div[class='panel-body']")
.QuerySelector("button"));
// Assert
Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode);
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
Assert.Equal("/", response.Headers.Location.OriginalString);
}
Pengaturan klien
Tabel berikut menunjukkan bawaan WebApplicationFactoryClientOptions yang tersedia saat membuat HttpClient
instance.
Opsi | Deskripsi | Bawaan |
---|---|---|
AllowAutoRedirect | Menentukan atau menetapkan apakah instans HttpClient harus mengikuti respons pengalihan secara otomatis atau tidak. |
true |
BaseAddress | Mendapatkan atau mengatur alamat dasar HttpClient dari instansi. |
http://localhost |
HandleCookies | Mendapatkan atau mengatur apakah instans HttpClient seharusnya menangani cookie. |
true |
MaxAutomaticRedirections | Mendapatkan atau mengatur jumlah maksimum respons pengalihan yang seharusnya diikuti oleh instans HttpClient . |
7 |
WebApplicationFactoryClientOptions
Buat kelas dan teruskan ke CreateClient() metode (nilai default ditampilkan dalam contoh kode):
// Default client option values are shown
var clientOptions = new WebApplicationFactoryClientOptions();
clientOptions.AllowAutoRedirect = true;
clientOptions.BaseAddress = new Uri("http://localhost");
clientOptions.HandleCookies = true;
clientOptions.MaxAutomaticRedirections = 7;
_client = _factory.CreateClient(clientOptions);
Menyuntikkan layanan contoh
Layanan dapat ditimpa dalam pengujian dengan panggilan ke ConfigureTestServices pada pembuat host.
Untuk menyuntikkan layanan tiruan, SUT harus memiliki kelas Startup
dengan metode Startup.ConfigureServices
.
Sampel SUT menyertakan layanan terlingkup yang mengembalikan kuotasi. Kutipan tersebut disematkan di bidang tersembunyi pada halaman Indeks ketika diminta.
Services/IQuoteService.cs
:
public interface IQuoteService
{
Task<string> GenerateQuote();
}
Services/QuoteService.cs
:
// Quote ©1975 BBC: The Doctor (Tom Baker); Dr. Who: Planet of Evil
// https://www.bbc.co.uk/programmes/p00pyrx6
public class QuoteService : IQuoteService
{
public Task<string> GenerateQuote()
{
return Task.FromResult<string>(
"Come on, Sarah. We've an appointment in London, " +
"and we're already 30,000 years late.");
}
}
Startup.cs
:
services.AddScoped<IQuoteService, QuoteService>();
Pages/Index.cshtml.cs
:
public class IndexModel : PageModel
{
private readonly ApplicationDbContext _db;
private readonly IQuoteService _quoteService;
public IndexModel(ApplicationDbContext db, IQuoteService quoteService)
{
_db = db;
_quoteService = quoteService;
}
[BindProperty]
public Message Message { get; set; }
public IList<Message> Messages { get; private set; }
[TempData]
public string MessageAnalysisResult { get; set; }
public string Quote { get; private set; }
public async Task OnGetAsync()
{
Messages = await _db.GetMessagesAsync();
Quote = await _quoteService.GenerateQuote();
}
Pages/Index.cs
:
<input id="quote" type="hidden" value="@Model.Quote">
Markup berikut dihasilkan saat aplikasi SUT dijalankan:
<input id="quote" type="hidden" value="Come on, Sarah. We've an appointment in
London, and we're already 30,000 years late.">
Untuk menguji layanan dan menyuntikkan mock service dalam pengujian integrasi, layanan tiruan disuntikkan ke SUT oleh tes. Layanan tiruan mengganti aplikasi QuoteService
dengan layanan yang disediakan oleh aplikasi pengujian, yang disebut TestQuoteService
:
IntegrationTests.IndexPageTests.cs
:
// Quote ©1975 BBC: The Doctor (Tom Baker); Pyramids of Mars
// https://www.bbc.co.uk/programmes/p00pys55
public class TestQuoteService : IQuoteService
{
public Task<string> GenerateQuote()
{
return Task.FromResult<string>(
"Something's interfering with time, Mr. Scarman, " +
"and time is my business.");
}
}
ConfigureTestServices
dipanggil, dan layanan terlingkup terdaftar:
[Fact]
public async Task Get_QuoteService_ProvidesQuoteInPage()
{
// Arrange
var client = _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.AddScoped<IQuoteService, TestQuoteService>();
});
})
.CreateClient();
//Act
var defaultPage = await client.GetAsync("/");
var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
var quoteElement = content.QuerySelector("#quote");
// Assert
Assert.Equal("Something's interfering with time, Mr. Scarman, " +
"and time is my business.", quoteElement.Attributes["value"].Value);
}
Markup yang dihasilkan selama eksekusi pengujian mencerminkan teks kutipan yang disediakan oleh TestQuoteService
, sehingga pernyataan lolos:
<input id="quote" type="hidden" value="Something's interfering with time,
Mr. Scarman, and time is my business.">
Autentikasi tiruan
Pengujian pada kelas AuthTests
memeriksa bahwa suatu titik akhir aman:
- Mengalihkan pengguna yang tidak diautentikasi ke halaman Masuk aplikasi.
- Mengembalikan konten untuk pengguna yang diautentikasi.
Dalam SUT, /SecurePage
halaman menggunakan AuthorizePage konvensi untuk menerapkan AuthorizeFilter pada halaman. Untuk informasi selengkapnya, lihat Razor Konvensi otorisasi halaman.
services.AddRazorPages(options =>
{
options.Conventions.AuthorizePage("/SecurePage");
});
Dalam Get_SecurePageRedirectsAnUnauthenticatedUser
pengujian, WebApplicationFactoryClientOptions diatur untuk melarang pengalihan dengan menetapkan AllowAutoRedirect ke false
:
[Fact]
public async Task Get_SecurePageRedirectsAnUnauthenticatedUser()
{
// Arrange
var client = _factory.CreateClient(
new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false
});
// Act
var response = await client.GetAsync("/SecurePage");
// Assert
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
Assert.StartsWith("http://localhost/Identity/Account/Login",
response.Headers.Location.OriginalString);
}
Dengan melarang klien untuk mengikuti pengalihan, pemeriksaan berikut dapat dilakukan:
- Kode status yang dikembalikan oleh SUT dapat diperiksa terhadap hasil yang diharapkan HttpStatusCode.Redirect , bukan kode status akhir setelah pengalihan ke halaman Login, yang akan menjadi HttpStatusCode.OK.
- Nilai
Location
header di header respons dicentang untuk mengonfirmasi bahwa header dimulai denganhttp://localhost/Identity/Account/Login
, bukan respons halaman Masuk akhir, di manaLocation
header tidak akan ada.
Aplikasi pengujian dapat meniru AuthenticationHandler<TOptions>ConfigureTestServices untuk menguji aspek autentikasi dan otorisasi. Skenario yang minimal menghasilkan AuthenticateResult.Success.
public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
public TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
: base(options, logger, encoder, clock)
{
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var claims = new[] { new Claim(ClaimTypes.Name, "Test user") };
var identity = new ClaimsIdentity(claims, "Test");
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, "Test");
var result = AuthenticateResult.Success(ticket);
return Task.FromResult(result);
}
}
TestAuthHandler
dipanggil untuk mengautentikasi pengguna ketika skema autentikasi diatur ke Test
di mana AddAuthentication
terdaftar untuk ConfigureTestServices
. Penting agar skema Test
sesuai dengan skema yang diharapkan oleh aplikasi Anda. Jika tidak, autentikasi tidak akan berfungsi.
[Fact]
public async Task Get_SecurePageIsReturnedForAnAuthenticatedUser()
{
// Arrange
var client = _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.AddAuthentication("Test")
.AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(
"Test", options => {});
});
})
.CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false,
});
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Test");
//Act
var response = await client.GetAsync("/SecurePage");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
Untuk informasi selengkapnya tentang WebApplicationFactoryClientOptions
, lihat bagian Opsi klien.
Mengatur lingkungan
Secara default, host SUT dan lingkungan aplikasi dikonfigurasi untuk menggunakan lingkungan Pengembangan. Untuk mengambil alih lingkungan SUT saat menggunakan IHostBuilder
:
- Atur
ASPNETCORE_ENVIRONMENT
variabel lingkungan (misalnya,Staging
, ,Production
atau nilai kustom lainnya, sepertiTesting
). - Ambil alih
CreateHostBuilder
dalam aplikasi pengujian untuk membaca variabel lingkungan yang diawali denganASPNETCORE
.
protected override IHostBuilder CreateHostBuilder() =>
base.CreateHostBuilder()
.ConfigureHostConfiguration(
config => config.AddEnvironmentVariables("ASPNETCORE"));
Jika SUT menggunakan Web Host (IWebHostBuilder
), ambil alih CreateWebHostBuilder
:
protected override IWebHostBuilder CreateWebHostBuilder() =>
base.CreateWebHostBuilder().UseEnvironment("Testing");
Cara infrastruktur pengujian menyimpulkan jalur akar konten aplikasi
WebApplicationFactory
Konstruktor menyimpulkan jalur akar konten aplikasi dengan mencari pada rakitan yang WebApplicationFactoryContentRootAttribute berisi pengujian integrasi dengan kunci yang sama dengan TEntryPoint
rakitan System.Reflection.Assembly.FullName
. Jika atribut dengan kunci yang benar tidak ditemukan, WebApplicationFactory
kembali mencari file solusi (.sln) dan menambahkan nama perakitan TEntryPoint
ke direktori solusi. Direktori akar aplikasi (jalur akar konten) digunakan untuk menemukan tampilan dan file konten.
Menonaktifkan penyalinan bayangan
Penyalinan bayangan menyebabkan pengujian dijalankan di direktori yang berbeda dari direktori output. Jika pengujian Anda mengandalkan pemuatan file yang relatif terhadap Assembly.Location
dan Anda mengalami masalah, Anda mungkin harus menonaktifkan penyalinan bayangan.
Untuk menonaktifkan penyalinan bayangan saat menggunakan xUnit, buat xunit.runner.json
file di direktori proyek pengujian Anda, dengan pengaturan konfigurasi yang benar:
{
"shadowCopy": false
}
Pembuangan objek
Setelah pengujian IClassFixture
implementasi dijalankan, TestServer dan HttpClient dibuang ketika xUnit membuang WebApplicationFactory
. Jika objek yang dibuat oleh pengembang memerlukan pembuangan, buang dalam implementasi IClassFixture
. Untuk informasi selengkapnya, lihat Menerapkan metode Dispose.
Contoh pengujian integrasi
Aplikasi sampel terdiri dari dua aplikasi:
Aplikasi | Direktori proyek | Deskripsi |
---|---|---|
Aplikasi pesan (SUT) | src/RazorPagesProject |
Memungkinkan pengguna untuk menambahkan, menghapus satu, menghapus semua, dan menganalisis pesan. |
Menguji aplikasi | tests/RazorPagesProject.Tests |
Digunakan untuk menguji integrasi SUT. |
Pengujian dapat dijalankan menggunakan fitur pengujian bawaan IDE, seperti Visual Studio. Jika menggunakan Visual Studio Code atau baris perintah, jalankan perintah berikut pada prompt perintah di tests/RazorPagesProject.Tests
direktori:
dotnet test
Organisasi aplikasi pesan (SUT)
SUT adalah Razor sistem pesan Pages dengan karakteristik berikut:
- Halaman Indeks aplikasi (
Pages/Index.cshtml
danPages/Index.cshtml.cs
) menyediakan metode UI dan model halaman untuk mengontrol penambahan, penghapusan, dan analisis pesan (kata rata-rata per pesan). - Pesan dijelaskan oleh
Message
kelas (Data/Message.cs
) dengan dua properti:Id
(kunci) danText
(pesan). PropertiText
diperlukan dan dibatasi hingga 200 karakter. - Pesan disimpan menggunakan database dalam memori Entity Framework†.
- Aplikasi ini berisi lapisan akses data (DAL) di kelas konteks databasenya,
AppDbContext
(Data/AppDbContext.cs
). - Jika database kosong pada startup aplikasi, penyimpanan pesan diinisialisasi dengan tiga pesan.
- Aplikasi ini menyertakan
/SecurePage
yang hanya dapat diakses oleh pengguna yang diautentikasi.
† Topik EF, Uji dengan InMemory, menjelaskan cara menggunakan database dalam memori untuk pengujian dengan MSTest. Topik ini menggunakan kerangka kerja pengujian xUnit . Konsep pengujian dan implementasi pengujian di berbagai kerangka kerja pengujian serupa tetapi tidak identik.
Meskipun aplikasi tidak menggunakan pola repositori dan bukan contoh pola Unit kerja (UoW) yang efektif, Razor Pages mendukung pola pengembangan ini. Untuk informasi selengkapnya, lihat Merancang lapisan persistensi infrastruktur dan Logika pengontrol Pengujian (sampel mengimplementasikan pola repositori).
Menguji organisasi aplikasi
Aplikasi pengujian adalah aplikasi konsol di direktori tests/RazorPagesProject.Tests
.
Menguji direktori aplikasi | Deskripsi |
---|---|
AuthTests |
Berisi metode pengujian untuk:
|
BasicTests |
Berisi metode pengujian untuk perutean dan jenis konten. |
IntegrationTests |
Berisi pengujian integrasi untuk halaman Indeks menggunakan kelas kustom WebApplicationFactory . |
Helpers/Utilities |
|
Kerangka kerja pengujian adalah xUnit. Pengujian integrasi dilakukan menggunakan Microsoft.AspNetCore.TestHost, yang mencakup TestServer. Karena paket Microsoft.AspNetCore.Mvc.Testing
digunakan untuk mengonfigurasi host dan server pengujian, paket TestHost
dan TestServer
tidak memerlukan referensi paket langsung dalam file proyek aplikasi pengujian atau konfigurasi pengembang di aplikasi pengujian.
Pengujian integrasi biasanya memerlukan himpunan data kecil dalam database sebelum eksekusi pengujian. Misalnya, pengujian penghapusan memerlukan penghapusan catatan dalam database, sehingga database harus memiliki setidaknya satu catatan agar penghapusan berhasil dilakukan.
Aplikasi contoh memuat database dengan tiga pesan dalam Utilities.cs
yang dapat digunakan dalam pengujian saat dijalankan.
public static void InitializeDbForTests(ApplicationDbContext db)
{
db.Messages.AddRange(GetSeedingMessages());
db.SaveChanges();
}
public static void ReinitializeDbForTests(ApplicationDbContext db)
{
db.Messages.RemoveRange(db.Messages);
InitializeDbForTests(db);
}
public static List<Message> GetSeedingMessages()
{
return new List<Message>()
{
new Message(){ Text = "TEST RECORD: You're standing on my scarf." },
new Message(){ Text = "TEST RECORD: Would you like a jelly baby?" },
new Message(){ Text = "TEST RECORD: To the rational mind, " +
"nothing is inexplicable; only unexplained." }
};
}
Konteks database SUT terdaftar dalam metode Startup.ConfigureServices
. Panggilan balik builder.ConfigureServices
dari aplikasi pengujian dijalankan setelah kode dari aplikasi Startup.ConfigureServices
dijalankan. Untuk menggunakan database yang berbeda untuk pengujian, konteks database aplikasi harus diganti di builder.ConfigureServices
. Untuk informasi selengkapnya, lihat bagian Kustomisasi WebApplicationFactory .
Untuk SUT yang masih menggunakan Web Host, panggilan balik aplikasi builder.ConfigureServices
pengujian dijalankan sebelum kode SUT Startup.ConfigureServices
. Panggilan balik aplikasi ujian builder.ConfigureTestServices
dieksekusi setelahnya.
Sumber Daya Tambahan:
Artikel ini mengasumsikan pemahaman dasar tentang pengujian unit. Jika tidak terbiasa dengan konsep pengujian, lihat artikel Pengujian Unit di .NET Core dan .NET Standard dan konten tertautnya.
Melihat atau mengunduh kode sampel (cara mengunduh)
Aplikasi sampel adalah Razor aplikasi Pages dan mengasumsikan pengetahuan dasar mengenai Razor Pages. Jika Anda tidak terbiasa dengan Razor Pages, lihat artikel berikut ini:
Untuk menguji SPAs, kami merekomendasikan alat seperti Playwright untuk .NET, yang dapat mengotomatiskan browser.
Pengantar pengujian integrasi
Pengujian integrasi mengevaluasi komponen aplikasi pada tingkat yang lebih luas daripada pengujian unit. Pengujian unit digunakan untuk menguji komponen perangkat lunak terisolasi, seperti metode kelas individual. Pengujian integrasi mengonfirmasi bahwa dua komponen aplikasi atau lebih bekerja sama untuk menghasilkan hasil yang diharapkan, mungkin termasuk setiap komponen yang diperlukan untuk memproses permintaan sepenuhnya.
Pengujian yang lebih luas ini digunakan untuk menguji infrastruktur aplikasi dan seluruh kerangka kerja, sering kali termasuk komponen berikut:
- Basis data
- Sistem file
- Appliance jaringan
- Jalur permintaan-tanggapan
Pengujian unit menggunakan komponen fabrikasi, yang dikenal sebagai objek palsu atau tiruan, sebagai pengganti komponen infrastruktur.
Berbeda dengan pengujian unit, pengujian integrasi:
- Gunakan komponen aktual yang digunakan aplikasi dalam produksi.
- Memerlukan lebih banyak kode dan pemrosesan data.
- Membutuhkan waktu lebih lama untuk berjalan.
Oleh karena itu, batasi penggunaan pengujian integrasi ke skenario infrastruktur yang paling penting. Jika perilaku dapat diuji menggunakan pengujian unit atau pengujian integrasi, pilih pengujian unit.
Dalam diskusi pengujian integrasi, proyek yang diuji sering disebut System Under Test, atau "SUT" singkatnya. "SUT" digunakan di seluruh artikel ini untuk merujuk ke aplikasi ASP.NET Core yang sedang diuji.
Jangan menulis pengujian integrasi untuk setiap permutasi akses data dan file yang melibatkan database dan sistem file. Terlepas dari berapa banyak tempat di seluruh aplikasi berinteraksi dengan database dan sistem file, serangkaian pengujian integrasi baca, tulis, perbarui, dan hapus yang berfokus biasanya mampu menguji database dan komponen sistem file dengan memadai. Gunakan pengujian unit untuk pengujian rutin logika metode yang berinteraksi dengan komponen-komponen ini. Dalam pengujian unit, penggunaan infrastruktur palsu atau tiruan menghasilkan eksekusi pengujian yang lebih cepat.
pengujian integrasi ASP.NET Core
Pengujian integrasi di ASP.NET Core memerlukan hal berikut:
- Proyek pengujian digunakan untuk memuat dan menjalankan pengujian. Proyek pengujian memiliki referensi ke SUT.
- Proyek pengujian membuat host web pengujian untuk SUT dan menggunakan klien server pengujian untuk menangani permintaan dan respons dengan SUT.
- Pelari uji digunakan untuk menjalankan pengujian dan melaporkan hasil pengujian.
Pengujian integrasi mengikuti urutan langkah pengujian yang mencakup tahapan Arrange, Act, dan Assert yang biasa:
- Server web SUT telah dikonfigurasi.
- Klien server pengujian dibuat untuk mengirimkan permintaan ke aplikasi.
- Langkah Atur pengujian dijalankan: Aplikasi pengujian menyiapkan permintaan.
- Langkah pengujian Act dijalankan: Klien mengirimkan permintaan dan menerima respons.
- Langkah pengujian Assert dijalankan: Respons aktual divalidasi sebagai lulus atau gagal berdasarkan respons yang diharapkan .
- Proses berlanjut sampai semua pengujian dijalankan.
- Hasil pengujian dilaporkan.
Biasanya, host web pengujian dikonfigurasi secara berbeda dari host web normal aplikasi untuk eksekusi pengujian. Misalnya, database yang berbeda atau pengaturan aplikasi yang berbeda dapat digunakan untuk pengujian.
Komponen infrastruktur, seperti host web pengujian dan server pengujian dalam memori (TestServer), disediakan atau dikelola oleh paket Microsoft.AspNetCore.Mvc.Testing . Penggunaan paket ini menyederhanakan pembuatan dan eksekusi pengujian.
Paket Microsoft.AspNetCore.Mvc.Testing
menangani tugas-tugas berikut:
- Menyalin file dependensi (
.deps
) dari SUT ke direktori proyekbin
pengujian. - Menetapkan akar konten ke akar proyek SUT agar file statis serta halaman dan tampilan dapat ditemukan saat pengujian berlangsung.
-
Menyediakan kelas WebApplicationFactory untuk mempermudah proses awal pengoperasian SUT dengan
TestServer
.
Dokumentasi tes unit menjelaskan cara menyiapkan proyek pengujian dan pengelola pengujian, bersama dengan instruksi terperinci tentang cara menjalankan tes serta rekomendasi tentang cara memberi nama tes dan kelas tes.
Memisahkan pengujian unit dari pengujian integrasi ke dalam proyek yang berbeda. Memisahkan pengujian:
- Membantu memastikan bahwa komponen pengujian infrastruktur tidak secara tidak sengaja disertakan dalam pengujian unit.
- Memungkinkan kontrol atas set pengujian yang akan dijalankan.
Hampir tidak ada perbedaan antara konfigurasi untuk pengujian Razor aplikasi Pages dan aplikasi MVC. Satu-satunya perbedaan adalah bagaimana tes diberi nama.
Razor Di aplikasi Pages, pengujian titik akhir halaman biasanya dinamai sesuai dengan kelas model halaman (misalnya, IndexPageTests
untuk menguji integrasi komponen untuk halaman Indeks). Dalam aplikasi MVC, pengujian biasanya diatur oleh kelas pengontrol dan dinamai sesuai pengontrol yang mereka uji (misalnya, HomeControllerTests
untuk menguji integrasi komponen untuk Home pengontrol).
Menguji prasyarat aplikasi
Proyek pengujian harus:
- Merujuk ke paket
Microsoft.AspNetCore.Mvc.Testing
. - Tentukan Web SDK dalam file proyek (
<Project Sdk="Microsoft.NET.Sdk.Web">
).
Prasyarat ini dapat dilihat di aplikasi sampel. Periksa file tests/RazorPagesProject.Tests/RazorPagesProject.Tests.csproj
. Aplikasi sampel menggunakan kerangka kerja pengujian xUnit dan pustaka pengurai AngleSharp , sehingga aplikasi sampel juga mereferensikan:
Di aplikasi yang menggunakan xunit.runner.visualstudio
versi 2.4.2 atau yang lebih baru, proyek pengujian harus mereferensikan Microsoft.NET.Test.Sdk
paket.
Entity Framework Core juga digunakan dalam pengujian. Lihat file proyek di GitHub.
Lingkungan SUT
Jika lingkungan SUT tidak diatur, lingkungan akan default ke Pengembangan.
Pengujian dasar dengan WebApplicationFactory default
Ekspos kelas yang ditentukan Program
secara implisit ke proyek pengujian dengan melakukan salah satu hal berikut:
Mengekspos tipe data internal dari aplikasi web ke proyek pengujian. Ini dapat dilakukan dalam file proyek SUT (
.csproj
):<ItemGroup> <InternalsVisibleTo Include="MyTestProject" /> </ItemGroup>
Buat kelas publik
Program
menggunakan deklarasi kelas parsial:var builder = WebApplication.CreateBuilder(args); // ... Configure services, routes, etc. app.Run(); + public partial class Program { }
Aplikasi sampel menggunakan pendekatan kelas parsial
Program
.
WebApplicationFactory<TEntryPoint> digunakan untuk membuat TestServer untuk pengujian integrasi.
TEntryPoint
adalah kelas titik masuk SUT, biasanya Program.cs
.
Kelas uji menerapkan antarmuka fikstur kelas untuk menunjukkan bahwa kelas tersebut berisi pengujian dan menyediakan instans objek yang dapat digunakan bersama di seluruh pengujian dalam kelas.
Kelas pengujian berikut, BasicTests
, menggunakan WebApplicationFactory
untuk bootstrap SUT dan menyediakan HttpClient untuk metode pengujian, Get_EndpointsReturnSuccessAndCorrectContentType
. Metode memverifikasi kode status respons berhasil (200-299) dan Content-Type
header adalah text/html; charset=utf-8
untuk beberapa halaman aplikasi.
CreateClient() membuat instans HttpClient
yang secara otomatis mengikuti pengalihan dan menangani cookie.
public class BasicTests
: IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;
public BasicTests(WebApplicationFactory<Program> factory)
{
_factory = factory;
}
[Theory]
[InlineData("/")]
[InlineData("/Index")]
[InlineData("/About")]
[InlineData("/Privacy")]
[InlineData("/Contact")]
public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
{
// Arrange
var client = _factory.CreateClient();
// Act
var response = await client.GetAsync(url);
// Assert
response.EnsureSuccessStatusCode(); // Status Code 200-299
Assert.Equal("text/html; charset=utf-8",
response.Content.Headers.ContentType.ToString());
}
}
Secara default, cookie yang tidak penting tidak dipertahankan di seluruh permintaan saat kebijakan persetujuan Peraturan Perlindungan Data Umum diaktifkan. Untuk mempertahankan cookie yang tidak penting, seperti yang digunakan oleh penyedia TempData, tandai sebagai esensial dalam pengujian Anda. Untuk petunjuk tentang menandai cookie sebagai penting, lihat Cookie penting.
AngleSharp vs Application Parts
untuk pemeriksaan antipemalsuan
Artikel ini menggunakan AngleSharp parser untuk menangani pemeriksaan antiforgery, dengan cara memuat halaman dan mengurai HTML. Untuk menguji titik akhir dari pengontrol dan tampilan Halaman pada tingkat yang lebih rendah tanpa mempedulikan bagaimana tampilan tersebut dirender di browser, pertimbangkan untuk menggunakan Razor. Pendekatan Bagian Aplikasi menyuntikkan pengontrol atau Razor Halaman yang dapat digunakan untuk membuat permintaan JSON ke dalam aplikasi untuk mendapatkan nilai yang diperlukan. Untuk informasi selengkapnya, lihat blog Pengujian Integrasi Sumber Daya ASP.NET Core yang Dilindungi dengan Antiforgery Menggunakan Bagian Aplikasi dan repo GitHub terkait oleh Martin Costello.
Menyesuaikan WebApplicationFactory
Konfigurasi host web dapat dibuat secara independen dari kelas pengujian dengan mewarisi dari WebApplicationFactory<TEntryPoint> untuk membuat satu atau beberapa pabrik kustom:
Warisi dari
WebApplicationFactory
dan ambil alih ConfigureWebHost. IWebHostBuilder memungkinkan konfigurasi kumpulan layanan denganIWebHostBuilder.ConfigureServices
public class CustomWebApplicationFactory<TProgram> : WebApplicationFactory<TProgram> where TProgram : class { protected override void ConfigureWebHost(IWebHostBuilder builder) { builder.ConfigureServices(services => { var dbContextDescriptor = services.SingleOrDefault( d => d.ServiceType == typeof(DbContextOptions<ApplicationDbContext>)); services.Remove(dbContextDescriptor); var dbConnectionDescriptor = services.SingleOrDefault( d => d.ServiceType == typeof(DbConnection)); services.Remove(dbConnectionDescriptor); // Create open SqliteConnection so EF won't automatically close it. services.AddSingleton<DbConnection>(container => { var connection = new SqliteConnection("DataSource=:memory:"); connection.Open(); return connection; }); services.AddDbContext<ApplicationDbContext>((container, options) => { var connection = container.GetRequiredService<DbConnection>(); options.UseSqlite(connection); }); }); builder.UseEnvironment("Development"); } }
Penyemaian database di aplikasi sampel dilakukan dengan
InitializeDbForTests
metode . Metode ini dijelaskan di bagian Sampel pengujian integrasi: Menguji organisasi aplikasi.Konteks database SUT terdaftar di
Program.cs
. Panggilan balikbuilder.ConfigureServices
dari aplikasi pengujian dijalankan setelah kode dari aplikasiProgram.cs
dijalankan. Untuk menggunakan database yang berbeda untuk pengujian daripada database aplikasi, konteks database aplikasi harus diganti dibuilder.ConfigureServices
.Aplikasi sampel menemukan pendeskripsi layanan untuk konteks database dan menggunakan deskriptor untuk menghapus pendaftaran layanan. Pabrik kemudian menambahkan sebuah
ApplicationDbContext
baru yang menggunakan database berbasis memori untuk pengujian.Untuk menyambungkan ke database lain, ubah
DbConnection
. Untuk menggunakan database pengujian SQL Server:
- Rujuk paket
Microsoft.EntityFrameworkCore.SqlServer
NuGet dalam file proyek. - Panggil
UseInMemoryDatabase
.
Gunakan kustom
CustomWebApplicationFactory
dalam kelas pengujian. Contoh berikut menggunakan pabrik di kelasIndexPageTests
:public class IndexPageTests : IClassFixture<CustomWebApplicationFactory<Program>> { private readonly HttpClient _client; private readonly CustomWebApplicationFactory<Program> _factory; public IndexPageTests( CustomWebApplicationFactory<Program> factory) { _factory = factory; _client = factory.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false }); }
Klien aplikasi sampel dikonfigurasi untuk mencegah
HttpClient
pengalihan berikut. Seperti yang dijelaskan kemudian di bagian Mock autentikasi, ini memungkinkan pengujian untuk memeriksa hasil dari respons pertama aplikasi. Respons pertama adalah pengalihan di banyak pengujian ini denganLocation
header.Pengujian umum menggunakan metode dan pembantu
HttpClient
untuk memproses permintaan dan respons:[Fact] public async Task Post_DeleteAllMessagesHandler_ReturnsRedirectToRoot() { // Arrange var defaultPage = await _client.GetAsync("/"); var content = await HtmlHelpers.GetDocumentAsync(defaultPage); //Act var response = await _client.SendAsync( (IHtmlFormElement)content.QuerySelector("form[id='messages']"), (IHtmlButtonElement)content.QuerySelector("button[id='deleteAllBtn']")); // Assert Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode); Assert.Equal(HttpStatusCode.Redirect, response.StatusCode); Assert.Equal("/", response.Headers.Location.OriginalString); }
Setiap permintaan POST ke SUT harus memenuhi pemeriksaan anti-pemalsuan yang secara otomatis dilakukan oleh sistem perlindungan data aplikasi. Untuk mengatur permintaan POST pengujian, aplikasi pengujian harus:
- Buat permintaan untuk halaman tersebut.
- Uraikan antiforgery cookie dan minta token validasi dari respons.
- Lakukan permintaan POST dengan antiforgery cookie dan pastikan token validasi permintaan tersedia.
Metode ekstensi SendAsync
pembantu (Helpers/HttpClientExtensions.cs
) dan metode pembantu GetDocumentAsync
(Helpers/HtmlHelpers.cs
) dalam aplikasi contoh menggunakan pengurai AngleSharp untuk menangani pemeriksaan antiforgery dengan metode berikut:
-
GetDocumentAsync
: Menerima HttpResponseMessage dan mengembalikanIHtmlDocument
.GetDocumentAsync
menggunakan pengaturan yang menyiapkan respons virtual berdasarkanHttpResponseMessage
yang asli. Untuk informasi selengkapnya, lihat dokumentasi AngleSharp. -
SendAsync
metode ekstensi untukHttpClient
menyusun sebuah HttpRequestMessage dan memanggil SendAsync(HttpRequestMessage) untuk mengirimkan permintaan ke SUT. Kelebihan beban untukSendAsync
menerima formulir HTML (IHtmlFormElement
) dan yang berikut:- Tombol Kirim pada formulir (
IHtmlElement
) - Kumpulan nilai formulir (
IEnumerable<KeyValuePair<string, string>>
) - Tombol Kirim (
IHtmlElement
) dan nilai formulir (IEnumerable<KeyValuePair<string, string>>
)
- Tombol Kirim pada formulir (
AngleSharp adalah pustaka penguraian pihak ketiga yang digunakan untuk tujuan demonstrasi dalam artikel ini dan aplikasi sampel. AngleSharp tidak didukung atau diperlukan untuk pengujian integrasi aplikasi ASP.NET Core. Pengurai lain dapat digunakan, seperti Html Agility Pack (HAP). Pendekatan lain adalah menulis kode untuk menangani token verifikasi permintaan sistem antiforgery dan antiforgery cookie secara langsung. Lihat AngleSharp vs Application Parts
untuk pemeriksaan antiforgery di artikel ini untuk informasi selengkapnya.
Penyedia database dalam memori EF-Core dapat digunakan untuk pengujian terbatas dan dasar, namun penyedia SQLite adalah pilihan yang direkomendasikan untuk pengujian dalam memori.
Lihat Perluas Startup dengan filter startup yang menunjukkan cara mengonfigurasi middleware menggunakan IStartupFilter, yang berguna ketika pengujian membutuhkan layanan atau middleware kustom.
Menyesuaikan klien dengan WithWebHostBuilder
Ketika konfigurasi tambahan diperlukan dalam metode pengujian, WithWebHostBuilder membuat WebApplicationFactory
baru dengan IWebHostBuilder yang disesuaikan lebih lanjut oleh konfigurasi.
Kode sampel memanggil WithWebHostBuilder
untuk menggantikan layanan yang telah dikonfigurasi dengan stub pengujian. Untuk informasi selengkapnya dan contoh penggunaan, lihat Inject mock services di artikel ini.
Metode pengujian Post_DeleteMessageHandler_ReturnsRedirectToRoot
aplikasi sampel menunjukkan penggunaan WithWebHostBuilder
. Pengujian ini melakukan penghapusan rekaman dalam database dengan memicu pengiriman formulir di SUT.
Karena pengujian lain di IndexPageTests
melakukan operasi di kelas yang menghapus semua rekaman dalam database dan mungkin dijalankan sebelum metode Post_DeleteMessageHandler_ReturnsRedirectToRoot
, database diinisialisasi ulang dalam metode pengujian ini untuk memastikan adanya rekaman yang dapat dihapus oleh Sistem yang Diuji (SUT). Memilih tombol pertama hapus dari formulir messages
di SUT disimulasikan dalam permintaan ke SUT.
[Fact]
public async Task Post_DeleteMessageHandler_ReturnsRedirectToRoot()
{
// Arrange
using (var scope = _factory.Services.CreateScope())
{
var scopedServices = scope.ServiceProvider;
var db = scopedServices.GetRequiredService<ApplicationDbContext>();
Utilities.ReinitializeDbForTests(db);
}
var defaultPage = await _client.GetAsync("/");
var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
//Act
var response = await _client.SendAsync(
(IHtmlFormElement)content.QuerySelector("form[id='messages']"),
(IHtmlButtonElement)content.QuerySelector("form[id='messages']")
.QuerySelector("div[class='panel-body']")
.QuerySelector("button"));
// Assert
Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode);
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
Assert.Equal("/", response.Headers.Location.OriginalString);
}
Pengaturan klien
Lihat halaman WebApplicationFactoryClientOptions untuk default dan opsi yang tersedia saat membuat instance HttpClient
.
Buat kelas WebApplicationFactoryClientOptions
dan passing ke metode CreateClient().
public class IndexPageTests :
IClassFixture<CustomWebApplicationFactory<Program>>
{
private readonly HttpClient _client;
private readonly CustomWebApplicationFactory<Program>
_factory;
public IndexPageTests(
CustomWebApplicationFactory<Program> factory)
{
_factory = factory;
_client = factory.CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false
});
}
CATATAN: Untuk menghindari peringatan pengalihan HTTPS dalam log saat menggunakan Middleware Pengalihan HTTPS, atur BaseAddress = new Uri("https://localhost")
Menyuntikkan layanan contoh
Layanan dapat ditimpa dalam pengujian dengan panggilan ke ConfigureTestServices pada pembuat host. Untuk membatasi layanan yang ditimpa khusus untuk pengujian itu sendiri, metode WithWebHostBuilder ini digunakan untuk mendapatkan pembangun host. Ini dapat dilihat dalam pengujian berikut:
- Dapatkan_LayananKutipan_MenyediakanKutipanDiHalaman
- Get_GithubProfilePageCanGetAGithubUser
- Dapatkan_HalamanAmanDikembalikanUntukPenggunaTerautentikasi
Sampel SUT menyertakan layanan terlingkup yang mengembalikan kuotasi. Kutipan tersebut disematkan di bidang tersembunyi pada halaman Indeks ketika diminta.
Services/IQuoteService.cs
:
public interface IQuoteService
{
Task<string> GenerateQuote();
}
Services/QuoteService.cs
:
// Quote ©1975 BBC: The Doctor (Tom Baker); Dr. Who: Planet of Evil
// https://www.bbc.co.uk/programmes/p00pyrx6
public class QuoteService : IQuoteService
{
public Task<string> GenerateQuote()
{
return Task.FromResult<string>(
"Come on, Sarah. We've an appointment in London, " +
"and we're already 30,000 years late.");
}
}
Program.cs
:
services.AddScoped<IQuoteService, QuoteService>();
Pages/Index.cshtml.cs
:
public class IndexModel : PageModel
{
private readonly ApplicationDbContext _db;
private readonly IQuoteService _quoteService;
public IndexModel(ApplicationDbContext db, IQuoteService quoteService)
{
_db = db;
_quoteService = quoteService;
}
[BindProperty]
public Message Message { get; set; }
public IList<Message> Messages { get; private set; }
[TempData]
public string MessageAnalysisResult { get; set; }
public string Quote { get; private set; }
public async Task OnGetAsync()
{
Messages = await _db.GetMessagesAsync();
Quote = await _quoteService.GenerateQuote();
}
Pages/Index.cs
:
<input id="quote" type="hidden" value="@Model.Quote">
Markup berikut dihasilkan saat aplikasi SUT dijalankan:
<input id="quote" type="hidden" value="Come on, Sarah. We've an appointment in
London, and we're already 30,000 years late.">
Untuk menguji layanan dan menyuntikkan mock service dalam pengujian integrasi, layanan tiruan disuntikkan ke SUT oleh tes. Layanan tiruan mengganti aplikasi QuoteService
dengan layanan yang disediakan oleh aplikasi pengujian, yang disebut TestQuoteService
:
IntegrationTests.IndexPageTests.cs
:
// Quote ©1975 BBC: The Doctor (Tom Baker); Pyramids of Mars
// https://www.bbc.co.uk/programmes/p00pys55
public class TestQuoteService : IQuoteService
{
public Task<string> GenerateQuote()
{
return Task.FromResult(
"Something's interfering with time, Mr. Scarman, " +
"and time is my business.");
}
}
ConfigureTestServices
dipanggil, dan layanan terlingkup terdaftar:
[Fact]
public async Task Get_QuoteService_ProvidesQuoteInPage()
{
// Arrange
var client = _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.AddScoped<IQuoteService, TestQuoteService>();
});
})
.CreateClient();
//Act
var defaultPage = await client.GetAsync("/");
var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
var quoteElement = content.QuerySelector("#quote");
// Assert
Assert.Equal("Something's interfering with time, Mr. Scarman, " +
"and time is my business.", quoteElement.Attributes["value"].Value);
}
Markup yang dihasilkan selama eksekusi pengujian mencerminkan teks kutipan yang disediakan oleh TestQuoteService
, sehingga pernyataan lolos:
<input id="quote" type="hidden" value="Something's interfering with time,
Mr. Scarman, and time is my business.">
Autentikasi tiruan
Pengujian pada kelas AuthTests
memeriksa bahwa suatu titik akhir aman:
- Mengalihkan pengguna yang tidak diautentikasi ke halaman masuk aplikasi.
- Mengembalikan konten untuk pengguna yang diautentikasi.
Dalam SUT, /SecurePage
halaman menggunakan AuthorizePage konvensi untuk menerapkan AuthorizeFilter pada halaman. Untuk informasi selengkapnya, lihat Razor Konvensi otorisasi halaman.
services.AddRazorPages(options =>
{
options.Conventions.AuthorizePage("/SecurePage");
});
Dalam Get_SecurePageRedirectsAnUnauthenticatedUser
pengujian, WebApplicationFactoryClientOptions diatur untuk melarang pengalihan dengan menetapkan AllowAutoRedirect ke false
:
[Fact]
public async Task Get_SecurePageRedirectsAnUnauthenticatedUser()
{
// Arrange
var client = _factory.CreateClient(
new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false
});
// Act
var response = await client.GetAsync("/SecurePage");
// Assert
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
Assert.StartsWith("http://localhost/Identity/Account/Login",
response.Headers.Location.OriginalString);
}
Dengan melarang klien untuk mengikuti pengalihan, pemeriksaan berikut dapat dilakukan:
- Kode status yang dikembalikan oleh SUT dapat diperiksa terhadap hasil yang diharapkan HttpStatusCode.Redirect , bukan kode status akhir setelah pengalihan ke halaman masuk, yang akan menjadi HttpStatusCode.OK.
- Nilai header
Location
di header respons diperiksa untuk mengonfirmasi bahwa nilai tersebut dimulai denganhttp://localhost/Identity/Account/Login
, bukan respons halaman akhir masuk, di manaLocation
header tidak hadir.
Aplikasi pengujian dapat meniru AuthenticationHandler<TOptions>ConfigureTestServices untuk menguji aspek autentikasi dan otorisasi. Skenario yang minimal menghasilkan AuthenticateResult.Success.
public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
public TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
: base(options, logger, encoder, clock)
{
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var claims = new[] { new Claim(ClaimTypes.Name, "Test user") };
var identity = new ClaimsIdentity(claims, "Test");
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, "TestScheme");
var result = AuthenticateResult.Success(ticket);
return Task.FromResult(result);
}
}
TestAuthHandler
dipanggil untuk mengautentikasi pengguna ketika skema autentikasi diatur ke TestScheme
di mana AddAuthentication
terdaftar untuk ConfigureTestServices
. Penting agar skema TestScheme
sesuai dengan skema yang diharapkan oleh aplikasi Anda. Jika tidak, autentikasi tidak akan berfungsi.
[Fact]
public async Task Get_SecurePageIsReturnedForAnAuthenticatedUser()
{
// Arrange
var client = _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.AddAuthentication(defaultScheme: "TestScheme")
.AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(
"TestScheme", options => { });
});
})
.CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false,
});
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue(scheme: "TestScheme");
//Act
var response = await client.GetAsync("/SecurePage");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
Untuk informasi selengkapnya tentang WebApplicationFactoryClientOptions
, lihat bagian Opsi klien.
Pengujian dasar untuk middleware autentikasi
Lihat repositori GitHub ini untuk pengujian dasar middleware autentikasi. Ini berisi server pengujian yang khusus untuk skenario pengujian.
Mengatur lingkungan
Atur lingkungan di pabrik aplikasi kustom:
public class CustomWebApplicationFactory<TProgram>
: WebApplicationFactory<TProgram> where TProgram : class
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
var dbContextDescriptor = services.SingleOrDefault(
d => d.ServiceType ==
typeof(DbContextOptions<ApplicationDbContext>));
services.Remove(dbContextDescriptor);
var dbConnectionDescriptor = services.SingleOrDefault(
d => d.ServiceType ==
typeof(DbConnection));
services.Remove(dbConnectionDescriptor);
// Create open SqliteConnection so EF won't automatically close it.
services.AddSingleton<DbConnection>(container =>
{
var connection = new SqliteConnection("DataSource=:memory:");
connection.Open();
return connection;
});
services.AddDbContext<ApplicationDbContext>((container, options) =>
{
var connection = container.GetRequiredService<DbConnection>();
options.UseSqlite(connection);
});
});
builder.UseEnvironment("Development");
}
}
Cara infrastruktur pengujian menyimpulkan jalur akar konten aplikasi
WebApplicationFactory
Konstruktor menyimpulkan jalur akar konten aplikasi dengan mencari pada rakitan yang WebApplicationFactoryContentRootAttribute berisi pengujian integrasi dengan kunci yang sama dengan TEntryPoint
rakitan System.Reflection.Assembly.FullName
. Jika atribut dengan kunci yang benar tidak ditemukan, WebApplicationFactory
kembali mencari file solusi (.sln) dan menambahkan nama perakitan TEntryPoint
ke direktori solusi. Direktori akar aplikasi (jalur akar konten) digunakan untuk menemukan tampilan dan file konten.
Menonaktifkan penyalinan bayangan
Penyalinan bayangan menyebabkan pengujian dijalankan di direktori yang berbeda dari direktori output. Jika pengujian Anda mengandalkan pemuatan file yang relatif terhadap Assembly.Location
dan Anda mengalami masalah, Anda mungkin harus menonaktifkan penyalinan bayangan.
Untuk menonaktifkan penyalinan bayangan saat menggunakan xUnit, buat xunit.runner.json
file di direktori proyek pengujian Anda, dengan pengaturan konfigurasi yang benar:
{
"shadowCopy": false
}
Pembuangan objek
Setelah pengujian IClassFixture
implementasi dijalankan, TestServer dan HttpClient dibuang ketika xUnit membuang WebApplicationFactory
. Jika objek yang dibuat oleh pengembang memerlukan pembuangan, buang dalam implementasi IClassFixture
. Untuk informasi selengkapnya, lihat Menerapkan metode Dispose.
Contoh pengujian integrasi
Aplikasi sampel terdiri dari dua aplikasi:
Aplikasi | Direktori proyek | Deskripsi |
---|---|---|
Aplikasi pesan (SUT) | src/RazorPagesProject |
Memungkinkan pengguna untuk menambahkan, menghapus satu, menghapus semua, dan menganalisis pesan. |
Menguji aplikasi | tests/RazorPagesProject.Tests |
Digunakan untuk menguji integrasi SUT. |
Pengujian dapat dijalankan menggunakan fitur pengujian bawaan IDE, seperti Visual Studio. Jika menggunakan Visual Studio Code atau baris perintah, jalankan perintah berikut pada prompt perintah di tests/RazorPagesProject.Tests
direktori:
dotnet test
Organisasi aplikasi pesan (SUT)
SUT adalah Razor sistem pesan Pages dengan karakteristik berikut:
- Halaman Indeks aplikasi (
Pages/Index.cshtml
danPages/Index.cshtml.cs
) menyediakan metode UI dan model halaman untuk mengontrol penambahan, penghapusan, dan analisis pesan (kata rata-rata per pesan). - Pesan dijelaskan oleh
Message
kelas (Data/Message.cs
) dengan dua properti:Id
(kunci) danText
(pesan). PropertiText
diperlukan dan dibatasi hingga 200 karakter. - Pesan disimpan menggunakan database dalam memori Entity Framework†.
- Aplikasi ini berisi lapisan akses data (DAL) di kelas konteks databasenya,
AppDbContext
(Data/AppDbContext.cs
). - Jika database kosong pada startup aplikasi, penyimpanan pesan diinisialisasi dengan tiga pesan.
- Aplikasi ini menyertakan
/SecurePage
yang hanya dapat diakses oleh pengguna yang diautentikasi.
† Artikel EF, Uji dengan InMemory, menjelaskan cara menggunakan database dalam memori untuk pengujian dengan MSTest. Topik ini menggunakan kerangka kerja pengujian xUnit . Konsep pengujian dan implementasi pengujian di berbagai kerangka kerja pengujian serupa tetapi tidak identik.
Meskipun aplikasi tidak menggunakan pola repositori dan bukan contoh pola Unit kerja (UoW) yang efektif, Razor Pages mendukung pola pengembangan ini. Untuk informasi selengkapnya, lihat Merancang lapisan persistensi infrastruktur dan Logika pengontrol Pengujian (sampel mengimplementasikan pola repositori).
Menguji organisasi aplikasi
Aplikasi pengujian adalah aplikasi konsol di direktori tests/RazorPagesProject.Tests
.
Menguji direktori aplikasi | Deskripsi |
---|---|
AuthTests |
Berisi metode pengujian untuk:
|
BasicTests |
Berisi metode pengujian untuk perutean dan jenis konten. |
IntegrationTests |
Berisi pengujian integrasi untuk halaman Indeks menggunakan kelas kustom WebApplicationFactory . |
Helpers/Utilities |
|
Kerangka kerja pengujian adalah xUnit. Pengujian integrasi dilakukan menggunakan Microsoft.AspNetCore.TestHost, yang mencakup TestServer. Karena paket Microsoft.AspNetCore.Mvc.Testing
digunakan untuk mengonfigurasi host dan server pengujian, paket TestHost
dan TestServer
tidak memerlukan referensi paket langsung dalam file proyek aplikasi pengujian atau konfigurasi pengembang di aplikasi pengujian.
Pengujian integrasi biasanya memerlukan himpunan data kecil dalam database sebelum eksekusi pengujian. Misalnya, pengujian penghapusan memerlukan penghapusan catatan dalam database, sehingga database harus memiliki setidaknya satu catatan agar penghapusan berhasil dilakukan.
Aplikasi contoh memuat database dengan tiga pesan dalam Utilities.cs
yang dapat digunakan dalam pengujian saat dijalankan.
public static void InitializeDbForTests(ApplicationDbContext db)
{
db.Messages.AddRange(GetSeedingMessages());
db.SaveChanges();
}
public static void ReinitializeDbForTests(ApplicationDbContext db)
{
db.Messages.RemoveRange(db.Messages);
InitializeDbForTests(db);
}
public static List<Message> GetSeedingMessages()
{
return new List<Message>()
{
new Message(){ Text = "TEST RECORD: You're standing on my scarf." },
new Message(){ Text = "TEST RECORD: Would you like a jelly baby?" },
new Message(){ Text = "TEST RECORD: To the rational mind, " +
"nothing is inexplicable; only unexplained." }
};
}
Konteks database SUT terdaftar di Program.cs
. Panggilan balik builder.ConfigureServices
dari aplikasi pengujian dijalankan setelah kode dari aplikasi Program.cs
dijalankan. Untuk menggunakan database yang berbeda untuk pengujian, konteks database aplikasi harus diganti di builder.ConfigureServices
. Untuk informasi selengkapnya, lihat bagian Kustomisasi WebApplicationFactory .
Sumber Daya Tambahan:
Artikel ini mengasumsikan pemahaman dasar tentang pengujian unit. Jika tidak terbiasa dengan konsep pengujian, lihat artikel Pengujian Unit di .NET Core dan .NET Standard dan konten tertautnya.
Melihat atau mengunduh kode sampel (cara mengunduh)
Aplikasi sampel adalah Razor aplikasi Pages dan mengasumsikan pengetahuan dasar mengenai Razor Pages. Jika Anda tidak terbiasa dengan Razor Pages, lihat artikel berikut ini:
Untuk menguji SPAs, kami merekomendasikan alat seperti Playwright untuk .NET, yang dapat mengotomatiskan browser.
Pengantar pengujian integrasi
Pengujian integrasi mengevaluasi komponen aplikasi pada tingkat yang lebih luas daripada pengujian unit. Pengujian unit digunakan untuk menguji komponen perangkat lunak terisolasi, seperti metode kelas individual. Pengujian integrasi mengonfirmasi bahwa dua komponen aplikasi atau lebih bekerja sama untuk menghasilkan hasil yang diharapkan, mungkin termasuk setiap komponen yang diperlukan untuk memproses permintaan sepenuhnya.
Pengujian yang lebih luas ini digunakan untuk menguji infrastruktur aplikasi dan seluruh kerangka kerja, sering kali termasuk komponen berikut:
- Basis data
- Sistem file
- Appliance jaringan
- Jalur permintaan-tanggapan
Pengujian unit menggunakan komponen fabrikasi, yang dikenal sebagai objek palsu atau tiruan, sebagai pengganti komponen infrastruktur.
Berbeda dengan pengujian unit, pengujian integrasi:
- Gunakan komponen aktual yang digunakan aplikasi dalam produksi.
- Memerlukan lebih banyak kode dan pemrosesan data.
- Membutuhkan waktu lebih lama untuk berjalan.
Oleh karena itu, batasi penggunaan pengujian integrasi ke skenario infrastruktur yang paling penting. Jika perilaku dapat diuji menggunakan pengujian unit atau pengujian integrasi, pilih pengujian unit.
Dalam diskusi pengujian integrasi, proyek yang diuji sering disebut System Under Test, atau "SUT" singkatnya. "SUT" digunakan di seluruh artikel ini untuk merujuk ke aplikasi ASP.NET Core yang sedang diuji.
Jangan menulis pengujian integrasi untuk setiap permutasi akses data dan file yang melibatkan database dan sistem file. Terlepas dari berapa banyak tempat di seluruh aplikasi berinteraksi dengan database dan sistem file, serangkaian pengujian integrasi baca, tulis, perbarui, dan hapus yang berfokus biasanya mampu menguji database dan komponen sistem file dengan memadai. Gunakan pengujian unit untuk pengujian rutin logika metode yang berinteraksi dengan komponen-komponen ini. Dalam pengujian unit, penggunaan infrastruktur palsu atau tiruan menghasilkan eksekusi pengujian yang lebih cepat.
pengujian integrasi ASP.NET Core
Pengujian integrasi di ASP.NET Core memerlukan hal berikut:
- Proyek pengujian digunakan untuk memuat dan menjalankan pengujian. Proyek pengujian memiliki referensi ke SUT.
- Proyek pengujian membuat host web pengujian untuk SUT dan menggunakan klien server pengujian untuk menangani permintaan dan respons dengan SUT.
- Pelari uji digunakan untuk menjalankan pengujian dan melaporkan hasil pengujian.
Pengujian integrasi mengikuti urutan langkah pengujian yang mencakup tahapan Arrange, Act, dan Assert yang biasa:
- Server web SUT telah dikonfigurasi.
- Klien server pengujian dibuat untuk mengirimkan permintaan ke aplikasi.
- Langkah Atur pengujian dijalankan: Aplikasi pengujian menyiapkan permintaan.
- Langkah pengujian Act dijalankan: Klien mengirimkan permintaan dan menerima respons.
- Langkah pengujian Assert dijalankan: Respons aktual divalidasi sebagai lulus atau gagal berdasarkan respons yang diharapkan .
- Proses berlanjut sampai semua pengujian dijalankan.
- Hasil pengujian dilaporkan.
Biasanya, host web pengujian dikonfigurasi secara berbeda dari host web normal aplikasi untuk eksekusi pengujian. Misalnya, database yang berbeda atau pengaturan aplikasi yang berbeda dapat digunakan untuk pengujian.
Komponen infrastruktur, seperti host web pengujian dan server pengujian dalam memori (TestServer), disediakan atau dikelola oleh paket Microsoft.AspNetCore.Mvc.Testing . Penggunaan paket ini menyederhanakan pembuatan dan eksekusi pengujian.
Paket Microsoft.AspNetCore.Mvc.Testing
menangani tugas-tugas berikut:
- Menyalin file dependensi (
.deps
) dari SUT ke direktori proyekbin
pengujian. - Menetapkan akar konten ke akar proyek SUT agar file statis serta halaman dan tampilan dapat ditemukan saat pengujian berlangsung.
-
Menyediakan kelas WebApplicationFactory untuk mempermudah proses awal pengoperasian SUT dengan
TestServer
.
Dokumentasi tes unit menjelaskan cara menyiapkan proyek pengujian dan pengelola pengujian, bersama dengan instruksi terperinci tentang cara menjalankan tes serta rekomendasi tentang cara memberi nama tes dan kelas tes.
Memisahkan pengujian unit dari pengujian integrasi ke dalam proyek yang berbeda. Memisahkan pengujian:
- Membantu memastikan bahwa komponen pengujian infrastruktur tidak secara tidak sengaja disertakan dalam pengujian unit.
- Memungkinkan kontrol atas set pengujian yang akan dijalankan.
Hampir tidak ada perbedaan antara konfigurasi untuk pengujian Razor aplikasi Pages dan aplikasi MVC. Satu-satunya perbedaan adalah bagaimana tes diberi nama.
Razor Di aplikasi Pages, pengujian titik akhir halaman biasanya dinamai sesuai dengan kelas model halaman (misalnya, IndexPageTests
untuk menguji integrasi komponen untuk halaman Indeks). Dalam aplikasi MVC, pengujian biasanya diatur oleh kelas pengontrol dan dinamai sesuai pengontrol yang mereka uji (misalnya, HomeControllerTests
untuk menguji integrasi komponen untuk Home pengontrol).
Menguji prasyarat aplikasi
Proyek pengujian harus:
- Merujuk ke paket
Microsoft.AspNetCore.Mvc.Testing
. - Tentukan Web SDK dalam file proyek (
<Project Sdk="Microsoft.NET.Sdk.Web">
).
Prasyarat ini dapat dilihat di aplikasi sampel. Periksa file tests/RazorPagesProject.Tests/RazorPagesProject.Tests.csproj
. Aplikasi sampel menggunakan kerangka kerja pengujian xUnit dan pustaka pengurai AngleSharp , sehingga aplikasi sampel juga mereferensikan:
Di aplikasi yang menggunakan xunit.runner.visualstudio
versi 2.4.2 atau yang lebih baru, proyek pengujian harus mereferensikan Microsoft.NET.Test.Sdk
paket.
Entity Framework Core juga digunakan dalam pengujian. Lihat file proyek di GitHub.
Lingkungan SUT
Jika lingkungan SUT tidak diatur, lingkungan akan default ke Pengembangan.
Pengujian dasar dengan WebApplicationFactory default
Ekspos kelas yang ditentukan Program
secara implisit ke proyek pengujian dengan melakukan salah satu hal berikut:
Mengekspos tipe data internal dari aplikasi web ke proyek pengujian. Ini dapat dilakukan dalam file proyek SUT (
.csproj
):<ItemGroup> <InternalsVisibleTo Include="MyTestProject" /> </ItemGroup>
Buat kelas publik
Program
menggunakan deklarasi kelas parsial:var builder = WebApplication.CreateBuilder(args); // ... Configure services, routes, etc. app.Run(); + public partial class Program { }
Aplikasi sampel menggunakan pendekatan kelas parsial
Program
.
WebApplicationFactory<TEntryPoint> digunakan untuk membuat TestServer untuk pengujian integrasi.
TEntryPoint
adalah kelas titik masuk SUT, biasanya Program.cs
.
Kelas uji menerapkan antarmuka fikstur kelas untuk menunjukkan bahwa kelas tersebut berisi pengujian dan menyediakan instans objek yang dapat digunakan bersama di seluruh pengujian dalam kelas.
Kelas pengujian berikut, BasicTests
, menggunakan WebApplicationFactory
untuk bootstrap SUT dan menyediakan HttpClient untuk metode pengujian, Get_EndpointsReturnSuccessAndCorrectContentType
. Metode memverifikasi kode status respons berhasil (200-299) dan Content-Type
header adalah text/html; charset=utf-8
untuk beberapa halaman aplikasi.
CreateClient() membuat instans HttpClient
yang secara otomatis mengikuti pengalihan dan menangani cookie.
public class BasicTests
: IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;
public BasicTests(WebApplicationFactory<Program> factory)
{
_factory = factory;
}
[Theory]
[InlineData("/")]
[InlineData("/Index")]
[InlineData("/About")]
[InlineData("/Privacy")]
[InlineData("/Contact")]
public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
{
// Arrange
var client = _factory.CreateClient();
// Act
var response = await client.GetAsync(url);
// Assert
response.EnsureSuccessStatusCode(); // Status Code 200-299
Assert.Equal("text/html; charset=utf-8",
response.Content.Headers.ContentType.ToString());
}
}
Secara default, cookie yang tidak penting tidak dipertahankan di seluruh permintaan saat kebijakan persetujuan Peraturan Perlindungan Data Umum diaktifkan. Untuk mempertahankan cookie yang tidak penting, seperti yang digunakan oleh penyedia TempData, tandai sebagai esensial dalam pengujian Anda. Untuk petunjuk tentang menandai cookie sebagai penting, lihat Cookie penting.
AngleSharp vs Application Parts
untuk pemeriksaan antipemalsuan
Artikel ini menggunakan AngleSharp parser untuk menangani pemeriksaan antiforgery, dengan cara memuat halaman dan mengurai HTML. Untuk menguji titik akhir dari pengontrol dan tampilan Halaman pada tingkat yang lebih rendah tanpa mempedulikan bagaimana tampilan tersebut dirender di browser, pertimbangkan untuk menggunakan Razor. Pendekatan Bagian Aplikasi menyuntikkan pengontrol atau Razor Halaman yang dapat digunakan untuk membuat permintaan JSON ke dalam aplikasi untuk mendapatkan nilai yang diperlukan. Untuk informasi selengkapnya, lihat blog Pengujian Integrasi Sumber Daya ASP.NET Core yang Dilindungi dengan Antiforgery Menggunakan Bagian Aplikasi dan repo GitHub terkait oleh Martin Costello.
Menyesuaikan WebApplicationFactory
Konfigurasi host web dapat dibuat secara independen dari kelas pengujian dengan mewarisi dari WebApplicationFactory<TEntryPoint> untuk membuat satu atau beberapa pabrik kustom:
Warisi dari
WebApplicationFactory
dan ambil alih ConfigureWebHost. IWebHostBuilder memungkinkan konfigurasi kumpulan layanan denganIWebHostBuilder.ConfigureServices
public class CustomWebApplicationFactory<TProgram> : WebApplicationFactory<TProgram> where TProgram : class { protected override void ConfigureWebHost(IWebHostBuilder builder) { builder.ConfigureServices(services => { var dbContextDescriptor = services.SingleOrDefault( d => d.ServiceType == typeof(DbContextOptions<ApplicationDbContext>)); services.Remove(dbContextDescriptor); var dbConnectionDescriptor = services.SingleOrDefault( d => d.ServiceType == typeof(DbConnection)); services.Remove(dbConnectionDescriptor); // Create open SqliteConnection so EF won't automatically close it. services.AddSingleton<DbConnection>(container => { var connection = new SqliteConnection("DataSource=:memory:"); connection.Open(); return connection; }); services.AddDbContext<ApplicationDbContext>((container, options) => { var connection = container.GetRequiredService<DbConnection>(); options.UseSqlite(connection); }); }); builder.UseEnvironment("Development"); } }
Penyemaian database di aplikasi sampel dilakukan dengan
InitializeDbForTests
metode . Metode ini dijelaskan di bagian Sampel pengujian integrasi: Menguji organisasi aplikasi.Konteks database SUT terdaftar di
Program.cs
. Panggilan balikbuilder.ConfigureServices
dari aplikasi pengujian dijalankan setelah kode dari aplikasiProgram.cs
dijalankan. Untuk menggunakan database yang berbeda untuk pengujian daripada database aplikasi, konteks database aplikasi harus diganti dibuilder.ConfigureServices
.Aplikasi sampel menemukan pendeskripsi layanan untuk konteks database dan menggunakan deskriptor untuk menghapus pendaftaran layanan. Pabrik kemudian menambahkan sebuah
ApplicationDbContext
baru yang menggunakan database berbasis memori untuk pengujian.Untuk menyambungkan ke database lain, ubah
DbConnection
. Untuk menggunakan database pengujian SQL Server:- Rujuk paket
Microsoft.EntityFrameworkCore.SqlServer
NuGet dalam file proyek. - Panggil
UseInMemoryDatabase
.
- Rujuk paket
Gunakan kustom
CustomWebApplicationFactory
dalam kelas pengujian. Contoh berikut menggunakan pabrik di kelasIndexPageTests
:public class IndexPageTests : IClassFixture<CustomWebApplicationFactory<Program>> { private readonly HttpClient _client; private readonly CustomWebApplicationFactory<Program> _factory; public IndexPageTests( CustomWebApplicationFactory<Program> factory) { _factory = factory; _client = factory.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false }); } }
Klien aplikasi sampel dikonfigurasi untuk mencegah
HttpClient
pengalihan berikut. Seperti yang dijelaskan kemudian di bagian Mock autentikasi, ini memungkinkan pengujian untuk memeriksa hasil dari respons pertama aplikasi. Respons pertama adalah pengalihan di banyak pengujian ini denganLocation
header.Pengujian umum menggunakan metode dan pembantu
HttpClient
untuk memproses permintaan dan respons:[Fact] public async Task Post_DeleteAllMessagesHandler_ReturnsRedirectToRoot() { // Arrange var defaultPage = await _client.GetAsync("/"); var content = await HtmlHelpers.GetDocumentAsync(defaultPage); //Act var response = await _client.SendAsync( (IHtmlFormElement)content.QuerySelector("form[id='messages']"), (IHtmlButtonElement)content.QuerySelector("button[id='deleteAllBtn']")); // Assert Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode); Assert.Equal(HttpStatusCode.Redirect, response.StatusCode); Assert.Equal("/", response.Headers.Location.OriginalString); }
Setiap permintaan POST ke SUT harus memenuhi pemeriksaan anti-pemalsuan yang secara otomatis dilakukan oleh sistem perlindungan data aplikasi. Untuk mengatur permintaan POST pengujian, aplikasi pengujian harus:
- Buat permintaan untuk halaman tersebut.
- Uraikan antiforgery cookie dan minta token validasi dari respons.
- Lakukan permintaan POST dengan antiforgery cookie dan pastikan token validasi permintaan tersedia.
Metode ekstensi SendAsync
pembantu (Helpers/HttpClientExtensions.cs
) dan metode pembantu GetDocumentAsync
(Helpers/HtmlHelpers.cs
) dalam aplikasi contoh menggunakan pengurai AngleSharp untuk menangani pemeriksaan antiforgery dengan metode berikut:
-
GetDocumentAsync
: Menerima HttpResponseMessage dan mengembalikanIHtmlDocument
.GetDocumentAsync
menggunakan pengaturan yang menyiapkan respons virtual berdasarkanHttpResponseMessage
yang asli. Untuk informasi selengkapnya, lihat dokumentasi AngleSharp. -
SendAsync
metode ekstensi untukHttpClient
menyusun sebuah HttpRequestMessage dan memanggil SendAsync(HttpRequestMessage) untuk mengirimkan permintaan ke SUT. Kelebihan beban untukSendAsync
menerima formulir HTML (IHtmlFormElement
) dan yang berikut:- Tombol Kirim pada formulir (
IHtmlElement
) - Kumpulan nilai formulir (
IEnumerable<KeyValuePair<string, string>>
) - Tombol Kirim (
IHtmlElement
) dan nilai formulir (IEnumerable<KeyValuePair<string, string>>
)
- Tombol Kirim pada formulir (
AngleSharp adalah pustaka penguraian pihak ketiga yang digunakan untuk tujuan demonstrasi dalam artikel ini dan aplikasi sampel. AngleSharp tidak didukung atau diperlukan untuk pengujian integrasi aplikasi ASP.NET Core. Pengurai lain dapat digunakan, seperti Html Agility Pack (HAP). Pendekatan lain adalah menulis kode untuk menangani token verifikasi permintaan sistem antiforgery dan antiforgery cookie secara langsung. Lihat AngleSharp vs Application Parts
untuk pemeriksaan antiforgery di artikel ini untuk informasi selengkapnya.
Penyedia database dalam memori EF-Core dapat digunakan untuk pengujian terbatas dan dasar, namun penyedia SQLite adalah pilihan yang direkomendasikan untuk pengujian dalam memori.
Lihat Perluas Startup dengan filter startup yang menunjukkan cara mengonfigurasi middleware menggunakan IStartupFilter, yang berguna ketika pengujian membutuhkan layanan atau middleware kustom.
Menyesuaikan klien dengan WithWebHostBuilder
Ketika konfigurasi tambahan diperlukan dalam metode pengujian, WithWebHostBuilder membuat WebApplicationFactory
baru dengan IWebHostBuilder yang disesuaikan lebih lanjut oleh konfigurasi.
Kode sampel memanggil WithWebHostBuilder
untuk menggantikan layanan yang telah dikonfigurasi dengan stub pengujian. Untuk informasi selengkapnya dan contoh penggunaan, lihat Inject mock services di artikel ini.
Metode pengujian Post_DeleteMessageHandler_ReturnsRedirectToRoot
aplikasi sampel menunjukkan penggunaan WithWebHostBuilder
. Pengujian ini melakukan penghapusan rekaman dalam database dengan memicu pengiriman formulir di SUT.
Karena pengujian lain di IndexPageTests
melakukan operasi di kelas yang menghapus semua rekaman dalam database dan mungkin dijalankan sebelum metode Post_DeleteMessageHandler_ReturnsRedirectToRoot
, database diinisialisasi ulang dalam metode pengujian ini untuk memastikan adanya rekaman yang dapat dihapus oleh Sistem yang Diuji (SUT). Memilih tombol pertama hapus dari formulir messages
di SUT disimulasikan dalam permintaan ke SUT.
[Fact]
public async Task Post_DeleteMessageHandler_ReturnsRedirectToRoot()
{
// Arrange
using (var scope = _factory.Services.CreateScope())
{
var scopedServices = scope.ServiceProvider;
var db = scopedServices.GetRequiredService<ApplicationDbContext>();
Utilities.ReinitializeDbForTests(db);
}
var defaultPage = await _client.GetAsync("/");
var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
//Act
var response = await _client.SendAsync(
(IHtmlFormElement)content.QuerySelector("form[id='messages']"),
(IHtmlButtonElement)content.QuerySelector("form[id='messages']")
.QuerySelector("div[class='panel-body']")
.QuerySelector("button"));
// Assert
Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode);
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
Assert.Equal("/", response.Headers.Location.OriginalString);
}
Pengaturan klien
Lihat halaman WebApplicationFactoryClientOptions untuk default dan opsi yang tersedia saat membuat instance HttpClient
.
Buat kelas WebApplicationFactoryClientOptions
dan passing ke metode CreateClient().
public class IndexPageTests :
IClassFixture<CustomWebApplicationFactory<Program>>
{
private readonly HttpClient _client;
private readonly CustomWebApplicationFactory<Program>
_factory;
public IndexPageTests(
CustomWebApplicationFactory<Program> factory)
{
_factory = factory;
_client = factory.CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false
});
}
}
CATATAN: Untuk menghindari peringatan pengalihan HTTPS dalam log saat menggunakan Middleware Pengalihan HTTPS, atur BaseAddress = new Uri("https://localhost")
Menyuntikkan layanan contoh
Layanan dapat ditimpa dalam pengujian dengan panggilan ke ConfigureTestServices pada pembuat host. Untuk membatasi layanan yang ditimpa khusus untuk pengujian itu sendiri, metode WithWebHostBuilder ini digunakan untuk mendapatkan pembangun host. Ini dapat dilihat dalam pengujian berikut:
- Dapatkan_LayananKutipan_MenyediakanKutipanDiHalaman
- Get_GithubProfilePageCanGetAGithubUser
- Dapatkan_HalamanAmanDikembalikanUntukPenggunaTerautentikasi
Sampel SUT menyertakan layanan terlingkup yang mengembalikan kuotasi. Kutipan tersebut disematkan di bidang tersembunyi pada halaman Indeks ketika diminta.
Services/IQuoteService.cs
:
public interface IQuoteService
{
Task<string> GenerateQuote();
}
Services/QuoteService.cs
:
// Quote ©1975 BBC: The Doctor (Tom Baker); Dr. Who: Planet of Evil
// https://www.bbc.co.uk/programmes/p00pyrx6
public class QuoteService : IQuoteService
{
public Task<string> GenerateQuote()
{
return Task.FromResult<string>(
"Come on, Sarah. We've an appointment in London, " +
"and we're already 30,000 years late.");
}
}
Program.cs
:
services.AddScoped<IQuoteService, QuoteService>();
Pages/Index.cshtml.cs
:
public class IndexModel : PageModel
{
private readonly ApplicationDbContext _db;
private readonly IQuoteService _quoteService;
public IndexModel(ApplicationDbContext db, IQuoteService quoteService)
{
_db = db;
_quoteService = quoteService;
}
[BindProperty]
public Message Message { get; set; }
public IList<Message> Messages { get; private set; }
[TempData]
public string MessageAnalysisResult { get; set; }
public string Quote { get; private set; }
public async Task OnGetAsync()
{
Messages = await _db.GetMessagesAsync();
Quote = await _quoteService.GenerateQuote();
}
Pages/Index.cs
:
<input id="quote" type="hidden" value="@Model.Quote">
Markup berikut dihasilkan saat aplikasi SUT dijalankan:
<input id="quote" type="hidden" value="Come on, Sarah. We've an appointment in
London, and we're already 30,000 years late.">
Untuk menguji layanan dan menyuntikkan mock service dalam pengujian integrasi, layanan tiruan disuntikkan ke SUT oleh tes. Layanan tiruan mengganti aplikasi QuoteService
dengan layanan yang disediakan oleh aplikasi pengujian, yang disebut TestQuoteService
:
IntegrationTests.IndexPageTests.cs
:
// Quote ©1975 BBC: The Doctor (Tom Baker); Pyramids of Mars
// https://www.bbc.co.uk/programmes/p00pys55
public class TestQuoteService : IQuoteService
{
public Task<string> GenerateQuote()
{
return Task.FromResult(
"Something's interfering with time, Mr. Scarman, " +
"and time is my business.");
}
}
ConfigureTestServices
dipanggil, dan layanan terlingkup terdaftar:
[Fact]
public async Task Get_QuoteService_ProvidesQuoteInPage()
{
// Arrange
var client = _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.AddScoped<IQuoteService, TestQuoteService>();
});
})
.CreateClient();
//Act
var defaultPage = await client.GetAsync("/");
var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
var quoteElement = content.QuerySelector("#quote");
// Assert
Assert.Equal("Something's interfering with time, Mr. Scarman, " +
"and time is my business.", quoteElement.Attributes["value"].Value);
}
Markup yang dihasilkan selama eksekusi pengujian mencerminkan teks kutipan yang disediakan oleh TestQuoteService
, sehingga pernyataan lolos:
<input id="quote" type="hidden" value="Something's interfering with time,
Mr. Scarman, and time is my business.">
Autentikasi tiruan
Pengujian pada kelas AuthTests
memeriksa bahwa suatu titik akhir aman:
- Mengalihkan pengguna yang tidak diautentikasi ke halaman masuk aplikasi.
- Mengembalikan konten untuk pengguna yang diautentikasi.
Dalam SUT, /SecurePage
halaman menggunakan AuthorizePage konvensi untuk menerapkan AuthorizeFilter pada halaman. Untuk informasi selengkapnya, lihat Razor Konvensi otorisasi halaman.
services.AddRazorPages(options =>
{
options.Conventions.AuthorizePage("/SecurePage");
});
Dalam Get_SecurePageRedirectsAnUnauthenticatedUser
pengujian, WebApplicationFactoryClientOptions diatur untuk melarang pengalihan dengan menetapkan AllowAutoRedirect ke false
:
[Fact]
public async Task Get_SecurePageRedirectsAnUnauthenticatedUser()
{
// Arrange
var client = _factory.CreateClient(
new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false
});
// Act
var response = await client.GetAsync("/SecurePage");
// Assert
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
Assert.StartsWith("http://localhost/Identity/Account/Login",
response.Headers.Location.OriginalString);
}
Dengan melarang klien untuk mengikuti pengalihan, pemeriksaan berikut dapat dilakukan:
- Kode status yang dikembalikan oleh SUT dapat diperiksa terhadap hasil yang diharapkan HttpStatusCode.Redirect , bukan kode status akhir setelah pengalihan ke halaman masuk, yang akan menjadi HttpStatusCode.OK.
- Nilai header
Location
di header respons diperiksa untuk mengonfirmasi bahwa nilai tersebut dimulai denganhttp://localhost/Identity/Account/Login
, bukan respons halaman akhir masuk, di manaLocation
header tidak hadir.
Aplikasi pengujian dapat meniru AuthenticationHandler<TOptions>ConfigureTestServices untuk menguji aspek autentikasi dan otorisasi. Skenario yang minimal menghasilkan AuthenticateResult.Success.
public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
public TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger, UrlEncoder encoder)
: base(options, logger, encoder)
{
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var claims = new[] { new Claim(ClaimTypes.Name, "Test user") };
var identity = new ClaimsIdentity(claims, "Test");
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, "TestScheme");
var result = AuthenticateResult.Success(ticket);
return Task.FromResult(result);
}
}
TestAuthHandler
dipanggil untuk mengautentikasi pengguna ketika skema autentikasi diatur ke TestScheme
di mana AddAuthentication
terdaftar untuk ConfigureTestServices
. Penting agar skema TestScheme
sesuai dengan skema yang diharapkan oleh aplikasi Anda. Jika tidak, autentikasi tidak akan berfungsi.
[Fact]
public async Task Get_SecurePageIsReturnedForAnAuthenticatedUser()
{
// Arrange
var client = _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.AddAuthentication(defaultScheme: "TestScheme")
.AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(
"TestScheme", options => { });
});
})
.CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false,
});
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue(scheme: "TestScheme");
//Act
var response = await client.GetAsync("/SecurePage");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
Untuk informasi selengkapnya tentang WebApplicationFactoryClientOptions
, lihat bagian Opsi klien.
Pengujian dasar untuk middleware autentikasi
Lihat repositori GitHub ini untuk pengujian dasar middleware autentikasi. Ini berisi server pengujian yang khusus untuk skenario pengujian.
Mengatur lingkungan
Atur lingkungan di pabrik aplikasi kustom:
public class CustomWebApplicationFactory<TProgram>
: WebApplicationFactory<TProgram> where TProgram : class
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
var dbContextDescriptor = services.SingleOrDefault(
d => d.ServiceType ==
typeof(DbContextOptions<ApplicationDbContext>));
services.Remove(dbContextDescriptor);
var dbConnectionDescriptor = services.SingleOrDefault(
d => d.ServiceType ==
typeof(DbConnection));
services.Remove(dbConnectionDescriptor);
// Create open SqliteConnection so EF won't automatically close it.
services.AddSingleton<DbConnection>(container =>
{
var connection = new SqliteConnection("DataSource=:memory:");
connection.Open();
return connection;
});
services.AddDbContext<ApplicationDbContext>((container, options) =>
{
var connection = container.GetRequiredService<DbConnection>();
options.UseSqlite(connection);
});
});
builder.UseEnvironment("Development");
}
}
Cara infrastruktur pengujian menyimpulkan jalur akar konten aplikasi
WebApplicationFactory
Konstruktor menyimpulkan jalur akar konten aplikasi dengan mencari pada rakitan yang WebApplicationFactoryContentRootAttribute berisi pengujian integrasi dengan kunci yang sama dengan TEntryPoint
rakitan System.Reflection.Assembly.FullName
. Jika atribut dengan kunci yang benar tidak ditemukan, WebApplicationFactory
kembali mencari file solusi (.sln) dan menambahkan nama perakitan TEntryPoint
ke direktori solusi. Direktori akar aplikasi (jalur akar konten) digunakan untuk menemukan tampilan dan file konten.
Menonaktifkan penyalinan bayangan
Penyalinan bayangan menyebabkan pengujian dijalankan di direktori yang berbeda dari direktori output. Jika pengujian Anda mengandalkan pemuatan file yang relatif terhadap Assembly.Location
dan Anda mengalami masalah, Anda mungkin harus menonaktifkan penyalinan bayangan.
Untuk menonaktifkan penyalinan bayangan saat menggunakan xUnit, buat xunit.runner.json
file di direktori proyek pengujian Anda, dengan pengaturan konfigurasi yang benar:
{
"shadowCopy": false
}
Pembuangan objek
Setelah pengujian IClassFixture
implementasi dijalankan, TestServer dan HttpClient dibuang ketika xUnit membuang WebApplicationFactory
. Jika objek yang dibuat oleh pengembang memerlukan pembuangan, buang dalam implementasi IClassFixture
. Untuk informasi selengkapnya, lihat Menerapkan metode Dispose.
Contoh pengujian integrasi
Aplikasi sampel terdiri dari dua aplikasi:
Aplikasi | Direktori proyek | Deskripsi |
---|---|---|
Aplikasi pesan (SUT) | src/RazorPagesProject |
Memungkinkan pengguna untuk menambahkan, menghapus satu, menghapus semua, dan menganalisis pesan. |
Menguji aplikasi | tests/RazorPagesProject.Tests |
Digunakan untuk menguji integrasi SUT. |
Pengujian dapat dijalankan menggunakan fitur pengujian bawaan IDE, seperti Visual Studio. Jika menggunakan Visual Studio Code atau baris perintah, jalankan perintah berikut pada prompt perintah di tests/RazorPagesProject.Tests
direktori:
dotnet test
Organisasi aplikasi pesan (SUT)
SUT adalah Razor sistem pesan Pages dengan karakteristik berikut:
- Halaman Indeks aplikasi (
Pages/Index.cshtml
danPages/Index.cshtml.cs
) menyediakan metode UI dan model halaman untuk mengontrol penambahan, penghapusan, dan analisis pesan (kata rata-rata per pesan). - Pesan dijelaskan oleh
Message
kelas (Data/Message.cs
) dengan dua properti:Id
(kunci) danText
(pesan). PropertiText
diperlukan dan dibatasi hingga 200 karakter. - Pesan disimpan menggunakan database dalam memori Entity Framework†.
- Aplikasi ini berisi lapisan akses data (DAL) di kelas konteks databasenya,
AppDbContext
(Data/AppDbContext.cs
). - Jika database kosong pada startup aplikasi, penyimpanan pesan diinisialisasi dengan tiga pesan.
- Aplikasi ini menyertakan
/SecurePage
yang hanya dapat diakses oleh pengguna yang diautentikasi.
† Artikel EF, Uji dengan InMemory, menjelaskan cara menggunakan database dalam memori untuk pengujian dengan MSTest. Topik ini menggunakan kerangka kerja pengujian xUnit . Konsep pengujian dan implementasi pengujian di berbagai kerangka kerja pengujian serupa tetapi tidak identik.
Meskipun aplikasi tidak menggunakan pola repositori dan bukan contoh pola Unit kerja (UoW) yang efektif, Razor Pages mendukung pola pengembangan ini. Untuk informasi selengkapnya, lihat Merancang lapisan persistensi infrastruktur dan Logika pengontrol Pengujian (sampel mengimplementasikan pola repositori).
Menguji organisasi aplikasi
Aplikasi pengujian adalah aplikasi konsol di direktori tests/RazorPagesProject.Tests
.
Menguji direktori aplikasi | Deskripsi |
---|---|
AuthTests |
Berisi metode pengujian untuk:
|
BasicTests |
Berisi metode pengujian untuk perutean dan jenis konten. |
IntegrationTests |
Berisi pengujian integrasi untuk halaman Indeks menggunakan kelas kustom WebApplicationFactory . |
Helpers/Utilities |
|
Kerangka kerja pengujian adalah xUnit. Pengujian integrasi dilakukan menggunakan Microsoft.AspNetCore.TestHost, yang mencakup TestServer. Karena paket Microsoft.AspNetCore.Mvc.Testing
digunakan untuk mengonfigurasi host dan server pengujian, paket TestHost
dan TestServer
tidak memerlukan referensi paket langsung dalam file proyek aplikasi pengujian atau konfigurasi pengembang di aplikasi pengujian.
Pengujian integrasi biasanya memerlukan himpunan data kecil dalam database sebelum eksekusi pengujian. Misalnya, pengujian penghapusan memerlukan penghapusan catatan dalam database, sehingga database harus memiliki setidaknya satu catatan agar penghapusan berhasil dilakukan.
Aplikasi contoh memuat database dengan tiga pesan dalam Utilities.cs
yang dapat digunakan dalam pengujian saat dijalankan.
public static void InitializeDbForTests(ApplicationDbContext db)
{
db.Messages.AddRange(GetSeedingMessages());
db.SaveChanges();
}
public static void ReinitializeDbForTests(ApplicationDbContext db)
{
db.Messages.RemoveRange(db.Messages);
InitializeDbForTests(db);
}
public static List<Message> GetSeedingMessages()
{
return new List<Message>()
{
new Message(){ Text = "TEST RECORD: You're standing on my scarf." },
new Message(){ Text = "TEST RECORD: Would you like a jelly baby?" },
new Message(){ Text = "TEST RECORD: To the rational mind, " +
"nothing is inexplicable; only unexplained." }
};
}
Konteks database SUT terdaftar di Program.cs
. Panggilan balik builder.ConfigureServices
dari aplikasi pengujian dijalankan setelah kode dari aplikasi Program.cs
dijalankan. Untuk menggunakan database yang berbeda untuk pengujian, konteks database aplikasi harus diganti di builder.ConfigureServices
. Untuk informasi selengkapnya, lihat bagian Kustomisasi WebApplicationFactory .
Sumber Daya Tambahan:
Artikel ini mengasumsikan pemahaman dasar tentang pengujian unit. Jika tidak terbiasa dengan konsep pengujian, lihat artikel Pengujian Unit di .NET Core dan .NET Standard dan konten tertautnya.
Melihat atau mengunduh kode sampel (cara mengunduh)
Aplikasi sampel adalah Razor aplikasi Pages dan mengasumsikan pengetahuan dasar mengenai Razor Pages. Jika Anda tidak terbiasa dengan Razor Pages, lihat artikel berikut ini:
Untuk menguji SPAs, kami merekomendasikan alat seperti Playwright untuk .NET, yang dapat mengotomatiskan browser.
Pengantar pengujian integrasi
Pengujian integrasi mengevaluasi komponen aplikasi pada tingkat yang lebih luas daripada pengujian unit. Pengujian unit digunakan untuk menguji komponen perangkat lunak terisolasi, seperti metode kelas individual. Pengujian integrasi mengonfirmasi bahwa dua komponen aplikasi atau lebih bekerja sama untuk menghasilkan hasil yang diharapkan, mungkin termasuk setiap komponen yang diperlukan untuk memproses permintaan sepenuhnya.
Pengujian yang lebih luas ini digunakan untuk menguji infrastruktur aplikasi dan seluruh kerangka kerja, sering kali termasuk komponen berikut:
- Basis data
- Sistem file
- Appliance jaringan
- Jalur permintaan-tanggapan
Pengujian unit menggunakan komponen fabrikasi, yang dikenal sebagai objek palsu atau tiruan, sebagai pengganti komponen infrastruktur.
Berbeda dengan pengujian unit, pengujian integrasi:
- Gunakan komponen aktual yang digunakan aplikasi dalam produksi.
- Memerlukan lebih banyak kode dan pemrosesan data.
- Membutuhkan waktu lebih lama untuk berjalan.
Oleh karena itu, batasi penggunaan pengujian integrasi ke skenario infrastruktur yang paling penting. Jika perilaku dapat diuji menggunakan pengujian unit atau pengujian integrasi, pilih pengujian unit.
Dalam diskusi pengujian integrasi, proyek yang diuji sering disebut System Under Test, atau "SUT" singkatnya. "SUT" digunakan di seluruh artikel ini untuk merujuk ke aplikasi ASP.NET Core yang sedang diuji.
Jangan menulis pengujian integrasi untuk setiap permutasi akses data dan file yang melibatkan database dan sistem file. Terlepas dari berapa banyak tempat di seluruh aplikasi berinteraksi dengan database dan sistem file, serangkaian pengujian integrasi baca, tulis, perbarui, dan hapus yang berfokus biasanya mampu menguji database dan komponen sistem file dengan memadai. Gunakan pengujian unit untuk pengujian rutin logika metode yang berinteraksi dengan komponen-komponen ini. Dalam pengujian unit, penggunaan infrastruktur palsu atau tiruan menghasilkan eksekusi pengujian yang lebih cepat.
pengujian integrasi ASP.NET Core
Pengujian integrasi di ASP.NET Core memerlukan hal berikut:
- Proyek pengujian digunakan untuk memuat dan menjalankan pengujian. Proyek pengujian memiliki referensi ke SUT.
- Proyek pengujian membuat host web pengujian untuk SUT dan menggunakan klien server pengujian untuk menangani permintaan dan respons dengan SUT.
- Pelari uji digunakan untuk menjalankan pengujian dan melaporkan hasil pengujian.
Pengujian integrasi mengikuti urutan langkah pengujian yang mencakup tahapan Arrange, Act, dan Assert yang biasa:
- Server web SUT telah dikonfigurasi.
- Klien server pengujian dibuat untuk mengirimkan permintaan ke aplikasi.
- Langkah Atur pengujian dijalankan: Aplikasi pengujian menyiapkan permintaan.
- Langkah pengujian Act dijalankan: Klien mengirimkan permintaan dan menerima respons.
- Langkah pengujian Assert dijalankan: Respons aktual divalidasi sebagai lulus atau gagal berdasarkan respons yang diharapkan .
- Proses berlanjut sampai semua pengujian dijalankan.
- Hasil pengujian dilaporkan.
Biasanya, host web pengujian dikonfigurasi secara berbeda dari host web normal aplikasi untuk eksekusi pengujian. Misalnya, database yang berbeda atau pengaturan aplikasi yang berbeda dapat digunakan untuk pengujian.
Komponen infrastruktur, seperti host web pengujian dan server pengujian dalam memori (TestServer), disediakan atau dikelola oleh paket Microsoft.AspNetCore.Mvc.Testing . Penggunaan paket ini menyederhanakan pembuatan dan eksekusi pengujian.
Paket Microsoft.AspNetCore.Mvc.Testing
menangani tugas-tugas berikut:
- Menyalin file dependensi (
.deps
) dari SUT ke direktori proyekbin
pengujian. - Menetapkan akar konten ke akar proyek SUT agar file statis serta halaman dan tampilan dapat ditemukan saat pengujian berlangsung.
-
Menyediakan kelas WebApplicationFactory untuk mempermudah proses awal pengoperasian SUT dengan
TestServer
.
Dokumentasi tes unit menjelaskan cara menyiapkan proyek pengujian dan pengelola pengujian, bersama dengan instruksi terperinci tentang cara menjalankan tes serta rekomendasi tentang cara memberi nama tes dan kelas tes.
Memisahkan pengujian unit dari pengujian integrasi ke dalam proyek yang berbeda. Memisahkan pengujian:
- Membantu memastikan bahwa komponen pengujian infrastruktur tidak secara tidak sengaja disertakan dalam pengujian unit.
- Memungkinkan kontrol atas set pengujian yang akan dijalankan.
Hampir tidak ada perbedaan antara konfigurasi untuk pengujian Razor aplikasi Pages dan aplikasi MVC. Satu-satunya perbedaan adalah bagaimana tes diberi nama.
Razor Di aplikasi Pages, pengujian titik akhir halaman biasanya dinamai sesuai dengan kelas model halaman (misalnya, IndexPageTests
untuk menguji integrasi komponen untuk halaman Indeks). Dalam aplikasi MVC, pengujian biasanya diatur oleh kelas pengontrol dan dinamai sesuai pengontrol yang mereka uji (misalnya, HomeControllerTests
untuk menguji integrasi komponen untuk Home pengontrol).
Menguji prasyarat aplikasi
Proyek pengujian harus:
- Merujuk ke paket
Microsoft.AspNetCore.Mvc.Testing
. - Tentukan Web SDK dalam file proyek (
<Project Sdk="Microsoft.NET.Sdk.Web">
).
Prasyarat ini dapat dilihat di aplikasi sampel. Periksa file tests/RazorPagesProject.Tests/RazorPagesProject.Tests.csproj
. Aplikasi sampel menggunakan kerangka kerja pengujian xUnit dan pustaka pengurai AngleSharp , sehingga aplikasi sampel juga mereferensikan:
Di aplikasi yang menggunakan xunit.runner.visualstudio
versi 2.4.2 atau yang lebih baru, proyek pengujian harus mereferensikan Microsoft.NET.Test.Sdk
paket.
Entity Framework Core juga digunakan dalam pengujian. Lihat file proyek di GitHub.
Lingkungan SUT
Jika lingkungan SUT tidak diatur, lingkungan akan default ke Pengembangan.
Pengujian dasar dengan WebApplicationFactory default
Ekspos kelas yang ditentukan Program
secara implisit ke proyek pengujian dengan melakukan salah satu hal berikut:
Mengekspos tipe data internal dari aplikasi web ke proyek pengujian. Ini dapat dilakukan dalam file proyek SUT (
.csproj
):<ItemGroup> <InternalsVisibleTo Include="MyTestProject" /> </ItemGroup>
Buat kelas publik
Program
menggunakan deklarasi kelas parsial:var builder = WebApplication.CreateBuilder(args); // ... Configure services, routes, etc. app.Run(); + public partial class Program { }
Aplikasi sampel menggunakan pendekatan kelas parsial
Program
.
WebApplicationFactory<TEntryPoint> digunakan untuk membuat TestServer untuk pengujian integrasi.
TEntryPoint
adalah kelas titik masuk SUT, biasanya Program.cs
.
Kelas uji menerapkan antarmuka fikstur kelas untuk menunjukkan bahwa kelas tersebut berisi pengujian dan menyediakan instans objek yang dapat digunakan bersama di seluruh pengujian dalam kelas.
Kelas pengujian berikut, BasicTests
, menggunakan WebApplicationFactory
untuk bootstrap SUT dan menyediakan HttpClient untuk metode pengujian, Get_EndpointsReturnSuccessAndCorrectContentType
. Metode memverifikasi kode status respons berhasil (200-299) dan Content-Type
header adalah text/html; charset=utf-8
untuk beberapa halaman aplikasi.
CreateClient() membuat instans HttpClient
yang secara otomatis mengikuti pengalihan dan menangani cookie.
public class BasicTests
: IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;
public BasicTests(WebApplicationFactory<Program> factory)
{
_factory = factory;
}
[Theory]
[InlineData("/")]
[InlineData("/Index")]
[InlineData("/About")]
[InlineData("/Privacy")]
[InlineData("/Contact")]
public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
{
// Arrange
var client = _factory.CreateClient();
// Act
var response = await client.GetAsync(url);
// Assert
response.EnsureSuccessStatusCode(); // Status Code 200-299
Assert.Equal("text/html; charset=utf-8",
response.Content.Headers.ContentType.ToString());
}
}
[TestClass]
public class BasicTests
{
private static CustomWebApplicationFactory<Program> _factory;
[ClassInitialize]
public static void AssemblyInitialize(TestContext _)
{
_factory = new CustomWebApplicationFactory<Program>();
}
[ClassCleanup(ClassCleanupBehavior.EndOfClass)]
public static void AssemblyCleanup(TestContext _)
{
_factory.Dispose();
}
[TestMethod]
[DataRow("/")]
[DataRow("/Index")]
[DataRow("/About")]
[DataRow("/Privacy")]
[DataRow("/Contact")]
public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
{
// Arrange
var client = _factory.CreateClient();
// Act
var response = await client.GetAsync(url);
// Assert
response.EnsureSuccessStatusCode(); // Status Code 200-299
Assert.AreEqual("text/html; charset=utf-8",
response.Content.Headers.ContentType.ToString());
}
}
public class BasicTests
{
private CustomWebApplicationFactory<Program>
_factory;
[SetUp]
public void SetUp()
{
_factory = new CustomWebApplicationFactory<Program>();
}
[TearDown]
public void TearDown()
{
_factory.Dispose();
}
[DatapointSource]
public string[] values = ["/", "/Index", "/About", "/Privacy", "/Contact"];
[Theory]
public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
{
// Arrange
var client = _factory.CreateClient();
// Act
var response = await client.GetAsync(url);
// Assert
response.EnsureSuccessStatusCode(); // Status Code 200-299
Assert.That(response.Content.Headers.ContentType.ToString(), Is.EqualTo("text/html; charset=utf-8"));
}
}
Secara default, cookie yang tidak penting tidak dipertahankan di seluruh permintaan saat kebijakan persetujuan Peraturan Perlindungan Data Umum diaktifkan. Untuk mempertahankan cookie yang tidak penting, seperti yang digunakan oleh penyedia TempData, tandai sebagai esensial dalam pengujian Anda. Untuk petunjuk tentang menandai cookie sebagai penting, lihat Cookie penting.
AngleSharp vs Application Parts
untuk pemeriksaan antipemalsuan
Artikel ini menggunakan AngleSharp parser untuk menangani pemeriksaan antiforgery, dengan cara memuat halaman dan mengurai HTML. Untuk menguji titik akhir dari pengontrol dan tampilan Halaman pada tingkat yang lebih rendah tanpa mempedulikan bagaimana tampilan tersebut dirender di browser, pertimbangkan untuk menggunakan Razor. Pendekatan Bagian Aplikasi menyuntikkan pengontrol atau Razor Halaman yang dapat digunakan untuk membuat permintaan JSON ke dalam aplikasi untuk mendapatkan nilai yang diperlukan. Untuk informasi selengkapnya, lihat blog Pengujian Integrasi Sumber Daya ASP.NET Core yang Dilindungi dengan Antiforgery Menggunakan Bagian Aplikasi dan repo GitHub terkait oleh Martin Costello.
Menyesuaikan WebApplicationFactory
Konfigurasi host web dapat dibuat secara independen dari kelas pengujian dengan mewarisi dari WebApplicationFactory<TEntryPoint> untuk membuat satu atau beberapa pabrik kustom:
Warisi dari
WebApplicationFactory
dan ambil alih ConfigureWebHost. IWebHostBuilder memungkinkan konfigurasi kumpulan layanan denganIWebHostBuilder.ConfigureServices
public class CustomWebApplicationFactory<TProgram> : WebApplicationFactory<TProgram> where TProgram : class { protected override void ConfigureWebHost(IWebHostBuilder builder) { builder.ConfigureServices(services => { var dbContextDescriptor = services.SingleOrDefault( d => d.ServiceType == typeof(IDbContextOptionsConfiguration<ApplicationDbContext>)); services.Remove(dbContextDescriptor); var dbConnectionDescriptor = services.SingleOrDefault( d => d.ServiceType == typeof(DbConnection)); services.Remove(dbConnectionDescriptor); // Create open SqliteConnection so EF won't automatically close it. services.AddSingleton<DbConnection>(container => { var connection = new SqliteConnection("DataSource=:memory:"); connection.Open(); return connection; }); services.AddDbContext<ApplicationDbContext>((container, options) => { var connection = container.GetRequiredService<DbConnection>(); options.UseSqlite(connection); }); }); builder.UseEnvironment("Development"); } }
public class CustomWebApplicationFactory<TProgram> : WebApplicationFactory<TProgram> where TProgram : class { protected override void ConfigureWebHost(IWebHostBuilder builder) { builder.ConfigureServices(services => { var dbContextDescriptor = services.SingleOrDefault( d => d.ServiceType == typeof(IDbContextOptionsConfiguration<ApplicationDbContext>)); services.Remove(dbContextDescriptor); var dbConnectionDescriptor = services.SingleOrDefault( d => d.ServiceType == typeof(DbConnection)); services.Remove(dbConnectionDescriptor); // Create open SqliteConnection so EF won't automatically close it. services.AddSingleton<DbConnection>(container => { var connection = new SqliteConnection("DataSource=:memory:"); connection.Open(); return connection; }); services.AddDbContext<ApplicationDbContext>((container, options) => { var connection = container.GetRequiredService<DbConnection>(); options.UseSqlite(connection); }); }); builder.UseEnvironment("Development"); } }
public class CustomWebApplicationFactory<TProgram> : WebApplicationFactory<TProgram> where TProgram : class { protected override void ConfigureWebHost(IWebHostBuilder builder) { builder.ConfigureServices(services => { var dbContextDescriptor = services.SingleOrDefault( d => d.ServiceType == typeof(IDbContextOptionsConfiguration<ApplicationDbContext>)); services.Remove(dbContextDescriptor); var dbConnectionDescriptor = services.SingleOrDefault( d => d.ServiceType == typeof(DbConnection)); services.Remove(dbConnectionDescriptor); // Create open SqliteConnection so EF won't automatically close it. services.AddSingleton<DbConnection>(container => { var connection = new SqliteConnection("DataSource=:memory:"); connection.Open(); return connection; }); services.AddDbContext<ApplicationDbContext>((container, options) => { var connection = container.GetRequiredService<DbConnection>(); options.UseSqlite(connection); }); }); builder.UseEnvironment("Development"); } }
Penyemaian database di aplikasi sampel dilakukan dengan
InitializeDbForTests
metode . Metode ini dijelaskan di bagian Sampel pengujian integrasi: Menguji organisasi aplikasi.Konteks database SUT terdaftar di
Program.cs
. Panggilan balikbuilder.ConfigureServices
dari aplikasi pengujian dijalankan setelah kode dari aplikasiProgram.cs
dijalankan. Untuk menggunakan database yang berbeda untuk pengujian daripada database aplikasi, konteks database aplikasi harus diganti dibuilder.ConfigureServices
.Aplikasi sampel menemukan pendeskripsi layanan untuk konteks database dan menggunakan deskriptor untuk menghapus pendaftaran layanan. Pabrik kemudian menambahkan sebuah
ApplicationDbContext
baru yang menggunakan database berbasis memori untuk pengujian.Untuk menyambungkan ke database lain, ubah
DbConnection
. Untuk menggunakan database pengujian SQL Server:- Rujuk paket
Microsoft.EntityFrameworkCore.SqlServer
NuGet dalam file proyek. - Panggil
UseInMemoryDatabase
.
- Rujuk paket
Gunakan kustom
CustomWebApplicationFactory
dalam kelas pengujian. Contoh berikut menggunakan pabrik di kelasIndexPageTests
:public class IndexPageTests : IClassFixture<CustomWebApplicationFactory<Program>> { private readonly HttpClient _client; private readonly CustomWebApplicationFactory<Program> _factory; public IndexPageTests( CustomWebApplicationFactory<Program> factory) { _factory = factory; _client = factory.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false }); }
[TestClass] public class IndexPageTests { private static HttpClient _client; private static CustomWebApplicationFactory<Program> _factory; [ClassInitialize] public static void AssemblyInitialize(TestContext _) { _factory = new CustomWebApplicationFactory<Program>(); _client = _factory.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false }); } [ClassCleanup(ClassCleanupBehavior.EndOfClass)] public static void AssemblyCleanup(TestContext _) { _factory.Dispose(); }
public class IndexPageTests { private HttpClient _client; private CustomWebApplicationFactory<Program> _factory; [SetUp] public void SetUp() { _factory = new CustomWebApplicationFactory<Program>(); _client = _factory.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false }); } [TearDown] public void TearDown() { _factory.Dispose(); _client.Dispose(); }
Klien aplikasi sampel dikonfigurasi untuk mencegah
HttpClient
pengalihan berikut. Seperti yang dijelaskan kemudian di bagian Mock autentikasi, ini memungkinkan pengujian untuk memeriksa hasil dari respons pertama aplikasi. Respons pertama adalah pengalihan di banyak pengujian ini denganLocation
header.Pengujian umum menggunakan metode dan pembantu
HttpClient
untuk memproses permintaan dan respons:[Fact] public async Task Post_DeleteAllMessagesHandler_ReturnsRedirectToRoot() { // Arrange var defaultPage = await _client.GetAsync("/"); var content = await HtmlHelpers.GetDocumentAsync(defaultPage); //Act var response = await _client.SendAsync( (IHtmlFormElement)content.QuerySelector("form[id='messages']"), (IHtmlButtonElement)content.QuerySelector("button[id='deleteAllBtn']")); // Assert Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode); Assert.Equal(HttpStatusCode.Redirect, response.StatusCode); Assert.Equal("/", response.Headers.Location.OriginalString); }
[TestMethod] public async Task Post_DeleteAllMessagesHandler_ReturnsRedirectToRoot() { // Arrange var defaultPage = await _client.GetAsync("/"); var content = await HtmlHelpers.GetDocumentAsync(defaultPage); //Act var response = await _client.SendAsync( (IHtmlFormElement)content.QuerySelector("form[id='messages']"), (IHtmlButtonElement)content.QuerySelector("button[id='deleteAllBtn']")); // Assert Assert.AreEqual(HttpStatusCode.OK, defaultPage.StatusCode); Assert.AreEqual(HttpStatusCode.Redirect, response.StatusCode); Assert.AreEqual("/", response.Headers.Location.OriginalString); }
[Test] public async Task Post_DeleteAllMessagesHandler_ReturnsRedirectToRoot() { // Arrange var defaultPage = await _client.GetAsync("/"); var content = await HtmlHelpers.GetDocumentAsync(defaultPage); //Act var response = await _client.SendAsync( (IHtmlFormElement)content.QuerySelector("form[id='messages']"), (IHtmlButtonElement)content.QuerySelector("button[id='deleteAllBtn']")); // Assert Assert.That(defaultPage.StatusCode, Is.EqualTo(HttpStatusCode.OK)); Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Redirect)); Assert.That(response.Headers.Location.OriginalString, Is.EqualTo("/")); }
Setiap permintaan POST ke SUT harus memenuhi pemeriksaan anti-pemalsuan yang secara otomatis dilakukan oleh sistem perlindungan data aplikasi. Untuk mengatur permintaan POST pengujian, aplikasi pengujian harus:
- Buat permintaan untuk halaman tersebut.
- Uraikan antiforgery cookie dan minta token validasi dari respons.
- Lakukan permintaan POST dengan antiforgery cookie dan pastikan token validasi permintaan tersedia.
Metode ekstensi SendAsync
pembantu (Helpers/HttpClientExtensions.cs
) dan metode pembantu GetDocumentAsync
(Helpers/HtmlHelpers.cs
) dalam aplikasi contoh menggunakan pengurai AngleSharp untuk menangani pemeriksaan antiforgery dengan metode berikut:
-
GetDocumentAsync
: Menerima HttpResponseMessage dan mengembalikanIHtmlDocument
.GetDocumentAsync
menggunakan pengaturan yang menyiapkan respons virtual berdasarkanHttpResponseMessage
yang asli. Untuk informasi selengkapnya, lihat dokumentasi AngleSharp. -
SendAsync
metode ekstensi untukHttpClient
menyusun sebuah HttpRequestMessage dan memanggil SendAsync(HttpRequestMessage) untuk mengirimkan permintaan ke SUT. Kelebihan beban untukSendAsync
menerima formulir HTML (IHtmlFormElement
) dan yang berikut:- Tombol Kirim pada formulir (
IHtmlElement
) - Kumpulan nilai formulir (
IEnumerable<KeyValuePair<string, string>>
) - Tombol Kirim (
IHtmlElement
) dan nilai formulir (IEnumerable<KeyValuePair<string, string>>
)
- Tombol Kirim pada formulir (
AngleSharp adalah pustaka penguraian pihak ketiga yang digunakan untuk tujuan demonstrasi dalam artikel ini dan aplikasi sampel. AngleSharp tidak didukung atau diperlukan untuk pengujian integrasi aplikasi ASP.NET Core. Pengurai lain dapat digunakan, seperti Html Agility Pack (HAP). Pendekatan lain adalah menulis kode untuk menangani token verifikasi permintaan sistem antiforgery dan antiforgery cookie secara langsung. Lihat AngleSharp vs Application Parts
untuk pemeriksaan antiforgery di artikel ini untuk informasi selengkapnya.
Penyedia database dalam memori EF-Core dapat digunakan untuk pengujian terbatas dan dasar, namun penyedia SQLite adalah pilihan yang direkomendasikan untuk pengujian dalam memori.
Lihat Perluas Startup dengan filter startup yang menunjukkan cara mengonfigurasi middleware menggunakan IStartupFilter, yang berguna ketika pengujian membutuhkan layanan atau middleware kustom.
Menyesuaikan klien dengan WithWebHostBuilder
Ketika konfigurasi tambahan diperlukan dalam metode pengujian, WithWebHostBuilder membuat WebApplicationFactory
baru dengan IWebHostBuilder yang disesuaikan lebih lanjut oleh konfigurasi.
Kode sampel memanggil WithWebHostBuilder
untuk menggantikan layanan yang telah dikonfigurasi dengan stub pengujian. Untuk informasi selengkapnya dan contoh penggunaan, lihat Inject mock services di artikel ini.
Metode pengujian Post_DeleteMessageHandler_ReturnsRedirectToRoot
aplikasi sampel menunjukkan penggunaan WithWebHostBuilder
. Pengujian ini melakukan penghapusan rekaman dalam database dengan memicu pengiriman formulir di SUT.
Karena pengujian lain di IndexPageTests
melakukan operasi di kelas yang menghapus semua rekaman dalam database dan mungkin dijalankan sebelum metode Post_DeleteMessageHandler_ReturnsRedirectToRoot
, database diinisialisasi ulang dalam metode pengujian ini untuk memastikan adanya rekaman yang dapat dihapus oleh Sistem yang Diuji (SUT). Memilih tombol pertama hapus dari formulir messages
di SUT disimulasikan dalam permintaan ke SUT.
[Fact]
public async Task Post_DeleteMessageHandler_ReturnsRedirectToRoot()
{
// Arrange
using (var scope = _factory.Services.CreateScope())
{
var scopedServices = scope.ServiceProvider;
var db = scopedServices.GetRequiredService<ApplicationDbContext>();
Utilities.ReinitializeDbForTests(db);
}
var defaultPage = await _client.GetAsync("/");
var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
//Act
var response = await _client.SendAsync(
(IHtmlFormElement)content.QuerySelector("form[id='messages']"),
(IHtmlButtonElement)content.QuerySelector("form[id='messages']")
.QuerySelector("div[class='panel-body']")
.QuerySelector("button"));
// Assert
Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode);
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
Assert.Equal("/", response.Headers.Location.OriginalString);
}
[TestMethod]
public async Task Post_DeleteMessageHandler_ReturnsRedirectToRoot()
{
// Arrange
using (var scope = _factory.Services.CreateScope())
{
var scopedServices = scope.ServiceProvider;
var db = scopedServices.GetRequiredService<ApplicationDbContext>();
Utilities.ReinitializeDbForTests(db);
}
var defaultPage = await _client.GetAsync("/");
var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
//Act
var response = await _client.SendAsync(
(IHtmlFormElement)content.QuerySelector("form[id='messages']"),
(IHtmlButtonElement)content.QuerySelector("form[id='messages']")
.QuerySelector("div[class='panel-body']")
.QuerySelector("button"));
// Assert
Assert.AreEqual(HttpStatusCode.OK, defaultPage.StatusCode);
Assert.AreEqual(HttpStatusCode.Redirect, response.StatusCode);
Assert.AreEqual("/", response.Headers.Location.OriginalString);
}
[Test]
public async Task Post_DeleteMessageHandler_ReturnsRedirectToRoot()
{
// Arrange
using (var scope = _factory.Services.CreateScope())
{
var scopedServices = scope.ServiceProvider;
var db = scopedServices.GetRequiredService<ApplicationDbContext>();
Utilities.ReinitializeDbForTests(db);
}
var defaultPage = await _client.GetAsync("/");
var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
//Act
var response = await _client.SendAsync(
(IHtmlFormElement)content.QuerySelector("form[id='messages']"),
(IHtmlButtonElement)content.QuerySelector("form[id='messages']")
.QuerySelector("div[class='panel-body']")
.QuerySelector("button"));
// Assert
Assert.That(defaultPage.StatusCode, Is.EqualTo(HttpStatusCode.OK));
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Redirect));
Assert.That(response.Headers.Location.OriginalString, Is.EqualTo("/"));
}
Pengaturan klien
Lihat halaman WebApplicationFactoryClientOptions untuk default dan opsi yang tersedia saat membuat instance HttpClient
.
Buat kelas WebApplicationFactoryClientOptions
dan passing ke metode CreateClient().
public class IndexPageTests :
IClassFixture<CustomWebApplicationFactory<Program>>
{
private readonly HttpClient _client;
private readonly CustomWebApplicationFactory<Program>
_factory;
public IndexPageTests(
CustomWebApplicationFactory<Program> factory)
{
_factory = factory;
_client = factory.CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false
});
}
[TestClass]
public class IndexPageTests
{
private static HttpClient _client;
private static CustomWebApplicationFactory<Program>
_factory;
[ClassInitialize]
public static void AssemblyInitialize(TestContext _)
{
_factory = new CustomWebApplicationFactory<Program>();
_client = _factory.CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false
});
}
[ClassCleanup(ClassCleanupBehavior.EndOfClass)]
public static void AssemblyCleanup(TestContext _)
{
_factory.Dispose();
}
public class IndexPageTests
{
private HttpClient _client;
private CustomWebApplicationFactory<Program>
_factory;
[SetUp]
public void SetUp()
{
_factory = new CustomWebApplicationFactory<Program>();
_client = _factory.CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false
});
}
[TearDown]
public void TearDown()
{
_factory.Dispose();
_client.Dispose();
}
CATATAN: Untuk menghindari peringatan pengalihan HTTPS dalam log saat menggunakan Middleware Pengalihan HTTPS, atur BaseAddress = new Uri("https://localhost")
Menyuntikkan layanan contoh
Layanan dapat ditimpa dalam pengujian dengan panggilan ke ConfigureTestServices pada pembuat host. Untuk membatasi layanan yang ditimpa khusus untuk pengujian itu sendiri, metode WithWebHostBuilder ini digunakan untuk mendapatkan pembangun host. Ini dapat dilihat dalam pengujian berikut:
- Dapatkan_LayananKutipan_MenyediakanKutipanDiHalaman
- Get_GithubProfilePageCanGetAGithubUser
- Dapatkan_HalamanAmanDikembalikanUntukPenggunaTerautentikasi
Sampel SUT menyertakan layanan terlingkup yang mengembalikan kuotasi. Kutipan tersebut disematkan di bidang tersembunyi pada halaman Indeks ketika diminta.
Services/IQuoteService.cs
:
public interface IQuoteService
{
Task<string> GenerateQuote();
}
Services/QuoteService.cs
:
// Quote ©1975 BBC: The Doctor (Tom Baker); Dr. Who: Planet of Evil
// https://www.bbc.co.uk/programmes/p00pyrx6
public class QuoteService : IQuoteService
{
public Task<string> GenerateQuote()
{
return Task.FromResult<string>(
"Come on, Sarah. We've an appointment in London, " +
"and we're already 30,000 years late.");
}
}
Program.cs
:
services.AddScoped<IQuoteService, QuoteService>();
Pages/Index.cshtml.cs
:
public class IndexModel : PageModel
{
private readonly ApplicationDbContext _db;
private readonly IQuoteService _quoteService;
public IndexModel(ApplicationDbContext db, IQuoteService quoteService)
{
_db = db;
_quoteService = quoteService;
}
[BindProperty]
public Message Message { get; set; }
public IList<Message> Messages { get; private set; }
[TempData]
public string MessageAnalysisResult { get; set; }
public string Quote { get; private set; }
public async Task OnGetAsync()
{
Messages = await _db.GetMessagesAsync();
Quote = await _quoteService.GenerateQuote();
}
Pages/Index.cs
:
<input id="quote" type="hidden" value="@Model.Quote">
Markup berikut dihasilkan saat aplikasi SUT dijalankan:
<input id="quote" type="hidden" value="Come on, Sarah. We've an appointment in
London, and we're already 30,000 years late.">
Untuk menguji layanan dan menyuntikkan mock service dalam pengujian integrasi, layanan tiruan disuntikkan ke SUT oleh tes. Layanan tiruan mengganti aplikasi QuoteService
dengan layanan yang disediakan oleh aplikasi pengujian, yang disebut TestQuoteService
:
IntegrationTests.IndexPageTests.cs
:
// Quote ©1975 BBC: The Doctor (Tom Baker); Pyramids of Mars
// https://www.bbc.co.uk/programmes/p00pys55
public class TestQuoteService : IQuoteService
{
public Task<string> GenerateQuote()
{
return Task.FromResult(
"Something's interfering with time, Mr. Scarman, " +
"and time is my business.");
}
}
// Quote ©1975 BBC: The Doctor (Tom Baker); Pyramids of Mars
// https://www.bbc.co.uk/programmes/p00pys55
public class TestQuoteService : IQuoteService
{
public Task<string> GenerateQuote()
{
return Task.FromResult(
"Something's interfering with time, Mr. Scarman, " +
"and time is my business.");
}
}
// Quote ©1975 BBC: The Doctor (Tom Baker); Pyramids of Mars
// https://www.bbc.co.uk/programmes/p00pys55
public class TestQuoteService : IQuoteService
{
public Task<string> GenerateQuote()
{
return Task.FromResult(
"Something's interfering with time, Mr. Scarman, " +
"and time is my business.");
}
}
ConfigureTestServices
dipanggil, dan layanan terlingkup terdaftar:
[Fact]
public async Task Get_QuoteService_ProvidesQuoteInPage()
{
// Arrange
var client = _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.AddScoped<IQuoteService, TestQuoteService>();
});
})
.CreateClient();
//Act
var defaultPage = await client.GetAsync("/");
var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
var quoteElement = content.QuerySelector("#quote");
// Assert
Assert.Equal("Something's interfering with time, Mr. Scarman, " +
"and time is my business.", quoteElement.Attributes["value"].Value);
}
[TestMethod]
public async Task Get_QuoteService_ProvidesQuoteInPage()
{
// Arrange
var client = _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.AddScoped<IQuoteService, TestQuoteService>();
});
})
.CreateClient();
//Act
var defaultPage = await client.GetAsync("/");
var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
var quoteElement = content.QuerySelector("#quote");
// Assert
Assert.AreEqual("Something's interfering with time, Mr. Scarman, " +
"and time is my business.", quoteElement.Attributes["value"].Value);
}
[Test]
public async Task Get_QuoteService_ProvidesQuoteInPage()
{
// Arrange
var client = _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.AddScoped<IQuoteService, TestQuoteService>();
});
})
.CreateClient();
//Act
var defaultPage = await client.GetAsync("/");
var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
var quoteElement = content.QuerySelector("#quote");
// Assert
Assert.That(quoteElement.Attributes["value"].Value, Is.EqualTo(
"Something's interfering with time, Mr. Scarman, " +
"and time is my business."));
}
Markup yang dihasilkan selama eksekusi pengujian mencerminkan teks kutipan yang disediakan oleh TestQuoteService
, sehingga pernyataan lolos:
<input id="quote" type="hidden" value="Something's interfering with time,
Mr. Scarman, and time is my business.">
Autentikasi tiruan
Pengujian pada kelas AuthTests
memeriksa bahwa suatu titik akhir aman:
- Mengalihkan pengguna yang tidak diautentikasi ke halaman masuk aplikasi.
- Mengembalikan konten untuk pengguna yang diautentikasi.
Dalam SUT, /SecurePage
halaman menggunakan AuthorizePage konvensi untuk menerapkan AuthorizeFilter pada halaman. Untuk informasi selengkapnya, lihat Razor Konvensi otorisasi halaman.
services.AddRazorPages(options =>
{
options.Conventions.AuthorizePage("/SecurePage");
});
Dalam Get_SecurePageRedirectsAnUnauthenticatedUser
pengujian, WebApplicationFactoryClientOptions diatur untuk melarang pengalihan dengan menetapkan AllowAutoRedirect ke false
:
[Fact]
public async Task Get_SecurePageRedirectsAnUnauthenticatedUser()
{
// Arrange
var client = _factory.CreateClient(
new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false
});
// Act
var response = await client.GetAsync("/SecurePage");
// Assert
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
Assert.StartsWith("http://localhost/Identity/Account/Login",
response.Headers.Location.OriginalString);
}
[TestMethod]
public async Task Get_SecurePageRedirectsAnUnauthenticatedUser()
{
// Arrange
var client = _factory.CreateClient(
new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false
});
// Act
var response = await client.GetAsync("/SecurePage");
// Assert
Assert.AreEqual(HttpStatusCode.Redirect, response.StatusCode);
StringAssert.StartsWith(response.Headers.Location.OriginalString, "http://localhost/Identity/Account/Login");
}
[Test]
public async Task Get_SecurePageRedirectsAnUnauthenticatedUser()
{
// Arrange
var client = _factory.CreateClient(
new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false
});
// Act
var response = await client.GetAsync("/SecurePage");
// Assert
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Redirect));
Assert.That(response.Headers.Location.OriginalString, Does.StartWith("http://localhost/Identity/Account/Login"));
}
Dengan melarang klien untuk mengikuti pengalihan, pemeriksaan berikut dapat dilakukan:
- Kode status yang dikembalikan oleh SUT dapat diperiksa terhadap hasil yang diharapkan HttpStatusCode.Redirect , bukan kode status akhir setelah pengalihan ke halaman masuk, yang akan menjadi HttpStatusCode.OK.
- Nilai header
Location
di header respons diperiksa untuk mengonfirmasi bahwa nilai tersebut dimulai denganhttp://localhost/Identity/Account/Login
, bukan respons halaman akhir masuk, di manaLocation
header tidak hadir.
Aplikasi pengujian dapat meniru AuthenticationHandler<TOptions>ConfigureTestServices untuk menguji aspek autentikasi dan otorisasi. Skenario yang minimal menghasilkan AuthenticateResult.Success.
public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
public TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger, UrlEncoder encoder)
: base(options, logger, encoder)
{
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var claims = new[] { new Claim(ClaimTypes.Name, "Test user") };
var identity = new ClaimsIdentity(claims, "Test");
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, "TestScheme");
var result = AuthenticateResult.Success(ticket);
return Task.FromResult(result);
}
}
public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
public TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger, UrlEncoder encoder)
: base(options, logger, encoder)
{
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var claims = new[] { new Claim(ClaimTypes.Name, "Test user") };
var identity = new ClaimsIdentity(claims, "Test");
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, "TestScheme");
var result = AuthenticateResult.Success(ticket);
return Task.FromResult(result);
}
}
public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
public TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger, UrlEncoder encoder)
: base(options, logger, encoder)
{
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var claims = new[] { new Claim(ClaimTypes.Name, "Test user") };
var identity = new ClaimsIdentity(claims, "Test");
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, "TestScheme");
var result = AuthenticateResult.Success(ticket);
return Task.FromResult(result);
}
}
TestAuthHandler
dipanggil untuk mengautentikasi pengguna ketika skema autentikasi diatur ke TestScheme
di mana AddAuthentication
terdaftar untuk ConfigureTestServices
. Penting agar skema TestScheme
sesuai dengan skema yang diharapkan oleh aplikasi Anda. Jika tidak, autentikasi tidak akan berfungsi.
[Fact]
public async Task Get_SecurePageIsReturnedForAnAuthenticatedUser()
{
// Arrange
var client = _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.AddAuthentication(defaultScheme: "TestScheme")
.AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(
"TestScheme", options => { });
});
})
.CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false,
});
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue(scheme: "TestScheme");
//Act
var response = await client.GetAsync("/SecurePage");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
[TestMethod]
public async Task Get_SecurePageIsReturnedForAnAuthenticatedUser()
{
// Arrange
var client = _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.AddAuthentication(defaultScheme: "TestScheme")
.AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(
"TestScheme", options => { });
});
})
.CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false,
});
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue(scheme: "TestScheme");
//Act
var response = await client.GetAsync("/SecurePage");
// Assert
Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
}
[Test]
public async Task Get_SecurePageIsReturnedForAnAuthenticatedUser()
{
// Arrange
var client = _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.AddAuthentication(defaultScheme: "TestScheme")
.AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(
"TestScheme", options => { });
});
})
.CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false,
});
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue(scheme: "TestScheme");
//Act
var response = await client.GetAsync("/SecurePage");
// Assert
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
}
Untuk informasi selengkapnya tentang WebApplicationFactoryClientOptions
, lihat bagian Opsi klien.
Pengujian dasar untuk middleware autentikasi
Lihat repositori GitHub ini untuk pengujian dasar middleware autentikasi. Ini berisi server pengujian yang khusus untuk skenario pengujian.
Mengatur lingkungan
Atur lingkungan di pabrik aplikasi kustom:
public class CustomWebApplicationFactory<TProgram>
: WebApplicationFactory<TProgram> where TProgram : class
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
var dbContextDescriptor = services.SingleOrDefault(
d => d.ServiceType ==
typeof(IDbContextOptionsConfiguration<ApplicationDbContext>));
services.Remove(dbContextDescriptor);
var dbConnectionDescriptor = services.SingleOrDefault(
d => d.ServiceType ==
typeof(DbConnection));
services.Remove(dbConnectionDescriptor);
// Create open SqliteConnection so EF won't automatically close it.
services.AddSingleton<DbConnection>(container =>
{
var connection = new SqliteConnection("DataSource=:memory:");
connection.Open();
return connection;
});
services.AddDbContext<ApplicationDbContext>((container, options) =>
{
var connection = container.GetRequiredService<DbConnection>();
options.UseSqlite(connection);
});
});
builder.UseEnvironment("Development");
}
}
public class CustomWebApplicationFactory<TProgram>
: WebApplicationFactory<TProgram> where TProgram : class
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
var dbContextDescriptor = services.SingleOrDefault(
d => d.ServiceType ==
typeof(IDbContextOptionsConfiguration<ApplicationDbContext>));
services.Remove(dbContextDescriptor);
var dbConnectionDescriptor = services.SingleOrDefault(
d => d.ServiceType ==
typeof(DbConnection));
services.Remove(dbConnectionDescriptor);
// Create open SqliteConnection so EF won't automatically close it.
services.AddSingleton<DbConnection>(container =>
{
var connection = new SqliteConnection("DataSource=:memory:");
connection.Open();
return connection;
});
services.AddDbContext<ApplicationDbContext>((container, options) =>
{
var connection = container.GetRequiredService<DbConnection>();
options.UseSqlite(connection);
});
});
builder.UseEnvironment("Development");
}
}
public class CustomWebApplicationFactory<TProgram>
: WebApplicationFactory<TProgram> where TProgram : class
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
var dbContextDescriptor = services.SingleOrDefault(
d => d.ServiceType ==
typeof(IDbContextOptionsConfiguration<ApplicationDbContext>));
services.Remove(dbContextDescriptor);
var dbConnectionDescriptor = services.SingleOrDefault(
d => d.ServiceType ==
typeof(DbConnection));
services.Remove(dbConnectionDescriptor);
// Create open SqliteConnection so EF won't automatically close it.
services.AddSingleton<DbConnection>(container =>
{
var connection = new SqliteConnection("DataSource=:memory:");
connection.Open();
return connection;
});
services.AddDbContext<ApplicationDbContext>((container, options) =>
{
var connection = container.GetRequiredService<DbConnection>();
options.UseSqlite(connection);
});
});
builder.UseEnvironment("Development");
}
}
Cara infrastruktur pengujian menyimpulkan jalur akar konten aplikasi
WebApplicationFactory
Konstruktor menyimpulkan jalur akar konten aplikasi dengan mencari pada rakitan yang WebApplicationFactoryContentRootAttribute berisi pengujian integrasi dengan kunci yang sama dengan TEntryPoint
rakitan System.Reflection.Assembly.FullName
. Jika atribut dengan kunci yang benar tidak ditemukan, WebApplicationFactory
kembali mencari file solusi (.sln) dan menambahkan nama perakitan TEntryPoint
ke direktori solusi. Direktori akar aplikasi (jalur akar konten) digunakan untuk menemukan tampilan dan file konten.
Menonaktifkan penyalinan bayangan
Penyalinan bayangan menyebabkan pengujian dijalankan di direktori yang berbeda dari direktori output. Jika pengujian Anda mengandalkan pemuatan file yang relatif terhadap Assembly.Location
dan Anda mengalami masalah, Anda mungkin harus menonaktifkan penyalinan bayangan.
Untuk menonaktifkan penyalinan bayangan saat menggunakan xUnit, buat xunit.runner.json
file di direktori proyek pengujian Anda, dengan pengaturan konfigurasi yang benar:
{
"shadowCopy": false
}
Pembuangan objek
Setelah pengujian IClassFixture
implementasi dijalankan, TestServer dan HttpClient dibuang ketika xUnit membuang WebApplicationFactory
. Jika objek yang dibuat oleh pengembang memerlukan pembuangan, buang dalam implementasi IClassFixture
. Untuk informasi selengkapnya, lihat Menerapkan metode Dispose.
Setelah pengujian TestClass
dijalankan, TestServer dan HttpClient dibuang ketika MSTest membuang WebApplicationFactory
di metode ClassCleanup
. Jika objek yang dibuat oleh pengembang memerlukan pembuangan, lakukan pembuangan tersebut di dalam metode ClassCleanup
. Untuk informasi selengkapnya, lihat Menerapkan metode Dispose.
Setelah pengujian kelas uji dijalankan, TestServer dan HttpClient dibuang ketika NUnit membuang WebApplicationFactory
dalam metode TearDown
. Jika objek yang dibuat oleh pengembang memerlukan pembuangan, lakukan pembuangan tersebut di dalam metode TearDown
. Untuk informasi selengkapnya, lihat Menerapkan metode Dispose.
Contoh pengujian integrasi
Aplikasi sampel terdiri dari dua aplikasi:
Aplikasi | Direktori proyek | Deskripsi |
---|---|---|
Aplikasi pesan (SUT) | src/RazorPagesProject |
Memungkinkan pengguna untuk menambahkan, menghapus satu, menghapus semua, dan menganalisis pesan. |
Menguji aplikasi | tests/RazorPagesProject.Tests |
Digunakan untuk menguji integrasi SUT. |
Pengujian dapat dijalankan menggunakan fitur pengujian bawaan IDE, seperti Visual Studio. Jika menggunakan Visual Studio Code atau baris perintah, jalankan perintah berikut pada prompt perintah di tests/RazorPagesProject.Tests
direktori:
dotnet test
Organisasi aplikasi pesan (SUT)
SUT adalah Razor sistem pesan Pages dengan karakteristik berikut:
- Halaman Indeks aplikasi (
Pages/Index.cshtml
danPages/Index.cshtml.cs
) menyediakan metode UI dan model halaman untuk mengontrol penambahan, penghapusan, dan analisis pesan (kata rata-rata per pesan). - Pesan dijelaskan oleh
Message
kelas (Data/Message.cs
) dengan dua properti:Id
(kunci) danText
(pesan). PropertiText
diperlukan dan dibatasi hingga 200 karakter. - Pesan disimpan menggunakan database dalam memori Entity Framework†.
- Aplikasi ini berisi lapisan akses data (DAL) di kelas konteks databasenya,
AppDbContext
(Data/AppDbContext.cs
). - Jika database kosong pada startup aplikasi, penyimpanan pesan diinisialisasi dengan tiga pesan.
- Aplikasi ini menyertakan
/SecurePage
yang hanya dapat diakses oleh pengguna yang diautentikasi.
† Artikel EF, Uji dengan InMemory, menjelaskan cara menggunakan database dalam memori untuk pengujian dengan MSTest. Topik ini menggunakan kerangka kerja pengujian xUnit . Konsep pengujian dan implementasi pengujian di berbagai kerangka kerja pengujian serupa tetapi tidak identik.
Meskipun aplikasi tidak menggunakan pola repositori dan bukan contoh pola Unit kerja (UoW) yang efektif, Razor Pages mendukung pola pengembangan ini. Untuk informasi selengkapnya, lihat Merancang lapisan persistensi infrastruktur dan Logika pengontrol Pengujian (sampel mengimplementasikan pola repositori).
Menguji organisasi aplikasi
Aplikasi pengujian adalah aplikasi konsol di direktori tests/RazorPagesProject.Tests
.
Menguji direktori aplikasi | Deskripsi |
---|---|
AuthTests |
Berisi metode pengujian untuk:
|
BasicTests |
Berisi metode pengujian untuk perutean dan jenis konten. |
IntegrationTests |
Berisi pengujian integrasi untuk halaman Indeks menggunakan kelas kustom WebApplicationFactory . |
Helpers/Utilities |
|
Kerangka kerja pengujian adalah xUnit. Pengujian integrasi dilakukan menggunakan Microsoft.AspNetCore.TestHost, yang mencakup TestServer. Karena paket Microsoft.AspNetCore.Mvc.Testing
digunakan untuk mengonfigurasi host dan server pengujian, paket TestHost
dan TestServer
tidak memerlukan referensi paket langsung dalam file proyek aplikasi pengujian atau konfigurasi pengembang di aplikasi pengujian.
Pengujian integrasi biasanya memerlukan himpunan data kecil dalam database sebelum eksekusi pengujian. Misalnya, pengujian penghapusan memerlukan penghapusan catatan dalam database, sehingga database harus memiliki setidaknya satu catatan agar penghapusan berhasil dilakukan.
Aplikasi contoh memuat database dengan tiga pesan dalam Utilities.cs
yang dapat digunakan dalam pengujian saat dijalankan.
public static void InitializeDbForTests(ApplicationDbContext db)
{
db.Messages.AddRange(GetSeedingMessages());
db.SaveChanges();
}
public static void ReinitializeDbForTests(ApplicationDbContext db)
{
db.Messages.RemoveRange(db.Messages);
InitializeDbForTests(db);
}
public static List<Message> GetSeedingMessages()
{
return new List<Message>()
{
new Message(){ Text = "TEST RECORD: You're standing on my scarf." },
new Message(){ Text = "TEST RECORD: Would you like a jelly baby?" },
new Message(){ Text = "TEST RECORD: To the rational mind, " +
"nothing is inexplicable; only unexplained." }
};
}
Konteks database SUT terdaftar di Program.cs
. Panggilan balik builder.ConfigureServices
dari aplikasi pengujian dijalankan setelah kode dari aplikasi Program.cs
dijalankan. Untuk menggunakan database yang berbeda untuk pengujian, konteks database aplikasi harus diganti di builder.ConfigureServices
. Untuk informasi selengkapnya, lihat bagian Kustomisasi WebApplicationFactory .
Sumber Daya Tambahan:
ASP.NET Core