[{"data":1,"prerenderedAt":1197},["ShallowReactive",2],{"blog-\u002Fblog\u002Fdotnet-integration-testing":3},{"id":4,"title":5,"body":6,"description":1183,"difficulty":1184,"extension":1185,"framework":1186,"frameworkSlug":130,"meta":1187,"navigation":176,"order":161,"path":1188,"qaPath":1189,"seo":1190,"stem":1191,"subtopic":1192,"topic":1193,"topicSlug":1194,"updated":1195,"__hash__":1196},"blog\u002Fblog\u002Fdotnet-integration-testing.md","Integration Testing in ASP.NET Core with WebApplicationFactory",{"type":7,"value":8,"toc":1167},"minimark",[9,14,18,21,25,91,94,98,113,141,237,243,247,250,371,374,378,381,386,394,398,401,480,483,487,490,554,557,561,564,745,748,752,759,872,883,887,894,999,1002,1006,1013,1076,1083,1087,1095,1142,1145,1163],[10,11,13],"h2",{"id":12},"why-integration-testing-matters","Why integration testing matters",[15,16,17],"p",{},"Unit tests verify logic in isolation. But they cannot catch a misconfigured\nmiddleware pipeline, a mismatched JSON serializer, an EF Core mapping error,\nor an authorization policy that doesn't enforce what you think it does.\nIntegration tests fill this gap by exercising the full stack — HTTP request\nin, HTTP response out — in a fast, in-process server with no Docker or network\nrequired.",[15,19,20],{},"Interviewers ask about integration testing to assess whether a candidate\ndistinguishes it from unit testing, knows how to bootstrap a test server, and\nunderstands the tradeoffs between speed and fidelity.",[10,22,24],{"id":23},"unit-testing-vs-integration-testing","Unit testing vs integration testing",[26,27,28,43],"table",{},[29,30,31],"thead",{},[32,33,34,37,40],"tr",{},[35,36],"th",{},[35,38,39],{},"Unit test",[35,41,42],{},"Integration test",[44,45,46,58,69,80],"tbody",{},[32,47,48,52,55],{},[49,50,51],"td",{},"Scope",[49,53,54],{},"One class",[49,56,57],{},"Multiple layers (HTTP → service → DB)",[32,59,60,63,66],{},[49,61,62],{},"Speed",[49,64,65],{},"Milliseconds",[49,67,68],{},"Seconds",[32,70,71,74,77],{},[49,72,73],{},"Isolation",[49,75,76],{},"Full (mocked deps)",[49,78,79],{},"Partial (real pipeline, fake DB)",[32,81,82,85,88],{},[49,83,84],{},"What it catches",[49,86,87],{},"Logic bugs",[49,89,90],{},"Config bugs, serialization, middleware, policies",[15,92,93],{},"You need both. A well-tested codebase has many unit tests and a smaller number\nof high-value integration tests that cover the critical paths end-to-end.",[10,95,97],{"id":96},"webapplicationfactory-the-net-integration-test-host","WebApplicationFactory: the .NET integration test host",[15,99,100,104,105,108,109,112],{},[101,102,103],"code",{},"WebApplicationFactory\u003CTProgram>"," from ",[101,106,107],{},"Microsoft.AspNetCore.Mvc.Testing"," spins\nup the real ASP.NET Core pipeline in-process. You get a real ",[101,110,111],{},"HttpClient",", real\nmiddleware, real DI — but no network overhead.",[114,115,120],"pre",{"className":116,"code":117,"language":118,"meta":119,"style":119},"language-bash shiki shiki-themes github-light github-dark","dotnet add package Microsoft.AspNetCore.Mvc.Testing\n","bash","",[101,121,122],{"__ignoreMap":119},[123,124,127,131,135,138],"span",{"class":125,"line":126},"line",1,[123,128,130],{"class":129},"sScJk","dotnet",[123,132,134],{"class":133},"sZZnC"," add",[123,136,137],{"class":133}," package",[123,139,140],{"class":133}," Microsoft.AspNetCore.Mvc.Testing\n",[114,142,146],{"className":143,"code":144,"language":145,"meta":119,"style":119},"language-csharp shiki shiki-themes github-light github-dark","\u002F\u002F Minimal setup — use the app with its real configuration:\npublic class HealthTests : IClassFixture\u003CWebApplicationFactory\u003CProgram>>\n{\n    private readonly HttpClient _client;\n\n    public HealthTests(WebApplicationFactory\u003CProgram> factory)\n        => _client = factory.CreateClient();\n\n    [Fact]\n    public async Task GetHealth_Returns200()\n    {\n        var response = await _client.GetAsync(\"\u002Fhealth\");\n        response.EnsureSuccessStatusCode();\n    }\n}\n","csharp",[101,147,148,153,159,165,171,178,184,190,195,201,207,213,219,225,231],{"__ignoreMap":119},[123,149,150],{"class":125,"line":126},[123,151,152],{},"\u002F\u002F Minimal setup — use the app with its real configuration:\n",[123,154,156],{"class":125,"line":155},2,[123,157,158],{},"public class HealthTests : IClassFixture\u003CWebApplicationFactory\u003CProgram>>\n",[123,160,162],{"class":125,"line":161},3,[123,163,164],{},"{\n",[123,166,168],{"class":125,"line":167},4,[123,169,170],{},"    private readonly HttpClient _client;\n",[123,172,174],{"class":125,"line":173},5,[123,175,177],{"emptyLinePlaceholder":176},true,"\n",[123,179,181],{"class":125,"line":180},6,[123,182,183],{},"    public HealthTests(WebApplicationFactory\u003CProgram> factory)\n",[123,185,187],{"class":125,"line":186},7,[123,188,189],{},"        => _client = factory.CreateClient();\n",[123,191,193],{"class":125,"line":192},8,[123,194,177],{"emptyLinePlaceholder":176},[123,196,198],{"class":125,"line":197},9,[123,199,200],{},"    [Fact]\n",[123,202,204],{"class":125,"line":203},10,[123,205,206],{},"    public async Task GetHealth_Returns200()\n",[123,208,210],{"class":125,"line":209},11,[123,211,212],{},"    {\n",[123,214,216],{"class":125,"line":215},12,[123,217,218],{},"        var response = await _client.GetAsync(\"\u002Fhealth\");\n",[123,220,222],{"class":125,"line":221},13,[123,223,224],{},"        response.EnsureSuccessStatusCode();\n",[123,226,228],{"class":125,"line":227},14,[123,229,230],{},"    }\n",[123,232,234],{"class":125,"line":233},15,[123,235,236],{},"}\n",[15,238,239,242],{},[101,240,241],{},"IClassFixture\u003CWebApplicationFactory\u003CProgram>>"," creates one factory per test\nclass. The factory is shared across all tests in the class, making it fast.",[10,244,246],{"id":245},"custom-factories-swapping-services-for-tests","Custom factories: swapping services for tests",[15,248,249],{},"For integration tests you usually want to replace the real database with an\nin-memory one and swap real external services with fakes:",[114,251,253],{"className":143,"code":252,"language":145,"meta":119,"style":119},"public class TestWebFactory : WebApplicationFactory\u003CProgram>\n{\n    protected override void ConfigureWebHost(IWebHostBuilder builder)\n    {\n        builder.ConfigureServices(services =>\n        {\n            \u002F\u002F Remove the real DbContext registration:\n            var descriptor = services.SingleOrDefault(\n                d => d.ServiceType == typeof(DbContextOptions\u003CAppDbContext>));\n            if (descriptor != null) services.Remove(descriptor);\n\n            \u002F\u002F Add an in-memory database with a unique name per test run:\n            services.AddDbContext\u003CAppDbContext>(opts =>\n                opts.UseInMemoryDatabase(\"TestDb-\" + Guid.NewGuid()));\n\n            \u002F\u002F Replace real external services with fakes:\n            services.AddSingleton\u003CIEmailSender, FakeEmailSender>();\n            services.AddSingleton\u003CIPaymentGateway, FakePaymentGateway>();\n        });\n\n        builder.UseEnvironment(\"Testing\");\n    }\n}\n",[101,254,255,260,264,269,273,278,283,288,293,298,303,307,312,317,322,326,332,338,344,350,355,361,366],{"__ignoreMap":119},[123,256,257],{"class":125,"line":126},[123,258,259],{},"public class TestWebFactory : WebApplicationFactory\u003CProgram>\n",[123,261,262],{"class":125,"line":155},[123,263,164],{},[123,265,266],{"class":125,"line":161},[123,267,268],{},"    protected override void ConfigureWebHost(IWebHostBuilder builder)\n",[123,270,271],{"class":125,"line":167},[123,272,212],{},[123,274,275],{"class":125,"line":173},[123,276,277],{},"        builder.ConfigureServices(services =>\n",[123,279,280],{"class":125,"line":180},[123,281,282],{},"        {\n",[123,284,285],{"class":125,"line":186},[123,286,287],{},"            \u002F\u002F Remove the real DbContext registration:\n",[123,289,290],{"class":125,"line":192},[123,291,292],{},"            var descriptor = services.SingleOrDefault(\n",[123,294,295],{"class":125,"line":197},[123,296,297],{},"                d => d.ServiceType == typeof(DbContextOptions\u003CAppDbContext>));\n",[123,299,300],{"class":125,"line":203},[123,301,302],{},"            if (descriptor != null) services.Remove(descriptor);\n",[123,304,305],{"class":125,"line":209},[123,306,177],{"emptyLinePlaceholder":176},[123,308,309],{"class":125,"line":215},[123,310,311],{},"            \u002F\u002F Add an in-memory database with a unique name per test run:\n",[123,313,314],{"class":125,"line":221},[123,315,316],{},"            services.AddDbContext\u003CAppDbContext>(opts =>\n",[123,318,319],{"class":125,"line":227},[123,320,321],{},"                opts.UseInMemoryDatabase(\"TestDb-\" + Guid.NewGuid()));\n",[123,323,324],{"class":125,"line":233},[123,325,177],{"emptyLinePlaceholder":176},[123,327,329],{"class":125,"line":328},16,[123,330,331],{},"            \u002F\u002F Replace real external services with fakes:\n",[123,333,335],{"class":125,"line":334},17,[123,336,337],{},"            services.AddSingleton\u003CIEmailSender, FakeEmailSender>();\n",[123,339,341],{"class":125,"line":340},18,[123,342,343],{},"            services.AddSingleton\u003CIPaymentGateway, FakePaymentGateway>();\n",[123,345,347],{"class":125,"line":346},19,[123,348,349],{},"        });\n",[123,351,353],{"class":125,"line":352},20,[123,354,177],{"emptyLinePlaceholder":176},[123,356,358],{"class":125,"line":357},21,[123,359,360],{},"        builder.UseEnvironment(\"Testing\");\n",[123,362,364],{"class":125,"line":363},22,[123,365,230],{},[123,367,369],{"class":125,"line":368},23,[123,370,236],{},[15,372,373],{},"Tests then inherit from this factory and get a pre-configured test server with\nno real database or email calls.",[10,375,377],{"id":376},"in-memory-database-vs-sqlite-vs-real-database","In-memory database vs SQLite vs real database",[15,379,380],{},"Three options for the test database, each at a different speed\u002Ffidelity tradeoff:",[382,383,385],"h3",{"id":384},"ef-core-inmemory-provider","EF Core InMemory provider",[15,387,388,389,393],{},"Fast, zero setup, but does ",[390,391,392],"strong",{},"not"," enforce foreign keys, unique indexes, or\nrelational constraints. Use only when you are testing service logic that happens\nto call EF, not when you care about database behavior.",[382,395,397],{"id":396},"sqlite-in-memory","SQLite in-memory",[15,399,400],{},"Runs real SQL, enforces constraints, supports migrations. Almost as fast as\nthe InMemory provider. The gotcha: in-memory SQLite databases are per-connection,\nso you must keep the connection alive for the test's duration.",[114,402,404],{"className":143,"code":403,"language":145,"meta":119,"style":119},"public class SqliteFixture : IDisposable\n{\n    public SqliteConnection Connection { get; } = new(\"Data Source=:memory:\");\n    public AppDbContext     Context    { get; }\n\n    public SqliteFixture()\n    {\n        Connection.Open();\n        var opts = new DbContextOptionsBuilder\u003CAppDbContext>()\n            .UseSqlite(Connection).Options;\n        Context = new AppDbContext(opts);\n        Context.Database.EnsureCreated();\n    }\n\n    public void Dispose() { Context.Dispose(); Connection.Close(); }\n}\n",[101,405,406,411,415,420,425,429,434,438,443,448,453,458,463,467,471,476],{"__ignoreMap":119},[123,407,408],{"class":125,"line":126},[123,409,410],{},"public class SqliteFixture : IDisposable\n",[123,412,413],{"class":125,"line":155},[123,414,164],{},[123,416,417],{"class":125,"line":161},[123,418,419],{},"    public SqliteConnection Connection { get; } = new(\"Data Source=:memory:\");\n",[123,421,422],{"class":125,"line":167},[123,423,424],{},"    public AppDbContext     Context    { get; }\n",[123,426,427],{"class":125,"line":173},[123,428,177],{"emptyLinePlaceholder":176},[123,430,431],{"class":125,"line":180},[123,432,433],{},"    public SqliteFixture()\n",[123,435,436],{"class":125,"line":186},[123,437,212],{},[123,439,440],{"class":125,"line":192},[123,441,442],{},"        Connection.Open();\n",[123,444,445],{"class":125,"line":197},[123,446,447],{},"        var opts = new DbContextOptionsBuilder\u003CAppDbContext>()\n",[123,449,450],{"class":125,"line":203},[123,451,452],{},"            .UseSqlite(Connection).Options;\n",[123,454,455],{"class":125,"line":209},[123,456,457],{},"        Context = new AppDbContext(opts);\n",[123,459,460],{"class":125,"line":215},[123,461,462],{},"        Context.Database.EnsureCreated();\n",[123,464,465],{"class":125,"line":221},[123,466,230],{},[123,468,469],{"class":125,"line":227},[123,470,177],{"emptyLinePlaceholder":176},[123,472,473],{"class":125,"line":233},[123,474,475],{},"    public void Dispose() { Context.Dispose(); Connection.Close(); }\n",[123,477,478],{"class":125,"line":328},[123,479,236],{},[15,481,482],{},"SQLite in-memory is the right default for most integration test suites.",[382,484,486],{"id":485},"testcontainers-real-sql-server-or-postgres","Testcontainers (real SQL Server or Postgres)",[15,488,489],{},"For tests that exercise database-specific behavior — JSON columns, full-text\nsearch, stored procedures, EF migrations — use Testcontainers to start a real\nDocker container:",[114,491,493],{"className":143,"code":492,"language":145,"meta":119,"style":119},"\u002F\u002F dotnet add package Testcontainers.MsSql\npublic class SqlServerTests : IAsyncLifetime\n{\n    private readonly MsSqlContainer _sql = new MsSqlBuilder().Build();\n\n    public async Task InitializeAsync()\n    {\n        await _sql.StartAsync();\n        \u002F\u002F run real migrations against the container\n    }\n\n    public async Task DisposeAsync() => await _sql.DisposeAsync();\n}\n",[101,494,495,500,505,509,514,518,523,527,532,537,541,545,550],{"__ignoreMap":119},[123,496,497],{"class":125,"line":126},[123,498,499],{},"\u002F\u002F dotnet add package Testcontainers.MsSql\n",[123,501,502],{"class":125,"line":155},[123,503,504],{},"public class SqlServerTests : IAsyncLifetime\n",[123,506,507],{"class":125,"line":161},[123,508,164],{},[123,510,511],{"class":125,"line":167},[123,512,513],{},"    private readonly MsSqlContainer _sql = new MsSqlBuilder().Build();\n",[123,515,516],{"class":125,"line":173},[123,517,177],{"emptyLinePlaceholder":176},[123,519,520],{"class":125,"line":180},[123,521,522],{},"    public async Task InitializeAsync()\n",[123,524,525],{"class":125,"line":186},[123,526,212],{},[123,528,529],{"class":125,"line":192},[123,530,531],{},"        await _sql.StartAsync();\n",[123,533,534],{"class":125,"line":197},[123,535,536],{},"        \u002F\u002F run real migrations against the container\n",[123,538,539],{"class":125,"line":203},[123,540,230],{},[123,542,543],{"class":125,"line":209},[123,544,177],{"emptyLinePlaceholder":176},[123,546,547],{"class":125,"line":215},[123,548,549],{},"    public async Task DisposeAsync() => await _sql.DisposeAsync();\n",[123,551,552],{"class":125,"line":221},[123,553,236],{},[15,555,556],{},"Testcontainers requires Docker and adds ~5–15 seconds startup. Reserve them for\ntests that truly need production-equivalent database behavior.",[10,558,560],{"id":559},"seeding-test-data","Seeding test data",[15,562,563],{},"Never rely on data from a previous test. Seed exactly the data each test needs:",[114,565,567],{"className":143,"code":566,"language":145,"meta":119,"style":119},"public class ProductTests : IClassFixture\u003CTestWebFactory>, IAsyncLifetime\n{\n    private readonly HttpClient   _client;\n    private readonly AppDbContext _db;\n\n    public ProductTests(TestWebFactory factory)\n    {\n        _client = factory.CreateClient();\n        var scope = factory.Services.CreateScope();\n        _db = scope.ServiceProvider.GetRequiredService\u003CAppDbContext>();\n    }\n\n    public async Task InitializeAsync()\n    {\n        await _db.Database.EnsureCreatedAsync();\n        _db.Products.AddRange(\n            new Product { Id = 1, Name = \"Widget\",  Price = 9.99m },\n            new Product { Id = 2, Name = \"Gadget\",  Price = 24.99m }\n        );\n        await _db.SaveChangesAsync();\n    }\n\n    public async Task DisposeAsync()\n    {\n        _db.Products.RemoveRange(_db.Products);\n        await _db.SaveChangesAsync();\n    }\n\n    [Fact]\n    public async Task GetProducts_ReturnsAllSeeded()\n    {\n        var response = await _client.GetAsync(\"\u002Fapi\u002Fproducts\");\n        var products = await response.Content.ReadFromJsonAsync\u003CList\u003CProduct>>();\n        Assert.Equal(2, products!.Count);\n    }\n}\n",[101,568,569,574,578,583,588,592,597,601,606,611,616,620,624,628,632,637,642,647,652,657,662,666,670,675,680,686,691,696,701,706,712,717,723,729,735,740],{"__ignoreMap":119},[123,570,571],{"class":125,"line":126},[123,572,573],{},"public class ProductTests : IClassFixture\u003CTestWebFactory>, IAsyncLifetime\n",[123,575,576],{"class":125,"line":155},[123,577,164],{},[123,579,580],{"class":125,"line":161},[123,581,582],{},"    private readonly HttpClient   _client;\n",[123,584,585],{"class":125,"line":167},[123,586,587],{},"    private readonly AppDbContext _db;\n",[123,589,590],{"class":125,"line":173},[123,591,177],{"emptyLinePlaceholder":176},[123,593,594],{"class":125,"line":180},[123,595,596],{},"    public ProductTests(TestWebFactory factory)\n",[123,598,599],{"class":125,"line":186},[123,600,212],{},[123,602,603],{"class":125,"line":192},[123,604,605],{},"        _client = factory.CreateClient();\n",[123,607,608],{"class":125,"line":197},[123,609,610],{},"        var scope = factory.Services.CreateScope();\n",[123,612,613],{"class":125,"line":203},[123,614,615],{},"        _db = scope.ServiceProvider.GetRequiredService\u003CAppDbContext>();\n",[123,617,618],{"class":125,"line":209},[123,619,230],{},[123,621,622],{"class":125,"line":215},[123,623,177],{"emptyLinePlaceholder":176},[123,625,626],{"class":125,"line":221},[123,627,522],{},[123,629,630],{"class":125,"line":227},[123,631,212],{},[123,633,634],{"class":125,"line":233},[123,635,636],{},"        await _db.Database.EnsureCreatedAsync();\n",[123,638,639],{"class":125,"line":328},[123,640,641],{},"        _db.Products.AddRange(\n",[123,643,644],{"class":125,"line":334},[123,645,646],{},"            new Product { Id = 1, Name = \"Widget\",  Price = 9.99m },\n",[123,648,649],{"class":125,"line":340},[123,650,651],{},"            new Product { Id = 2, Name = \"Gadget\",  Price = 24.99m }\n",[123,653,654],{"class":125,"line":346},[123,655,656],{},"        );\n",[123,658,659],{"class":125,"line":352},[123,660,661],{},"        await _db.SaveChangesAsync();\n",[123,663,664],{"class":125,"line":357},[123,665,230],{},[123,667,668],{"class":125,"line":363},[123,669,177],{"emptyLinePlaceholder":176},[123,671,672],{"class":125,"line":368},[123,673,674],{},"    public async Task DisposeAsync()\n",[123,676,678],{"class":125,"line":677},24,[123,679,212],{},[123,681,683],{"class":125,"line":682},25,[123,684,685],{},"        _db.Products.RemoveRange(_db.Products);\n",[123,687,689],{"class":125,"line":688},26,[123,690,661],{},[123,692,694],{"class":125,"line":693},27,[123,695,230],{},[123,697,699],{"class":125,"line":698},28,[123,700,177],{"emptyLinePlaceholder":176},[123,702,704],{"class":125,"line":703},29,[123,705,200],{},[123,707,709],{"class":125,"line":708},30,[123,710,711],{},"    public async Task GetProducts_ReturnsAllSeeded()\n",[123,713,715],{"class":125,"line":714},31,[123,716,212],{},[123,718,720],{"class":125,"line":719},32,[123,721,722],{},"        var response = await _client.GetAsync(\"\u002Fapi\u002Fproducts\");\n",[123,724,726],{"class":125,"line":725},33,[123,727,728],{},"        var products = await response.Content.ReadFromJsonAsync\u003CList\u003CProduct>>();\n",[123,730,732],{"class":125,"line":731},34,[123,733,734],{},"        Assert.Equal(2, products!.Count);\n",[123,736,738],{"class":125,"line":737},35,[123,739,230],{},[123,741,743],{"class":125,"line":742},36,[123,744,236],{},[15,746,747],{},"Use deterministic IDs or GUIDs to avoid collisions when tests run in parallel.",[10,749,751],{"id":750},"testing-authenticated-endpoints","Testing authenticated endpoints",[15,753,754,755,758],{},"The cleanest approach is a custom ",[101,756,757],{},"AuthenticationHandler"," that grants a fixed\ntest identity to every request through the test client:",[114,760,762],{"className":143,"code":761,"language":145,"meta":119,"style":119},"public class TestAuthHandler : AuthenticationHandler\u003CAuthenticationSchemeOptions>\n{\n    public TestAuthHandler(\n        IOptionsMonitor\u003CAuthenticationSchemeOptions> options,\n        ILoggerFactory logger, UrlEncoder encoder)\n        : base(options, logger, encoder) { }\n\n    protected override Task\u003CAuthenticateResult> HandleAuthenticateAsync()\n    {\n        var claims = new[]\n        {\n            new Claim(ClaimTypes.Name, \"test-user\"),\n            new Claim(ClaimTypes.Role, \"Admin\"),\n        };\n        var ticket = new AuthenticationTicket(\n            new ClaimsPrincipal(new ClaimsIdentity(claims, \"Test\")), \"Test\");\n        return Task.FromResult(AuthenticateResult.Success(ticket));\n    }\n}\n\n\u002F\u002F Register in TestWebFactory.ConfigureWebHost:\nservices.AddAuthentication(\"Test\")\n        .AddScheme\u003CAuthenticationSchemeOptions, TestAuthHandler>(\"Test\", _ => { });\n",[101,763,764,769,773,778,783,788,793,797,802,806,811,815,820,825,830,835,840,845,849,853,857,862,867],{"__ignoreMap":119},[123,765,766],{"class":125,"line":126},[123,767,768],{},"public class TestAuthHandler : AuthenticationHandler\u003CAuthenticationSchemeOptions>\n",[123,770,771],{"class":125,"line":155},[123,772,164],{},[123,774,775],{"class":125,"line":161},[123,776,777],{},"    public TestAuthHandler(\n",[123,779,780],{"class":125,"line":167},[123,781,782],{},"        IOptionsMonitor\u003CAuthenticationSchemeOptions> options,\n",[123,784,785],{"class":125,"line":173},[123,786,787],{},"        ILoggerFactory logger, UrlEncoder encoder)\n",[123,789,790],{"class":125,"line":180},[123,791,792],{},"        : base(options, logger, encoder) { }\n",[123,794,795],{"class":125,"line":186},[123,796,177],{"emptyLinePlaceholder":176},[123,798,799],{"class":125,"line":192},[123,800,801],{},"    protected override Task\u003CAuthenticateResult> HandleAuthenticateAsync()\n",[123,803,804],{"class":125,"line":197},[123,805,212],{},[123,807,808],{"class":125,"line":203},[123,809,810],{},"        var claims = new[]\n",[123,812,813],{"class":125,"line":209},[123,814,282],{},[123,816,817],{"class":125,"line":215},[123,818,819],{},"            new Claim(ClaimTypes.Name, \"test-user\"),\n",[123,821,822],{"class":125,"line":221},[123,823,824],{},"            new Claim(ClaimTypes.Role, \"Admin\"),\n",[123,826,827],{"class":125,"line":227},[123,828,829],{},"        };\n",[123,831,832],{"class":125,"line":233},[123,833,834],{},"        var ticket = new AuthenticationTicket(\n",[123,836,837],{"class":125,"line":328},[123,838,839],{},"            new ClaimsPrincipal(new ClaimsIdentity(claims, \"Test\")), \"Test\");\n",[123,841,842],{"class":125,"line":334},[123,843,844],{},"        return Task.FromResult(AuthenticateResult.Success(ticket));\n",[123,846,847],{"class":125,"line":340},[123,848,230],{},[123,850,851],{"class":125,"line":346},[123,852,236],{},[123,854,855],{"class":125,"line":352},[123,856,177],{"emptyLinePlaceholder":176},[123,858,859],{"class":125,"line":357},[123,860,861],{},"\u002F\u002F Register in TestWebFactory.ConfigureWebHost:\n",[123,863,864],{"class":125,"line":363},[123,865,866],{},"services.AddAuthentication(\"Test\")\n",[123,868,869],{"class":125,"line":368},[123,870,871],{},"        .AddScheme\u003CAuthenticationSchemeOptions, TestAuthHandler>(\"Test\", _ => { });\n",[15,873,874,875,878,879,882],{},"All requests through ",[101,876,877],{},"_client"," are now authenticated as ",[101,880,881],{},"test-user \u002F Admin",".\nTo test unauthorized access, create a second client with no auth header.",[10,884,886],{"id":885},"testing-middleware-in-isolation","Testing middleware in isolation",[15,888,889,890,893],{},"When you want to test a single middleware component without the full application\npipeline, use ",[101,891,892],{},"TestServer"," directly:",[114,895,897],{"className":143,"code":896,"language":145,"meta":119,"style":119},"[Fact]\npublic async Task ExceptionMiddleware_WhenThrows_Returns500WithProblemDetails()\n{\n    using var host = await new HostBuilder()\n        .ConfigureWebHost(webBuilder =>\n        {\n            webBuilder.UseTestServer();\n            webBuilder.Configure(app =>\n            {\n                app.UseMiddleware\u003CExceptionHandlingMiddleware>();\n                app.Run(_ => throw new InvalidOperationException(\"Boom!\"));\n            });\n        })\n        .StartAsync();\n\n    var response = await host.GetTestClient().GetAsync(\"\u002F\");\n\n    Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);\n    Assert.Equal(\"application\u002Fproblem+json\",\n        response.Content.Headers.ContentType!.MediaType);\n}\n",[101,898,899,904,909,913,918,923,927,932,937,942,947,952,957,962,967,971,976,980,985,990,995],{"__ignoreMap":119},[123,900,901],{"class":125,"line":126},[123,902,903],{},"[Fact]\n",[123,905,906],{"class":125,"line":155},[123,907,908],{},"public async Task ExceptionMiddleware_WhenThrows_Returns500WithProblemDetails()\n",[123,910,911],{"class":125,"line":161},[123,912,164],{},[123,914,915],{"class":125,"line":167},[123,916,917],{},"    using var host = await new HostBuilder()\n",[123,919,920],{"class":125,"line":173},[123,921,922],{},"        .ConfigureWebHost(webBuilder =>\n",[123,924,925],{"class":125,"line":180},[123,926,282],{},[123,928,929],{"class":125,"line":186},[123,930,931],{},"            webBuilder.UseTestServer();\n",[123,933,934],{"class":125,"line":192},[123,935,936],{},"            webBuilder.Configure(app =>\n",[123,938,939],{"class":125,"line":197},[123,940,941],{},"            {\n",[123,943,944],{"class":125,"line":203},[123,945,946],{},"                app.UseMiddleware\u003CExceptionHandlingMiddleware>();\n",[123,948,949],{"class":125,"line":209},[123,950,951],{},"                app.Run(_ => throw new InvalidOperationException(\"Boom!\"));\n",[123,953,954],{"class":125,"line":215},[123,955,956],{},"            });\n",[123,958,959],{"class":125,"line":221},[123,960,961],{},"        })\n",[123,963,964],{"class":125,"line":227},[123,965,966],{},"        .StartAsync();\n",[123,968,969],{"class":125,"line":233},[123,970,177],{"emptyLinePlaceholder":176},[123,972,973],{"class":125,"line":328},[123,974,975],{},"    var response = await host.GetTestClient().GetAsync(\"\u002F\");\n",[123,977,978],{"class":125,"line":334},[123,979,177],{"emptyLinePlaceholder":176},[123,981,982],{"class":125,"line":340},[123,983,984],{},"    Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);\n",[123,986,987],{"class":125,"line":346},[123,988,989],{},"    Assert.Equal(\"application\u002Fproblem+json\",\n",[123,991,992],{"class":125,"line":352},[123,993,994],{},"        response.Content.Headers.ContentType!.MediaType);\n",[123,996,997],{"class":125,"line":357},[123,998,236],{},[15,1000,1001],{},"This lets you test the middleware's error handling behavior without configuring\nrouting, authorization, or any other application concern.",[10,1003,1005],{"id":1004},"asserting-on-problemdetails-error-responses","Asserting on ProblemDetails error responses",[15,1007,1008,1009,1012],{},"ASP.NET Core returns ",[101,1010,1011],{},"ProblemDetails"," (RFC 7807) for validation and error\nresponses. Deserve two assertions: the HTTP status code and the Content-Type:",[114,1014,1016],{"className":143,"code":1015,"language":145,"meta":119,"style":119},"[Fact]\npublic async Task PostOrder_WithMissingSku_Returns400WithProblemDetails()\n{\n    var response = await _client.PostAsync(\"\u002Fapi\u002Forders\",\n        new StringContent(\"\"\"{\"quantity\":2}\"\"\", Encoding.UTF8, \"application\u002Fjson\"));\n\n    Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);\n    Assert.Equal(\"application\u002Fproblem+json\",\n        response.Content.Headers.ContentType!.MediaType);\n\n    var problem = await response.Content.ReadFromJsonAsync\u003CValidationProblemDetails>();\n    Assert.True(problem!.Errors.ContainsKey(\"Sku\"));\n}\n",[101,1017,1018,1022,1027,1031,1036,1041,1045,1050,1054,1058,1062,1067,1072],{"__ignoreMap":119},[123,1019,1020],{"class":125,"line":126},[123,1021,903],{},[123,1023,1024],{"class":125,"line":155},[123,1025,1026],{},"public async Task PostOrder_WithMissingSku_Returns400WithProblemDetails()\n",[123,1028,1029],{"class":125,"line":161},[123,1030,164],{},[123,1032,1033],{"class":125,"line":167},[123,1034,1035],{},"    var response = await _client.PostAsync(\"\u002Fapi\u002Forders\",\n",[123,1037,1038],{"class":125,"line":173},[123,1039,1040],{},"        new StringContent(\"\"\"{\"quantity\":2}\"\"\", Encoding.UTF8, \"application\u002Fjson\"));\n",[123,1042,1043],{"class":125,"line":180},[123,1044,177],{"emptyLinePlaceholder":176},[123,1046,1047],{"class":125,"line":186},[123,1048,1049],{},"    Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);\n",[123,1051,1052],{"class":125,"line":192},[123,1053,989],{},[123,1055,1056],{"class":125,"line":197},[123,1057,994],{},[123,1059,1060],{"class":125,"line":203},[123,1061,177],{"emptyLinePlaceholder":176},[123,1063,1064],{"class":125,"line":209},[123,1065,1066],{},"    var problem = await response.Content.ReadFromJsonAsync\u003CValidationProblemDetails>();\n",[123,1068,1069],{"class":125,"line":215},[123,1070,1071],{},"    Assert.True(problem!.Errors.ContainsKey(\"Sku\"));\n",[123,1073,1074],{"class":125,"line":221},[123,1075,236],{},[15,1077,1078,1079,1082],{},"A 400 returning ",[101,1080,1081],{},"text\u002Fhtml"," means the error middleware is misconfigured even\nthough the status is \"correct.\"",[10,1084,1086],{"id":1085},"parallel-test-execution-and-isolation","Parallel test execution and isolation",[15,1088,1089,1090,1094],{},"xUnit can run test ",[1091,1092,1093],"em",{},"collections"," in parallel. Shared mutable state (a single\nin-memory database) is the most common cause of intermittent failures:",[114,1096,1098],{"className":143,"code":1097,"language":145,"meta":119,"style":119},"\u002F\u002F Group tests that share state into a collection (sequential within collection):\n[CollectionDefinition(\"Integration\")]\npublic class IntegrationCollection : ICollectionFixture\u003CTestWebFactory> { }\n\n[Collection(\"Integration\")]\npublic class OrderTests  { \u002F* shares TestWebFactory *\u002F }\n\n[Collection(\"Integration\")]\npublic class ProductTests { \u002F* same factory, same collection thread *\u002F }\n",[101,1099,1100,1105,1110,1115,1119,1124,1129,1133,1137],{"__ignoreMap":119},[123,1101,1102],{"class":125,"line":126},[123,1103,1104],{},"\u002F\u002F Group tests that share state into a collection (sequential within collection):\n",[123,1106,1107],{"class":125,"line":155},[123,1108,1109],{},"[CollectionDefinition(\"Integration\")]\n",[123,1111,1112],{"class":125,"line":161},[123,1113,1114],{},"public class IntegrationCollection : ICollectionFixture\u003CTestWebFactory> { }\n",[123,1116,1117],{"class":125,"line":167},[123,1118,177],{"emptyLinePlaceholder":176},[123,1120,1121],{"class":125,"line":173},[123,1122,1123],{},"[Collection(\"Integration\")]\n",[123,1125,1126],{"class":125,"line":180},[123,1127,1128],{},"public class OrderTests  { \u002F* shares TestWebFactory *\u002F }\n",[123,1130,1131],{"class":125,"line":186},[123,1132,177],{"emptyLinePlaceholder":176},[123,1134,1135],{"class":125,"line":192},[123,1136,1123],{},[123,1138,1139],{"class":125,"line":197},[123,1140,1141],{},"public class ProductTests { \u002F* same factory, same collection thread *\u002F }\n",[15,1143,1144],{},"Within a collection, use unique data (GUIDs, random IDs) so tests can run in\nparallel without stepping on each other.",[15,1146,1147,1150,1151,1154,1155,1158,1159,1162],{},[390,1148,1149],{},"Rule of thumb:"," Start integration tests with ",[101,1152,1153],{},"WebApplicationFactory"," and\nSQLite in-memory. Add Testcontainers only when you need real database constraints\nor migrations. Seed test data in ",[101,1156,1157],{},"InitializeAsync",", clean it in ",[101,1160,1161],{},"DisposeAsync",",\nand never depend on test execution order.",[1164,1165,1166],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}",{"title":119,"searchDepth":155,"depth":155,"links":1168},[1169,1170,1171,1172,1173,1178,1179,1180,1181,1182],{"id":12,"depth":155,"text":13},{"id":23,"depth":155,"text":24},{"id":96,"depth":155,"text":97},{"id":245,"depth":155,"text":246},{"id":376,"depth":155,"text":377,"children":1174},[1175,1176,1177],{"id":384,"depth":161,"text":385},{"id":396,"depth":161,"text":397},{"id":485,"depth":161,"text":486},{"id":559,"depth":155,"text":560},{"id":750,"depth":155,"text":751},{"id":885,"depth":155,"text":886},{"id":1004,"depth":155,"text":1005},{"id":1085,"depth":155,"text":1086},"How to test the full ASP.NET Core pipeline with WebApplicationFactory — swapping real dependencies with test doubles, using SQLite for database tests, and running authenticated requests without a real identity provider.","hard","md",".NET Core",{},"\u002Fblog\u002Fdotnet-integration-testing","\u002Fdotnet\u002Ftesting\u002Fintegration-testing",{"title":5,"description":1183},"blog\u002Fdotnet-integration-testing","Integration Testing","Testing","testing","2026-06-23","GDZTwTjjMXoWlCJzBWXOyiY761eX8Vl9gtPbUGGbFy8",1782244086339]