ASP.NET Web API 单元测试 - Web API 简单介绍
如果你对Web API已比较了解则可以跳过本篇直接看单元测试部分。
创建一个叫 UnitTesingWebAPI的空白的解决方案,并包含下列项目:
UnitTestingWebAPI.Domain:类库(包含Entity Models)
UnitTestingWebAPI.Data:类库(包含Repositories)
UnitTestingWebAPI.Services:类库(包含Services)
UnitTestingWebAPI.API.Core:类库(包含WebAPI组件,例如:Controllers, Filters, Massage Handlers)
UnitTestingWebAPI.API:空的ASP.NET Web Application(Web程序去管控(host) WebAPI
UnitTestingWebAPI.Tests:类库(包含单元测试)
Domain 层:
Articles.cs
publicclassArticle{publicintID{get;set;}publicstringTitle{get;set;}publicstringContents{get;set;}publicstringAuthor{get;set;}publicstringURL{get;set;}publicDateTimeDateCreated{get;set;}publicDateTimeDateEdited{get;set;}publicintBlogID{get;set;}publicvirtualBlogBlog{get;set;}publicArticle(){}}
Blog.cs
publicclassBlog{publicintID{get;set;}publicstringName{get;set;}publicstringURL{get;set;}publicstringOwner{get;set;}publicDateTimeDateCreated{get;set;}publicvirtualICollection<Article>Articles{get;set;}publicBlog(){Articles=newHashSet<Article>();}}
Respository 层:
为UnitTestingWebAPI.Data安装Entity Framework,有两种方式:
1:在UnitTestingWebAPI.Data上右键,点击管理Nuget包,选择对话窗口的左边选择在线包,找到EF进行安装
2:命令行:
install-packageEntityFramework
注:一定要注意我用红色圈住的地方。
添加下面的类:
Configurations/ArticleConfiguration.cs
publicclassArticleConfiguration:EntityTypeConfiguration<Article>{publicArticleConfiguration(){ToTable("Article");Property(a=>a.Title).IsRequired().HasMaxLength(100);Property(a=>a.Contents).IsRequired();Property(a=>a.Author).IsRequired().HasMaxLength(50);Property(a=>a.URL).IsRequired().HasMaxLength(200);Property(a=>a.DateCreated).HasColumnType("datetime2");Property(a=>a.DateEdited).HasColumnType("datetime2");}}
Configurations/BlogConfiguration.cs
publicclassBlogConfiguration:EntityTypeConfiguration<Blog>{publicBlogConfiguration(){ToTable("Blog");Property(b=>b.Name).IsRequired().HasMaxLength(100);Property(b=>b.URL).IsRequired().HasMaxLength(200);Property(b=>b.Owner).IsRequired().HasMaxLength(50);Property(b=>b.DateCreated).HasColumnType("datetime2");}}
Configurations/BloggerEntities
publicclassBloggerEntities:DbContext{publicBloggerEntities():base("BloggerEntities"){Configuration.ProxyCreationEnabled=false;}publicDbSet<Blog>Blogs{get;set;}publicDbSet<Article>Articles{get;set;}publicvirtualvoidCommit(){base.SaveChanges();}protectedoverridevoidOnModelCreating(DbModelBuildermodelBuilder){modelBuilder.Configurations.Add(newArticleConfiguration());modelBuilder.Configurations.Add(newBlogConfiguration());}}
Configurations/BloggerInitializer
publicclassBloggerInitializer:DropCreateDatabaseIfModelChanges<BloggerEntities>{protectedoverridevoidSeed(BloggerEntitiescontext){GetBlogs().ForEach(b=>context.Blogs.Add(b));context.Commit();}publicstaticList<Blog>GetBlogs(){List<Blog>_blogs=newList<Blog>();//AddtwoBlogsBlog_chsakellsBlog=newBlog(){Name="chsakell'sBlog",URL="https://chsakell.com/",Owner="ChrisSakellarios",Articles=GetChsakellsArticles()};Blog_dotNetCodeGeeks=newBlog(){Name="DotNETCodeGeeks",URL="dotnetcodegeeks",Owner=".NETCodeGeeks",Articles=GetDotNETGeeksArticles()};_blogs.Add(_chsakellsBlog);_blogs.Add(_dotNetCodeGeeks);return_blogs;}publicstaticList<Article>GetChsakellsArticles(){List<Article>_articles=newList<Article>();Article_oData=newArticle(){Author="ChrisS.",Title="ASP.NETWebAPIfeat.OData",URL="https://chsakell.com/2015/04/04/asp-net-web-api-feat-odata/",Contents=@"ODataisanopenstandardprotocolallowingthecreationandconsumptionofqueryableandinteroperableRESTfulAPIs.ItwasinitiatedbyMicrosoftandit’smostlyknownto.NETDevelopersfromWCFDataServices.TherearemanyotherserverplatformssupportingODataservicessuchasNode.js,PHP,JavaandSQLServerReportingServices.Moreover,WebAPIalsosupportsODataandthispostwillshowyouhowtointegratethosetwo.."};Article_wcfCustomSecurity=newArticle(){Author="ChrisS.",Title="SecureWCFServiceswithcustomencryptedtokens",URL="https://chsakell.com/2014/12/13/secure-wcf-services-with-custom-encrypted-tokens/",Contents=@"WindowsCommunicationFoundationframeworkcomeswithalotofoptionsoutofthebox,concerningthesecuritylogicyouwillapplytoyourservices.Differentbindingscanbeusedforcertainkindandlevelsofsecurity.EventheBasicHttpBindingbindingsupportssometypesofsecurity.Therearesometimesthoughwhereyoucannotordon’twanttouseWCFsecurityavailableoptionsandhence,youneedtodevelopyourownauthenticationlogicaccoardingtoyourbusinessneeds."};_articles.Add(_oData);_articles.Add(_wcfCustomSecurity);return_articles;}publicstaticList<Article>GetDotNETGeeksArticles(){List<Article>_articles=newList<Article>();Article_angularFeatWebAPI=newArticle(){Author="GordonBeeming",Title="AngularJSfeat.WebAPI",URL="http://www.dotnetcodegeeks.com/2015/05/angularjs-feat-web-api.html",Contents=@"DevelopingWebapplicationsusingAngularJSandWebAPIcanbequiteamuzing.Youcanpickthisarchitectureincaseyouhaveinmindawebapplicationwithlimittedpagerefreshesorpostbackstotheserverwhileeachapplication’sViewisbasedonpartialdataretrievedfromit."};_articles.Add(_angularFeatWebAPI);return_articles;}publicstaticList<Article>GetAllArticles(){List<Article>_articles=newList<Article>();_articles.AddRange(GetChsakellsArticles());_articles.AddRange(GetDotNETGeeksArticles());return_articles;}}
Infrastructure/Disposable.cs
publicclassDisposable:IDisposable{privateboolisDisposed;~Disposable(){Dispose(false);}publicvoidDispose(){Dispose(true);GC.SuppressFinalize(this);}privatevoidDispose(booldisposing){if(!isDisposed&&disposing){DisposeCore();}isDisposed=true;}//OvveridethistodisposecustomobjectsprotectedvirtualvoidDisposeCore(){}}
Infrastructure/IDbFactory.cs
publicinterfaceIDbFactory:IDisposable{BloggerEntitiesInit();}
Infrastructure/DbFactory.cs
publicclassDbFactory:Disposable,IDbFactory{BloggerEntitiesdbContext;publicBloggerEntitiesInit(){returndbContext??(dbContext=newBloggerEntities());}protectedoverridevoidDisposeCore(){if(dbContext!=null)dbContext.Dispose();}}
Infrastrure/IRepository.cs
publicinterfaceIRepository<T>whereT:class{//MarksanentityasnewvoidAdd(Tentity);//MarksanentityasmodifiedvoidUpdate(Tentity);//MarksanentitytoberemovedvoidDelete(Tentity);voidDelete(Expression<Func<T,bool>>where);//GetanentitybyintidTGetById(intid);//GetanentityusingdelegateTGet(Expression<Func<T,bool>>where);//GetsallentitiesoftypeTIEnumerable<T>GetAll();//GetsentitiesusingdelegateIEnumerable<T>GetMany(Expression<Func<T,bool>>where);}
Infrastructure/RepositoryBase.cs
publicabstractclassRepositoryBase<T>whereT:class{#regionPropertiesprivateBloggerEntitiesdataContext;privatereadonlyIDbSet<T>dbSet;protectedIDbFactoryDbFactory{get;privateset;}protectedBloggerEntitiesDbContext{get{returndataContext??(dataContext=DbFactory.Init());}}#endregionprotectedRepositoryBase(IDbFactorydbFactory){DbFactory=dbFactory;dbSet=DbContext.Set<T>();}#regionImplementationpublicvirtualvoidAdd(Tentity){dbSet.Add(entity);}publicvirtualvoidUpdate(Tentity){dbSet.Attach(entity);dataContext.Entry(entity).State=EntityState.Modified;}publicvirtualvoidDelete(Tentity){dbSet.Remove(entity);}publicvirtualvoidDelete(Expression<Func<T,bool>>where){IEnumerable<T>objects=dbSet.Where<T>(where).AsEnumerable();foreach(Tobjinobjects)dbSet.Remove(obj);}publicvirtualTGetById(intid){returndbSet.Find(id);}publicvirtualIEnumerable<T>GetAll(){returndbSet.ToList();}publicvirtualIEnumerable<T>GetMany(Expression<Func<T,bool>>where){returndbSet.Where(where).ToList();}publicTGet(Expression<Func<T,bool>>where){returndbSet.Where(where).FirstOrDefault<T>();}#endregion}
Infrastrure/IUnitOfWork.cs
publicinterfaceIUnitOfWork{voidCommit();}
Infrastrure/UnitOfWork.cs
publicclassUnitOfWork:IUnitOfWork{privatereadonlyIDbFactorydbFactory;privateBloggerEntitiesdbContext;publicUnitOfWork(IDbFactorydbFactory){this.dbFactory=dbFactory;}publicBloggerEntitiesDbContext{get{returndbContext??(dbContext=dbFactory.Init());}}publicvoidCommit(){DbContext.Commit();}}
Infrastructure/BlogRepository.cs
publicclassBlogRepository:RepositoryBase<Blog>,IBlogRepository{publicBlogRepository(IDbFactorydbFactory):base(dbFactory){}publicBlogGetBlogByName(stringblogName){var_blog=this.DbContext.Blogs.Where(b=>b.Name==blogName).FirstOrDefault();return_blog;}}publicinterfaceIBlogRepository:IRepository<Blog>{BlogGetBlogByName(stringblogName);}
Repositories/ArticleRepository.cs
publicclassArticleRepository:RepositoryBase<Article>,IArticleRepository{publicArticleRepository(IDbFactorydbFactory):base(dbFactory){}publicArticleGetArticleByTitle(stringarticleTitle){var_article=this.DbContext.Articles.Where(b=>b.Title==articleTitle).FirstOrDefault();return_article;}}publicinterfaceIArticleRepository:IRepository<Article>{ArticleGetArticleByTitle(stringarticleTitle);}
Service 层
到UnitTestingWebAPI.Service项目上,添加对UnitTestingWebAPI.Domain, UnitTestingWebAPI.Data的引用,并添加下列文件:
ArticleService.cs
//operationsyouwanttoexposepublicinterfaceIArticleService{IEnumerable<Article>GetArticles(stringname=null);ArticleGetArticle(intid);ArticleGetArticle(stringname);voidCreateArticle(Articlearticle);voidUpdateArticle(Articlearticle);voidDeleteArticle(Articlearticle);voidSaveArticle();}publicclassArticleService:IArticleService{privatereadonlyIArticleRepositoryarticlesRepository;privatereadonlyIUnitOfWorkunitOfWork;publicArticleService(IArticleRepositoryarticlesRepository,IUnitOfWorkunitOfWork){this.articlesRepository=articlesRepository;this.unitOfWork=unitOfWork;}#regionIArticleServiceMemberspublicIEnumerable<Article>GetArticles(stringtitle=null){if(string.IsNullOrEmpty(title))returnarticlesRepository.GetAll();elsereturnarticlesRepository.GetAll().Where(c=>c.Title.ToLower().Contains(title.ToLower()));}publicArticleGetArticle(intid){vararticle=articlesRepository.GetById(id);returnarticle;}publicArticleGetArticle(stringtitle){vararticle=articlesRepository.GetArticleByTitle(title);returnarticle;}publicvoidCreateArticle(Articlearticle){articlesRepository.Add(article);}publicvoidUpdateArticle(Articlearticle){articlesRepository.Update(article);}publicvoidDeleteArticle(Articlearticle){articlesRepository.Delete(article);}publicvoidSaveArticle(){unitOfWork.Commit();}#endregion}
BlogService.cs
//operationsyouwanttoexposepublicinterfaceIBlogService{IEnumerable<Blog>GetBlogs(stringname=null);BlogGetBlog(intid);BlogGetBlog(stringname);voidCreateBlog(Blogblog);voidUpdateBlog(Blogblog);voidSaveBlog();voidDeleteBlog(Blogblog);}publicclassBlogService:IBlogService{privatereadonlyIBlogRepositoryblogsRepository;privatereadonlyIUnitOfWorkunitOfWork;publicBlogService(IBlogRepositoryblogsRepository,IUnitOfWorkunitOfWork){this.blogsRepository=blogsRepository;this.unitOfWork=unitOfWork;}#regionIBlogServiceMemberspublicIEnumerable<Blog>GetBlogs(stringname=null){if(string.IsNullOrEmpty(name))returnblogsRepository.GetAll();elsereturnblogsRepository.GetAll().Where(c=>c.Name==name);}publicBlogGetBlog(intid){varblog=blogsRepository.GetById(id);returnblog;}publicBlogGetBlog(stringname){varblog=blogsRepository.GetBlogByName(name);returnblog;}publicvoidCreateBlog(Blogblog){blogsRepository.Add(blog);}publicvoidUpdateBlog(Blogblog){blogsRepository.Update(blog);}publicvoidDeleteBlog(Blogblog){blogsRepository.Delete(blog);}publicvoidSaveBlog(){unitOfWork.Commit();}#endregion}
Web API Core 组件
在UnitTestingWebAPI.API.Core 上添加 UnitTestingWebAPI.API.Domain 和UnitTestingWebAPI.Service 项目,并安装下面的包(方法和前面Resporities层一样):
Entity Framework
Microsoft.AspNet.WebApi.Core
Microsoft.AspNet.WebApi.Client
添加下面的Web API Controller到 Controller 文件夹中:
Controllers/ArticlesController.cs
publicclassArticlesController:ApiController{privateIArticleService_articleService;publicArticlesController(IArticleServicearticleService){_articleService=articleService;}//GET:api/ArticlespublicIEnumerable<Article>GetArticles(){return_articleService.GetArticles();}//GET:api/Articles/5[ResponseType(typeof(Article))]publicIHttpActionResultGetArticle(intid){Articlearticle=_articleService.GetArticle(id);if(article==null){returnNotFound();}returnOk(article);}//PUT:api/Articles/5[ResponseType(typeof(void))]publicIHttpActionResultPutArticle(intid,Articlearticle){if(!ModelState.IsValid){returnBadRequest(ModelState);}if(id!=article.ID){returnBadRequest();}_articleService.UpdateArticle(article);try{_articleService.SaveArticle();}catch(DbUpdateConcurrencyException){if(!ArticleExists(id)){returnNotFound();}else{throw;}}returnStatusCode(HttpStatusCode.NoContent);}//POST:api/Articles[ResponseType(typeof(Article))]publicIHttpActionResultPostArticle(Articlearticle){if(!ModelState.IsValid){returnBadRequest(ModelState);}_articleService.CreateArticle(article);returnCreatedAtRoute("DefaultApi",new{id=article.ID},article);}//DELETE:api/Articles/5[ResponseType(typeof(Article))]publicIHttpActionResultDeleteArticle(intid){Articlearticle=_articleService.GetArticle(id);if(article==null){returnNotFound();}_articleService.DeleteArticle(article);returnOk(article);}privateboolArticleExists(intid){return_articleService.GetArticle(id)!=null;}}
Controllers/BlogsController.cs
publicclassBlogsController:ApiController{privateIBlogService_blogService;publicBlogsController(IBlogServiceblogService){_blogService=blogService;}//GET:api/BlogspublicIEnumerable<Blog>GetBlogs(){return_blogService.GetBlogs();}//GET:api/Blogs/5[ResponseType(typeof(Blog))]publicIHttpActionResultGetBlog(intid){Blogblog=_blogService.GetBlog(id);if(blog==null){returnNotFound();}returnOk(blog);}//PUT:api/Blogs/5[ResponseType(typeof(void))]publicIHttpActionResultPutBlog(intid,Blogblog){if(!ModelState.IsValid){returnBadRequest(ModelState);}if(id!=blog.ID){returnBadRequest();}_blogService.UpdateBlog(blog);try{_blogService.SaveBlog();}catch(DbUpdateConcurrencyException){if(!BlogExists(id)){returnNotFound();}else{throw;}}returnStatusCode(HttpStatusCode.NoContent);}//POST:api/Blogs[ResponseType(typeof(Blog))]publicIHttpActionResultPostBlog(Blogblog){if(!ModelState.IsValid){returnBadRequest(ModelState);}_blogService.CreateBlog(blog);returnCreatedAtRoute("DefaultApi",new{id=blog.ID},blog);}//DELETE:api/Blogs/5[ResponseType(typeof(Blog))]publicIHttpActionResultDeleteBlog(intid){Blogblog=_blogService.GetBlog(id);if(blog==null){returnNotFound();}_blogService.DeleteBlog(blog);returnOk(blog);}privateboolBlogExists(intid){return_blogService.GetBlog(id)!=null;}}
在有需要时添加下面的过滤器,它会反转Articles list的顺序:
Filters/ArticlesReversedFilter.cs
publicclassArticlesReversedFilter:ActionFilterAttribute{publicoverridevoidOnActionExecuted(HttpActionExecutedContextactionExecutedContext){varobjectContent=actionExecutedContext.Response.ContentasObjectContent;if(objectContent!=null){List<Article>_articles=objectContent.ValueasList<Article>;if(_articles!=null&&_articles.Count>0){_articles.Reverse();}}}}
当添加下面的媒体类型格式化器,可以返回一个用逗号分割来展示的文章列表:
MediaTypeFormatters/ArticleFormatter.cs
publicclassArticleFormatter:BufferedMediaTypeFormatter{publicArticleFormatter(){SupportedMediaTypes.Add(newMediaTypeHeaderValue("application/article"));}publicoverrideboolCanReadType(Typetype){returnfalse;}publicoverrideboolCanWriteType(Typetype){//forsinglearticleobjectif(type==typeof(Article))returntrue;else{//formultiplearticleobjectsType_type=typeof(IEnumerable<Article>);return_type.IsAssignableFrom(type);}}publicoverridevoidWriteToStream(Typetype,objectvalue,StreamwriteStream,HttpContentcontent){using(StreamWriterwriter=newStreamWriter(writeStream)){vararticles=valueasIEnumerable<Article>;if(articles!=null){foreach(vararticleinarticles){writer.Write(String.Format("[{0},\"{1}\",\"{2}\",\"{3}\",\"{4}\"]",article.ID,article.Title,article.Author,article.URL,article.Contents));}}else{var_article=valueasArticle;if(_article==null){thrownewInvalidOperationException("Cannotserializetype");}writer.Write(String.Format("[{0},\"{1}\",\"{2}\",\"{3}\",\"{4}\"]",_article.ID,_article.Title,_article.Author,_article.URL,_article.Contents));}}}}
添加下面两个 消息 处理器,第一个负责Response中添加定制 header,第二个可以决定这个请求是否被接受:
MessageHandler/HeaderAppenderHandler.cs
publicclassHeaderAppenderHandler:DelegatingHandler{asyncprotectedoverrideTask<HttpResponseMessage>SendAsync(HttpRequestMessagerequest,CancellationTokencancellationToken){HttpResponseMessageresponse=awaitbase.SendAsync(request,cancellationToken);response.Headers.Add("X-WebAPI-Header","WebAPIUnittestinginchsakell'sblog.");returnresponse;}}
HeaderAppenderHandler/EndRequestHandler.cs
publicclassEndRequestHandler:DelegatingHandler{asyncprotectedoverrideTask<HttpResponseMessage>SendAsync(HttpRequestMessagerequest,CancellationTokencancellationToken){if(request.RequestUri.AbsoluteUri.Contains("test")){varresponse=newHttpResponseMessage(HttpStatusCode.OK){Content=newStringContent("Unittestingmessagehandlers!")};vartsc=newTaskCompletionSource<HttpResponseMessage>();tsc.SetResult(response);returnawaittsc.Task;}else{returnawaitbase.SendAsync(request,cancellationToken);}}}
添加下面被用于从Web 应用程序中注册Controller 的 DefaultAssembliesResolver
CustomAssembliesResolver.cs
publicclassCustomAssembliesResolver:DefaultAssembliesResolver{publicoverrideICollection<Assembly>GetAssemblies(){varbaseAssemblies=base.GetAssemblies().ToList();varassemblies=newList<Assembly>(baseAssemblies){typeof(BlogsController).Assembly};baseAssemblies.AddRange(assemblies);returnbaseAssemblies.Distinct().ToList();}}
Asp.NET Web Application
添加 UnitTestingWebAPI.API Web应用项目,并且添加引用 UnitTestingWebAPI.Core, UnitTestingWebAPI.Data 和 UnitTestingWebAPI.Service,同样需要安装下列组件包:
Entity Framework
Microsoft.AspNet.WebApi.WebHost
Microsoft.AspNet.WebApi.Core
Microsoft.AspNet.WebApi.Client
Microsoft.AspNet.WebApi.Owin
Microsoft.Owin.Host.SystemWeb
Microsoft.Owin
Autofac.WebApi2
在Global配置文件(如果没有就新增一个)中配置初始化数据库配置
Global.asax
protectedvoidApplication_Start(){GlobalConfiguration.Configure(WebApiConfig.Register);//InitdatabaseSystem.Data.Entity.Database.SetInitializer(newBloggerInitializer());}
同样要记得添加一个相关的Connection String在Web.config文件中.
<connectionStrings><addname="BloggerDbConnStr"connectionString="DataSource=(localdb)\v11.0;InitialCatalog=BloggerDB;IntegratedSecurity=True"providerName="System.Data.SqlClient"/></connectionStrings>
注册外部Controller
在Web Application的根目录创建一个 Owin Startup.cs 文件并且粘贴下面的代码,在(autofac configration)需要的时候,这部分代码会确保 UnitTestingWebAPI.API.Core(CustomAssembliesResolver)项目正确使用WebApi Controller和注入合适的仓库以及服务.
Startup.cs
publicclassStartup{publicvoidConfiguration(IAppBuilderappBuilder){varconfig=newHttpConfiguration();config.Services.Replace(typeof(IAssembliesResolver),newCustomAssembliesResolver());config.Formatters.Add(newArticleFormatter());config.Routes.MapHttpRoute(name:"DefaultApi",routeTemplate:"api/{controller}/{id}",defaults:new{id=RouteParameter.Optional});//Autofacconfigurationvarbuilder=newContainerBuilder();builder.RegisterApiControllers(typeof(BlogsController).Assembly);builder.RegisterType<UnitOfWork>().As<IUnitOfWork>().InstancePerRequest();builder.RegisterType<DbFactory>().As<IDbFactory>().InstancePerRequest();//Repositoriesbuilder.RegisterAssemblyTypes(typeof(BlogRepository).Assembly).Where(t=>t.Name.EndsWith("Repository")).AsImplementedInterfaces().InstancePerRequest();//Servicesbuilder.RegisterAssemblyTypes(typeof(ArticleService).Assembly).Where(t=>t.Name.EndsWith("Service")).AsImplementedInterfaces().InstancePerRequest();IContainercontainer=builder.Build();config.DependencyResolver=newAutofacWebApiDependencyResolver(container);appBuilder.UseWebApi(config);}}
在这个时候,你应该可以启动Web应用程序并且使用下面的请求来获取article或blogs(这里的端口可能不一致):
http://localhost:57414/api/Articles
http://localhost:57414/api/Blogs
同样附上原文:chsakell's Blog
文章中的源码:http://down.51cto.com/data/2243634
到此为止,WebAPI部分介绍的差不多了,有问题请留言.
声明:本站所有文章资源内容,如无特殊说明或标注,均为采集网络资源。如若本站内容侵犯了原著者的合法权益,可联系本站删除。