Часть 1. ConfigurationManagerЧасть 2. WebApplicationBuilderЧасть 3. Рассматриваем код WebApplicationBuilderЧасть 4. Создание конвейера промежуточного ПО в WebApplicationЧасть 5. Поддержка EF Core в WebApplicationBuilder В предыдущей статье я описал обходной путь, который был добавлен в .NET 6, чтобы инструменты EF Core, которые ранее полагались на существование определённых методов, таких как CreateHostBuilder, продолжали работать с новыми минимальными API хостинга. В этой статье я рассмотрю связанное изменение, обеспечивающее работу интеграционного тестирования с WebApplicationFactory в .NET 6. WebApplicationFactory использовала тот же класс HostFactoryResolver, что и инструменты EF Core, но потребовалось ещё несколько изменений, которые я рассмотрю в этой статье. Существует несколько способов протестировать приложение ASP.NET Core 3.x/5. Один из самых тщательных подходов — написание интеграционных тестов, которые запускают ваше приложение полностью в памяти. Это на удивление легко сделать с помощью пакета Microsoft.AspNetCore.Mvc.Testing и WebApplicationFactory<T>. Например, следующий код, судя по документации, показывает, как вы можете использовать WebApplicationFactory для создания экземпляра вашего приложения в памяти, создания HttpClient для выполнения запросов и отправки HTTP-запроса в памяти. Под капотом WebApplicationFactory<T> использует тот же HostFactoryResolver, который я описал в предыдущей статье*EN. Обобщённый параметр TEntryPoint обычно имеет значение Startup, но он просто должен быть одним из типов из входной сборки, чтобы система могла найти метод CreateHostBuilder(): Как я описал в предыдущей статье, HostFactoryResolver использует рефлексию, чтобы найти методы с условными именами CreateHostBuilder() или CreateWebHostBuilder() и вызвать их. Однако в .NET 6 минимальные API хостинга и программы верхнего уровня покончили с этими соглашениями, первоначально нарушив работу WebApplicationFactory (так же, как и EF Core). В предыдущей статье я описал изменения, внесённые в HostBuilder для поддержки HostFactoryResolver, используемого инструментами WebApplicationFactory и EF Core. В первую очередь это было достигнуто за счёт добавления дополнительных событий DiagnosticSource в HostBuilder. Они позволяют HostFactoryResolver получить доступ к HostBuilder без необходимости использовать соглашения предыдущих версий. WebApplicationFactory также выигрывает от этого нового механизма, но пришлось внести несколько дополнительных изменений. Инструментам EF Core просто необходимо получить доступ к построенному IHost, чтобы извлечь из него IServiceProvider. IServiceProvider фиксируется после создания IHost, поэтому подход «остановки приложения», показанный на изображении выше, работал просто отлично. Однако это не работает для WebApplicationFactory. WebApplicationFactory должна иметь возможность модифицировать HostBuilder вашего приложения до вызова Build(). Кроме того, она не может просто остановить вашу программу после вызова HostBuilder.Build(). В .NET 6 вы можете писать любой код между вызовами WebApplicationBuilder.Build() и WebApplication.Run(). Вы не можете изменить IServiceCollection, но вы можете зарегистрировать свои конечные точки и промежуточное ПО между этими вызовами: Это немного усложняет работу WebApplicationFactory, так как фабрике нужно выполнить весь код в Program.cs вплоть до вызова app.Run(), поэтому она не может полагаться только на события DiagnosticSource, добавленные в HostBuilder. В оставшейся части этого поста мы рассмотрим, как WebApplicationFactory достигает этого. На первый взгляд, способ использования WebApplicationFactory не изменился в .NET 6. Точно такой же тестовый код, который я показал выше, будет работать в .NET 6, даже если вы используете новые минимальные API хостинга с WebApplication и WebApplicationBuilder. Одна небольшая неприятность заключается в том, что нет известного класса Startup, который можно было бы использовать в качестве «маркера» для обобщённого параметра T в WebApplicationFactory<T>*EN. На практике это, скорее всего, будет проблемой только для программ уровня «Hello world», поскольку вы можете использовать любой класс в своём веб-приложении в качестве маркера, но об этой особенности следует знать. WebApplicationFactory предоставляет несколько способов настройки вашего приложения в интеграционных тестах, но по своей сути она предоставляет способ запуска экземпляра хоста вашего приложения в памяти. Один из основных методов в этом процессе — EnsureServer(), который частично показан ниже. EnsureServer() отвечает за заполнение поля _server типом TestServer с помощью HostFactoryResolver. Сначала он пытается создать экземпляр IHostBuilder через метод Program.CreateHostBuilder(), обычно используемый в ASP.NET Core 3.x/5. Если это не удаётся, он ищет метод Program.CreateWebHostBuilder(), используемый в ASP.NET Core 2.x. Если и это не удается, он прибегает к подходу .NET 6, который я извлёк из описанного выше метода и показал ниже. В этом методе используется новый тип DeferredHostBuilder, который мы вскоре рассмотрим, но важным разделом является вызов HostFactoryResolver.ResolveHostFactory(). Этот метод использует события DiagnosticSource, которые рассматривались в предыдущей статье, для настройки IHostBuilder и доступа к IHost. В частности, вызов регистрирует два обратных вызова: deferredHostBuilder.ConfigureHostBuilder: вызывается непосредственно перед построением IHostBuilder, ему передаётся экземпляр IHostBuilder. deferredHostBuilder.EntryPointCompleted: вызывается, если в процессе сборки возникает исключение. Важно отметить, что для аргумента stopApplication установлено значение false; это гарантирует, что процесс запуска приложения не будет прерван. Сравните это с подходом для EF Core, в котором stopApplication=true. Инструменты EF Core не хотят запускать ваше приложение, им просто нужен доступ к IHost (и IServiceProvider), поэтому они могут остановить приложение после сборки хоста. На следующей диаграмме показано взаимодействие WebApplicationFactory с HostFactoryResolver и DeferredHostBuilder, а также с другими типами, которые мы рассмотрим позже. Не беспокойтесь о том, чтобы полностью понять её сейчас, но я думаю, что сейчас полезно рассматривать это как схему того, куда мы идём! Вы можете удивиться, зачем нам здесь нужен новый тип, DeferredHostBuilder. Он необходим из-за асинхронного способа ожидания завершения работы «основного» приложения. В следующем разделе я подробно рассмотрю этот тип. DeferredHostBuilder — это ещё один IHostBuilder, который был представлен в .NET 6 (наряду с другими)! Он предназначен для «захвата» вызываемых в нём методов конфигурации (например, ConfigureServices()), а затем «воспроизведения» их для IHostBuilder реального приложения, как только он станет доступен. Эти методы «отсрочки» собирают методы конфигурации в виде мультикаст-делегата, например: Эти делегаты затем применяются к IHostBuilder, когда вызывается событие HostBuilding экземляра DiagnosticSource: Затем WebApplicationFactory вызывает Build() на DeferredHostBuilder. Этот метод, как показано ниже, вызывает метод _hostFactory, возвращаемый HostResolverFactory. Вызов этого метода запускает процесс, описанный в предыдущей статье, в котором приложение выполняется в отдельном потоке, с помощью событий DiagnosticSource вызывается настройка ConfigureHostBuilder() и возвращается экземпляр IHost. Помните, что приложение в отдельном потоке не перестаёт работать, когда мы извлекаем экземпляр IHost, потому что нам нужно, чтобы остальной код в Program.cs выполнился. DeferredHostBuilder сохраняет IHost в новый тип DefferedHost и возвращает его из вызова Build(). _hostStartTcs — это TaskCompletionSource, который используется для обработки пограничного случая, когда приложение, работающее в фоновом режиме, завершает работу из-за исключения. Это крайний случай, но без него тест может зависнуть на неопределенный срок. DeferredHost отвечает за ожидание правильного запуска приложения (а не только за сборку IHost). Ему нужно подождать, пока будут настроены все конечные точки, а также исполнится любой дополнительный стартовый код. DeferredHost достигает этого, используя существующие события IHostApplicationLifetime, которые вызываются в обычном приложении на универсальном хосте при запуске. На следующем изображении (взятом из статьи, посвященной анализу процесса запуска универсального хоста*EN) показано, что метод NotifyStarted() вызывается в IHostApplicationLifetime после запуска сервера. Вызов NotifyStarted() вызывает событие ApplicationStarted, которое DeferredHost использует для определения того, что приложение запущено, и можно безопасно запускать тесты. Когда WebApplicationFactory вызывает StartAsync() для DeferredHost, DeferredHost блокируется до тех пор, пока не возникнет событие ApplicationStarted. Метод StartAsync() добавляет дополнительные обратные вызовы к TaskCompletionSource, о которых я упоминал ранее, а затем ожидает задачу перед возвратом. Это заблокирует тестовый код до тех пор, пока не произойдёт одно из трех событий: Веб-приложение выбросит исключение, которое вызовет функцию обратного вызова EntryPointCompleted() на DeferredHostBuilder и отменит задачу. CancellationToken, переданный методу StartAsync(), отменится, что отменит задачу. Приложение запустится, вызвав событие ApplicationStarted, завершив задачу успехом. Как отмечено в комментариях к этому методу, если вы никогда не вызываете Start() или Run() в своем веб-приложении (и не генерируете исключение), это приведёт к взаимоблокировке, но тогда у вас, скорее всего, не будет действительного веб-приложения, так что это не большая проблема. Вот и всё! После вызова Start() WebApplicationFactory создаёт HttpClient, как и в предыдущих версиях, и вы можете выполнять вызовы в памяти, как и раньше. Стоит знать, что (в отличие от предыдущих версий ASP.NET Core) в ваших тестах будет исполнено всё, что содержится в Program.cs. Но, помимо этого нюанса, всё в вашем тестовом коде останется прежним. В этой статье я описал изменения, сделанные в WebApplicationFactory для поддержки новых минимальных API хостинга, использующих WebApplication и WebApplicationBuilder. Изменения потребовались, потому что в Program.cs больше нет «обычных» методов, которые можно вызывать с помощью рефлексии, а также внутри Program.cs происходит настройка вашего промежуточного программного обеспечения и конечных точек. Чтобы обойти эту проблему, WebApplicationFactory использует те же события DiagnosticSource, что и инструменты EF Core из предыдущей статьи, для настройки IHostBuilder и получения IHost. Однако, в отличие от инструментов EF Core, WebApplicationFactory не останавливает приложение после сборки IHost. Вместо этого она позволяет приложению продолжить работу и прослушивает событие IHostApplicationLifetime.ApplicationStarted. Это позволяет WebApplicationFactory блокироваться до тех пор, пока не будет выполнен весь код в Program.cs и приложение не будет готово начать обработку запросов. .NET разработчик