相关博文:《asp.net 5 使用 TestServer 进行单元测试》
在上一篇博文中,主要说的是,使用 TestServer 对 ASP.NET 5 WebApi 进行单元测试,依赖注入在 WebApi Startup.cs 中完成,所以 UnitTest 中只需要使用 TestServer 启动 WebApi 站点就可以了,因为整个解决方案的对象都是用 ASP.NET 5 “自带”的依赖注入进行管理,所以,在对 ASP.NET 5 类库进行单元测试的时候,我都是手动进行 new 创建的对象,比如针对 application 的单元测试,贴一段 AdImageServiceTest 中的代码:
namespace CNBlogs.Ad.Application.Tests{ public class AdTextServiceTest : BaseTest { PRivate IAdTextService _adTextService; public AdTextServiceTest(ITestOutputHelper output) : base(output) { Bootstrapper.Startup.ConfigureMapper(); IUnitOfWork unitOfWork = new UnitOfWork(dbContext); IMemcached memcached = new EnyimMemcached(null); _adTextService = new AdTextService(new AdTextRepository(dbContext), new AdTextCreativeRepository(dbContext), new UserService(memcached), unitOfWork, memcached); } [Fact] public async Task GetByAdTextsTest() { var adTexts = await _adTextService.GetAdTexts(); Assert.NotNull(adTexts); adTexts.ForEach(x => Console.WriteLine($"{x.Title}({x.Link})")); } }}
AdImageServiceTest 构造函数中的代码,是不是看起来很别扭呢?我当时这样写,也是没有办法的,因为依赖注入的配置是写在 Startup 中,比如下面代码:
namespace CNBlogs.Ad.WebApi{ public class Startup { public Startup(IHostingEnvironment env, IApplicationEnvironment appEnv) { // Set up configuration sources. var builder = new ConfigurationBuilder() .AddJsonFile("appsettings.json") .AddEnvironmentVariables(); Configuration = builder.Build(); } public IConfigurationRoot Configuration { get; set; } // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { // Add framework services. services.AddMvc(); services.AddSingleton<IUnitOfWork, UnitOfWork>(); services.AddScoped<IDbContext, EFDbContext>(); services.AddSingleton<IAdTextRepository, AdTextRepository>(); services.AddSingleton<IAdTextService, AdTextService>(); } }}
这样设计导致的结果是,针对类库项目的单元测试,就没办法使用依赖注入获取对象了,我后来想使用针对 WebApi 单元测试的方式,来对类库进行单元测试,比如用 TestServer 来启动,但类库中没有办法获取所注入的对象,构成函数注入会报错,[FromServices]
属性注入是 MVC Controller 中的东西,并不支持,所以针对类库的单元测试,和 WebApi 的单元测试并不是一样。
IServiceCollection 的程序包是 Microsoft.Extensions.DependencyInjection.Abstractions
,我原来以为它和 ASP.NET 5 Web 应用程序相关,其实它们也没啥关系,你可以脱离 ASP.NET 5 Web 应用程序,独立使用它,比如在类库的单元测试中,但如果这样设计使用,我们首先需要做一个工作,把 Startup.cs 中的 ConfigureServices 配置,独立出来,比如放在 CNBlogs.Ad.Bootstrapper
中,这样 Web 应用程序和单元测试项目,都可以使用它,减少代码的重复,比如我们可以进行下面设计:
namespace CNBlogs.Ad.Bootstrapper{ public static class Startup { public static void Configure(this IServiceCollection services, string connectionString) { services.AddEntityFramework() .AddSqlServer() .AddDbContext<EFDbContext>(options => options.UseSqlServer(connectionString)); services.AddEnyimMemcached(); ConfigureMapper(); services.AddSingleton<IUnitOfWork, UnitOfWork>(); services.AddScoped<IDbContext, EFDbContext>(); services.AddSingleton<IAdTextRepository, AdTextRepository>(); services.AddSingleton<IAdTextService, AdTextService>(); } public static void ConfigureMapper() { Mapper.CreateMap<CNBlogs.Ad.Domain.Entities.AdText, AdTextDTO>(); } }}
ASP.NET 5 WebApi 项目中的 Startup.cs 配置会非常简单,只需要下面代码:
public void ConfigureServices(IServiceCollection services){ services.Configure(Configuration["data:ConnectionString"]);//add using CNBlogs.Ad.Bootstrapper;}
好了,做好上面工作后,单元测试中使用依赖注入就非常简单了,为了减少重复代码,我们可以先抽离出一个 BaseTest:
namespace CNBlogs.Ad.BaseTests{ public class BaseTest { protected readonly ITestOutputHelper output; protected IServiceProvider provider; public BaseTest(ITestOutputHelper output) { var connectionString = ""; var services = new ServiceCollection(); this.output = output; services.Configure(connectionString);////add using CNBlogs.Ad.Bootstrapper; provider = services.BuildServiceProvider(); } }}
可以看到,我们并没有使用 TestServer,而是手动创建一个 ServiceCollection,它有点类似于我们之前写依赖注入的 Unity Container,不过它们有很大不同,ServiceCollection 的功能更加强大,从 Bootstrapper.Startup 中可以看到,它可以注入 EF、MVC、Memcache 等等服务对象,BuildServiceProvider 的作用就是获取注入的服务对象,我们下面会用到:
namespace CNBlogs.Ad.Repository.Tests{ public class AdTextServiceTest : BaseTest { private IAdTextService _adTextService; public AdTextServiceTest(ITestOutputHelper output) : base(output) { _adTextService = provider.GetService<IAdTextService>(); } [Fact] public async Task GetByAdTextsTest() { var adTexts = await _adTextService.GetAdTexts(); Assert.NotNull(adTexts); adTexts.ForEach(x => Console.WriteLine($"{x.Title}({x.Link})")); } }}
这段代码和一开始的那段代码,形成了鲜明对比,这就是代码设计的魅力所在!!!
新闻热点
疑难解答