.NET Core 2アプリケーションでStartup.csクラスの単体テストを行うにはどうすればよいですか?すべての機能は、モックできない静的拡張メソッドによって提供されるようです?
このConfigureServices
メソッドを例にとると:
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<BlogContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddMvc();
}
AddDbContext(...)&AddMvc()が呼び出されることを確認するテストを作成するにはどうすればよいですか?
はい、拡張メソッドAddDbContext
がservices
で呼び出されたという事実を確認したい場合、問題が発生しています。良いことは、実際にこの事実を正確にチェックするべきではないということです。
Startup
クラスはアプリケーション composition root です。コンポジションルートをテストするとき、ルートオブジェクト(ASP.NET Coreアプリケーションの場合はコントローラー)のインスタンス化に必要なすべての依存関係が実際に登録されていることを確認する必要があります。
次のコントローラーがあるとします:
public class TestController : Controller
{
public TestController(ISomeDependency dependency)
{
}
}
Startup
がISomeDependency
の型を登録しているかどうかを確認してみてください。ただし、ISomeDependency
の実装には、チェックする必要がある他の依存関係も必要になる場合があります。最終的には、さまざまな依存関係のチェックを多数行うテストになりますが、実際には、オブジェクト解決が欠落している依存関係例外をスローしないことを保証しません。このようなテストにはあまり価値はありません。
コンポジションのルートをテストするときに私にとってうまくいくアプローチは、実際の依存性注入コンテナを使用することです。次に、その上でコンポジションルートを呼び出し、ルートオブジェクトの解決がスローされないことをアサートします。
他の非スタブクラスを使用するため、純粋な単体テストとは見なされませんでした。しかし、このようなテストは、他の統合テストとは異なり、高速で安定しています。そして最も重要なのは、正しい依存関係の登録のために有効なチェックの価値をもたらすことです。このようなテストに合格すると、オブジェクトが製品内で正しくインスタンス化されることを確認できます。
そのようなテストのサンプルを次に示します。
[TestMethod]
public void ConfigureServices_RegistersDependenciesCorrectly()
{
// Arrange
// Setting up the stuff required for Configuration.GetConnectionString("DefaultConnection")
Mock<IConfigurationSection> configurationSectionStub = new Mock<IConfigurationSection>();
configurationSectionStub.Setup(x => x["DefaultConnection"]).Returns("TestConnectionString");
Mock<Microsoft.Extensions.Configuration.IConfiguration> configurationStub = new Mock<Microsoft.Extensions.Configuration.IConfiguration>();
configurationStub.Setup(x => x.GetSection("ConnectionStrings")).Returns(configurationSectionStub.Object);
IServiceCollection services = new ServiceCollection();
var target = new Startup(configurationStub.Object);
// Act
target.ConfigureServices(services);
// Mimic internal asp.net core logic.
services.AddTransient<TestController>();
// Assert
var serviceProvider = services.BuildServiceProvider();
var controller = serviceProvider.GetService<TestController>();
Assert.IsNotNull(controller);
}
このアプローチは機能し、実際のMVCパイプラインを使用します。これは、動作方法を変更する必要がある場合にのみモックする必要があるためです。
public void AddTransactionLoggingCreatesConnection()
{
var servCollection = new ServiceCollection();
//Add any injection stuff you need here
//servCollection.AddSingleton(logger.Object);
//Setup the MVC builder thats needed
IMvcBuilder mvcBuilder = new MvcBuilder(servCollection, new Microsoft.AspNetCore.Mvc.ApplicationParts.ApplicationPartManager());
IEnumerable<KeyValuePair<string, string>> confValues = new List<KeyValuePair<string, string>>()
{
new KeyValuePair<string, string>("TransactionLogging:Enabled", "True"),
new KeyValuePair<string, string>("TransactionLogging:Uri", "https://api.something.com/"),
new KeyValuePair<string, string>("TransactionLogging:Version", "1"),
new KeyValuePair<string, string>("TransactionLogging:Queue:Enabled", "True")
};
ConfigurationBuilder builder = new ConfigurationBuilder();
builder.AddInMemoryCollection(confValues);
var confRoot = builder.Build();
StartupExtensions.YourExtensionMethod(mvcBuilder); // Any other params
}
私も同様の問題を抱えていましたが、AspNetCoreでWebHostを使用し、program.csが何をするかを本質的に再作成し、すべてのサービスが存在し、nullではないことをアサートすることでそれを回避できました。さらに一歩進んで、.ConfigureServicesを使用してIServicesの特定の拡張機能を実行するか、作成したサービスを実際に操作して、それらが適切に構築されていることを確認できます。
1つの重要な点は、個別のアセンブリを心配する必要がないように、テストしているスタートアップクラスを継承する単体テストスタートアップクラスを作成したことです。継承を使用しない場合は、構成を使用できます。
[TestClass]
public class StartupTests
{
[TestMethod]
public void StartupTest()
{
var webHost = Microsoft.AspNetCore.WebHost.CreateDefaultBuilder().UseStartup<Startup>().Build();
Assert.IsNotNull(webHost);
Assert.IsNotNull(webHost.Services.GetRequiredService<IService1>());
Assert.IsNotNull(webHost.Services.GetRequiredService<IService2>());
}
}
public class Startup : MyStartup
{
public Startup(IConfiguration config) : base(config) { }
}