ASP.NET Web API Unit Testing
Unit testing can be beneficial to many aspects in software develepment, from the lowest level that is the source code to the highest level and the end user’s experience. Writing automated tests helps finding defects earlier in the development lifecycle process which leads to fewer late nights or weekend work (happier developers). Since defects are resolved before production, less defects reach end users (happier clients). It also increases reliability of source code, since if the base code doesn’t change all tests should always return the same results. Last but not least, anyone that decides to write unit tests is also forced to write testable code which leads to better software development practices.
Web API Unit Testing
ASP.NET Web API stack has many aspects that firstly must be well understood before writing unit tests against it and that’s what makes it difficult. This post is a full stack Web API Unit testing tutorial which means will show you how to unit test all the layers and components exist in your Web API application. Let’s see what we are gonna see on this post:
- Web API Solution Best Practices: Create a loosely coupled, scalable and testable Web API application
- Entity Framework Unit testing: Mocking generic repositories and testing the service layer
- Web API Controllers testing: Direct and integration testing
- Web API Filters unit testing: Direct and integration testing
- Web API Message Handlers unit testing: Direct and integration testing
- Web API Media type Formatters unit testing
- Web API routing unit testing
I will break the post in two main sections. The first one will be the one where we ‘re gonna structure the application and the second one will be the actual Unit testing. For the first one I will follow the Generic repository pattern which I have already describe in this post. If you feel familiar with those concepts and you just want to read about how the unit testing is done, you can skip this step. Mind though that part of this section will be the Controller registration of a referenced library which has an important role in our Unit testing.
Section One: Structuring the Web API Application
Create a new blank solution named UnitTestingWebAPI and add the following projects:
- UnitTestingWebAPI.Domain: Class library (Contains Entity Models)
- UnitTestingWebAPI.Data: Class library (Contains Repositories)
- UnitTestingWebAPI.Services: Class library (Contains Services)
- UnitTestingWebAPI.API.Core: Class library (Contains Web API components such as Controllers, Filters, Message Handlers)
- UnitTestingWebAPI.API: Empty ASP.NET Web Application (Web application to host Web API)
- UnitTestingWebAPI.Tests: Class library (Contains the Unit Tests)
Switch to UnitTestingWebAPI.Domain and add the following classes:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
public class Article { public int ID { get ; set ; } public string Title { get ; set ; } public string Contents { get ; set ; } public string Author { get ; set ; } public string URL { get ; set ; } public DateTime DateCreated { get ; set ; } public DateTime DateEdited { get ; set ; } public int BlogID { get ; set ; } public virtual Blog Blog { get ; set ; } public Article() { } } |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
public class Blog { public int ID { get ; set ; } public string Name { get ; set ; } public string URL { get ; set ; } public string Owner { get ; set ; } public DateTime DateCreated { get ; set ; } public virtual ICollection<Article> Articles { get ; set ; } public Blog() { Articles = new HashSet<Article>(); } } |
Repository Layer
Switch to UnitTestingWebAPI.Data, install Entity Framework from Nuget packages, add a reference toUnitTestingWebAPI.Data and add the following classes (create the respective folder if required):
1
2
3
4
5
6
7
8
9
10
11
12
13
|
public class ArticleConfiguration : EntityTypeConfiguration<Article> { public ArticleConfiguration() { 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" ); } } |
1
2
3
4
5
6
7
8
9
10
11
|
public class BlogConfiguration : EntityTypeConfiguration<Blog> { public BlogConfiguration() { 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" ); } } |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
public class BloggerEntities : DbContext { public BloggerEntities() : base ( "BloggerEntities" ) { Configuration.ProxyCreationEnabled = false ; } public DbSet<Blog> Blogs { get ; set ; } public DbSet<Article> Articles { get ; set ; } public virtual void Commit() { base .SaveChanges(); } protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Configurations.Add( new ArticleConfiguration()); modelBuilder.Configurations.Add( new BlogConfiguration()); } } |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
|
public class BloggerInitializer : DropCreateDatabaseIfModelChanges<BloggerEntities> { protected override void Seed(BloggerEntities context) { GetBlogs().ForEach(b => context.Blogs.Add(b)); context.Commit(); } public static List<Blog> GetBlogs() { List<Blog> _blogs = new List<Blog>(); // Add two Blogs Blog _chsakellsBlog = new Blog() { Name = "chsakell's Blog" , Owner = "Chris Sakellarios" , Articles = GetChsakellsArticles() }; Blog _dotNetCodeGeeks = new Blog() { Name = "DotNETCodeGeeks" , URL = "dotnetcodegeeks" , Owner = ".NET Code Geeks" , Articles = GetDotNETGeeksArticles() }; _blogs.Add(_chsakellsBlog); _blogs.Add(_dotNetCodeGeeks); return _blogs; } public static List<Article> GetChsakellsArticles() { List<Article> _articles = new List<Article>(); Article _oData = new Article() { Author = "Chris S." , Title = "ASP.NET Web API feat. OData" , Contents = @"OData is an open standard protocol allowing the creation and consumption of queryable and interoperable RESTful APIs. It was initiated by Microsoft and it’s mostly known to .NET Developers from WCF Data Services. There are many other server platforms supporting OData services such as Node.js, PHP, Java and SQL Server Reporting Services. More over, Web API also supports OData and this post will show you how to integrate those two.." }; Article _wcfCustomSecurity= new Article() { Author = "Chris S." , Title = "Secure WCF Services with custom encrypted tokens" , Contents = @"Windows Communication Foundation framework comes with a lot of options out of the box, concerning the security logic you will apply to your services. Different bindings can be used for certain kind and levels of security. Even the BasicHttpBinding binding supports some types of security. There are some times though where you cannot or don’t want to use WCF security available options and hence, you need to develop your own authentication logic accoarding to your business needs." }; _articles.Add(_oData); _articles.Add(_wcfCustomSecurity); return _articles; } public static List<Article> GetDotNETGeeksArticles() { List<Article> _articles = new List<Article>(); Article _angularFeatWebAPI = new Article() { Author = "Gordon Beeming" , Title = "AngularJS feat. Web API" , Contents = @"Developing Web applications using AngularJS and Web API can be quite amuzing. You can pick this architecture in case you have in mind a web application with limitted page refreshes or post backs to the server while each application’s View is based on partial data retrieved from it." }; _articles.Add(_angularFeatWebAPI); return _articles; } public static List<Article> GetAllArticles() { List<Article> _articles = new List<Article>(); _articles.AddRange(GetChsakellsArticles()); _articles.AddRange(GetDotNETGeeksArticles()); return _articles; } } |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
public class Disposable : IDisposable { private bool isDisposed; ~Disposable() { Dispose( false ); } public void Dispose() { Dispose( true ); GC.SuppressFinalize( this ); } private void Dispose( bool disposing) { if (!isDisposed && disposing) { DisposeCore(); } isDisposed = true ; } // Ovveride this to dispose custom objects protected virtual void DisposeCore() { } } |
1
2
3
4
|
public interface IDbFactory : IDisposable { BloggerEntities Init(); } |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
public class DbFactory : Disposable, IDbFactory { BloggerEntities dbContext; public BloggerEntities Init() { return dbContext ?? (dbContext = new BloggerEntities()); } protected override void DisposeCore() { if (dbContext != null ) dbContext.Dispose(); } } |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
public interface IRepository<T> where T : class { // Marks an entity as new void Add(T entity); // Marks an entity as modified void Update(T entity); // Marks an entity to be removed void Delete(T entity); void Delete(Expression<Func<T, bool >> where ); // Get an entity by int id T GetById( int id); // Get an entity using delegate T Get(Expression<Func<T, bool >> where ); // Gets all entities of type T IEnumerable<T> GetAll(); // Gets entities using delegate IEnumerable<T> GetMany(Expression<Func<T, bool >> where ); } |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
|
public abstract class RepositoryBase<T> where T : class { #region Properties private BloggerEntities dataContext; private readonly IDbSet<T> dbSet; protected IDbFactory DbFactory { get ; private set ; } protected BloggerEntities DbContext { get { return dataContext ?? (dataContext = DbFactory.Init()); } } #endregion protected RepositoryBase(IDbFactory dbFactory) { DbFactory = dbFactory; dbSet = DbContext.Set<T>(); } #region Implementation public virtual void Add(T entity) { dbSet.Add(entity); } public virtual void Update(T entity) { dbSet.Attach(entity); dataContext.Entry(entity).State = EntityState.Modified; } public virtual void Delete(T entity) { dbSet.Remove(entity); } public virtual void Delete(Expression<Func<T, bool >> where ) { IEnumerable<T> objects = dbSet.Where<T>( where ).AsEnumerable(); foreach (T obj in objects) dbSet.Remove(obj); } public virtual T GetById( int id) { return dbSet.Find(id); } public virtual IEnumerable<T> GetAll() { return dbSet.ToList(); } public virtual IEnumerable<T> GetMany(Expression<Func<T, bool >> where ) { return dbSet.Where( where ).ToList(); } public T Get(Expression<Func<T, bool >> where ) { return dbSet.Where( where ).FirstOrDefault<T>(); } #endregion } |
1
2
3
4
|
public interface IUnitOfWork { void Commit(); } |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
public class UnitOfWork : IUnitOfWork { private readonly IDbFactory dbFactory; private BloggerEntities dbContext; public UnitOfWork(IDbFactory dbFactory) { this .dbFactory = dbFactory; } public BloggerEntities DbContext { get { return dbContext ?? (dbContext = dbFactory.Init()); } } public void Commit() { DbContext.Commit(); } } |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
public class ArticleRepository : RepositoryBase<Article>, IArticleRepository { public ArticleRepository(IDbFactory dbFactory) : base (dbFactory) { } public Article GetArticleByTitle( string articleTitle) { var _article = this .DbContext.Articles.Where(b => b.Title == articleTitle).FirstOrDefault(); return _article; } } public interface IArticleRepository : IRepository<Article> { Article GetArticleByTitle( string articleTitle); } |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
public class BlogRepository : RepositoryBase<Blog>, IBlogRepository { public BlogRepository(IDbFactory dbFactory) : base (dbFactory) { } public Blog GetBlogByName( string blogName) { var _blog = this .DbContext.Blogs.Where(b => b.Name == blogName).FirstOrDefault(); return _blog; } } public interface IBlogRepository : IRepository<Blog> { Blog GetBlogByName( string blogName); } |
Service layer
Switch to UnitTestingWebAPI.Service project, add references toUnitTestingWebAPI.Domain,UnitTestingWebAPI.Data and add the following two files:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
|
// operations you want to expose public interface IArticleService { IEnumerable<Article> GetArticles( string name = null ); Article GetArticle( int id); Article GetArticle( string name); void CreateArticle(Article article); void UpdateArticle(Article article); void DeleteArticle(Article article); void SaveArticle(); } public class ArticleService : IArticleService { private readonly IArticleRepository articlesRepository; private readonly IUnitOfWork unitOfWork; public ArticleService(IArticleRepository articlesRepository, IUnitOfWork unitOfWork) { this .articlesRepository = articlesRepository; this .unitOfWork = unitOfWork; } #region IArticleService Members public IEnumerable<Article> GetArticles( string title = null ) { if ( string .IsNullOrEmpty(title)) return articlesRepository.GetAll(); else return articlesRepository.GetAll().Where(c => c.Title.ToLower().Contains(title.ToLower())); } public Article GetArticle( int id) { var article = articlesRepository.GetById(id); return article; } public Article GetArticle( string title) { var article = articlesRepository.GetArticleByTitle(title); return article; } public void CreateArticle(Article article) { articlesRepository.Add(article); } public void UpdateArticle(Article article) { articlesRepository.Update(article); } public void DeleteArticle(Article article) { articlesRepository.Delete(article); } public void SaveArticle() { unitOfWork.Commit(); } #endregion } |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
|
// operations you want to expose public interface IBlogService { IEnumerable<Blog> GetBlogs( string name = null ); Blog GetBlog( int id); Blog GetBlog( string name); void CreateBlog(Blog blog); void UpdateBlog(Blog blog); void SaveBlog(); void DeleteBlog(Blog blog); } public class BlogService : IBlogService { private readonly IBlogRepository blogsRepository; private readonly IUnitOfWork unitOfWork; public BlogService(IBlogRepository blogsRepository, IUnitOfWork unitOfWork) { this .blogsRepository = blogsRepository; this .unitOfWork = unitOfWork; } #region IBlogService Members public IEnumerable<Blog> GetBlogs( string name = null ) { if ( string .IsNullOrEmpty(name)) return blogsRepository.GetAll(); else return blogsRepository.GetAll().Where(c => c.Name == name); } public Blog GetBlog( int id) { var blog = blogsRepository.GetById(id); return blog; } public Blog GetBlog( string name) { var blog = blogsRepository.GetBlogByName(name); return blog; } public void CreateBlog(Blog blog) { blogsRepository.Add(blog); } public void UpdateBlog(Blog blog) { blogsRepository.Update(blog); } public void DeleteBlog(Blog blog) { blogsRepository.Delete(blog); } public void SaveBlog() { unitOfWork.Commit(); } #endregion } |
Web API Core Components
Switch to UnitTestingWebAPI.API.Core and add references to UnitTestingWebAPI.API.Domain andUnitTestingWebAPI.API.Service projects. Install the following packages from Nuget Packages:
- Entity Framework
- Microsoft.AspNet.WebApi.Core
- Microsoft.AspNet.WebApi.Client
Add the following Web API Controllers to a Controllers folder:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
|
public class ArticlesController : ApiController { private IArticleService _articleService; public ArticlesController(IArticleService articleService) { _articleService = articleService; } // GET: api/Articles public IEnumerable<Article> GetArticles() { return _articleService.GetArticles(); } // GET: api/Articles/5 [ResponseType( typeof (Article))] public IHttpActionResult GetArticle( int id) { Article article = _articleService.GetArticle(id); if (article == null ) { return NotFound(); } return Ok(article); } // PUT: api/Articles/5 [ResponseType( typeof ( void ))] public IHttpActionResult PutArticle( int id, Article article) { if (!ModelState.IsValid) { return BadRequest(ModelState); } if (id != article.ID) { return BadRequest(); } _articleService.UpdateArticle(article); try { _articleService.SaveArticle(); } catch (DbUpdateConcurrencyException) { if (!ArticleExists(id)) { return NotFound(); } else { throw ; } } return StatusCode(HttpStatusCode.NoContent); } // POST: api/Articles [ResponseType( typeof (Article))] public IHttpActionResult PostArticle(Article article) { if (!ModelState.IsValid) { return BadRequest(ModelState); } _articleService.CreateArticle(article); return CreatedAtRoute( "DefaultApi" , new { id = article.ID }, article); } // DELETE: api/Articles/5 [ResponseType( typeof (Article))] public IHttpActionResult DeleteArticle( int id) { Article article = _articleService.GetArticle(id); if (article == null ) { return NotFound(); } _articleService.DeleteArticle(article); return Ok(article); } private bool ArticleExists( int id) { return _articleService.GetArticle(id) != null ; } } |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
|
public class BlogsController : ApiController { private IBlogService _blogService; public BlogsController(IBlogService blogService) { _blogService = blogService; } // GET: api/Blogs public IEnumerable<Blog> GetBlogs() { return _blogService.GetBlogs(); } // GET: api/Blogs/5 [ResponseType( typeof (Blog))] public IHttpActionResult GetBlog( int id) { Blog blog = _blogService.GetBlog(id); if (blog == null ) { return NotFound(); } return Ok(blog); } // PUT: api/Blogs/5 [ResponseType( typeof ( void ))] public IHttpActionResult PutBlog( int id, Blog blog) { if (!ModelState.IsValid) { return BadRequest(ModelState); } if (id != blog.ID) { return BadRequest(); } _blogService.UpdateBlog(blog); try { _blogService.SaveBlog(); } catch (DbUpdateConcurrencyException) { if (!BlogExists(id)) { return NotFound(); } else { throw ; } } return StatusCode(HttpStatusCode.NoContent); } // POST: api/Blogs [ResponseType( typeof (Blog))] public IHttpActionResult PostBlog(Blog blog) { if (!ModelState.IsValid) { return BadRequest(ModelState); } _blogService.CreateBlog(blog); return CreatedAtRoute( "DefaultApi" , new { id = blog.ID }, blog); } // DELETE: api/Blogs/5 [ResponseType( typeof (Blog))] public IHttpActionResult DeleteBlog( int id) { Blog blog = _blogService.GetBlog(id); if (blog == null ) { return NotFound(); } _blogService.DeleteBlog(blog); return Ok(blog); } private bool BlogExists( int id) { return _blogService.GetBlog(id) != null ; } } |
Add the following filter which when applied, it reverses the order of a List of articles:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
public class ArticlesReversedFilter : ActionFilterAttribute { public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext) { var objectContent = actionExecutedContext.Response.Content as ObjectContent; if (objectContent != null ) { List<Article> _articles = objectContent.Value as List<Article>; if (_articles != null && _articles.Count > 0) { _articles.Reverse(); } } } } |
Add the following MediaTypeFormatter which can return a comma serated representation of articles:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
|
public class ArticleFormatter : BufferedMediaTypeFormatter { public ArticleFormatter() { SupportedMediaTypes.Add( new MediaTypeHeaderValue( "application/article" )); } public override bool CanReadType(Type type) { return false ; } public override bool CanWriteType(Type type) { //for single article object if (type == typeof (Article)) return true ; else { // for multiple article objects Type _type = typeof (IEnumerable<Article>); return _type.IsAssignableFrom(type); } } public override void WriteToStream(Type type, object value, Stream writeStream, HttpContent content) { using (StreamWriter writer = new StreamWriter(writeStream)) { var articles = value as IEnumerable<Article>; if (articles != null ) { foreach ( var article in articles) { writer.Write(String.Format( "[{0},\"{1}\",\"{2}\",\"{3}\",\"{4}\"]" , article.ID, article.Title, article.Author, article.URL, article.Contents)); } } else { var _article = value as Article; if (_article == null ) { throw new InvalidOperationException( "Cannot serialize type" ); } writer.Write(String.Format( "[{0},\"{1}\",\"{2}\",\"{3}\",\"{4}\"]" , _article.ID, _article.Title, _article.Author, _article.URL, _article.Contents)); } } } } |
Add the following two Message Handlers. The first one is responsible to add a custom header in the response and the second one is able to terminate the request if applied:
1
2
3
4
5
6
7
8
9
10
11
|
public class HeaderAppenderHandler : DelegatingHandler { async protected override Task<HttpResponseMessage> SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { HttpResponseMessage response = await base .SendAsync(request, cancellationToken); response.Headers.Add( "X-WebAPI-Header" , "Web API Unit testing in chsakell's blog." ); return response; } } |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
public class EndRequestHandler : DelegatingHandler { async protected override Task<HttpResponseMessage> SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { if (request.RequestUri.AbsoluteUri.Contains( "test" )) { var response = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent( "Unit testing message handlers!" ) }; var tsc = new TaskCompletionSource<HttpResponseMessage>(); tsc.SetResult(response); return await tsc.Task; } else { return await base .SendAsync(request, cancellationToken); } } } |
Add the following DefaultAssembliesResolver which will be used for Controller registration from the Web Application project:
1
2
3
4
5
6
7
8
9
10
11
|
public class CustomAssembliesResolver : DefaultAssembliesResolver { public override ICollection<Assembly> GetAssemblies() { var baseAssemblies = base .GetAssemblies().ToList(); var assemblies = new List<Assembly>(baseAssemblies) { typeof (BlogsController).Assembly }; baseAssemblies.AddRange(assemblies); return baseAssemblies.Distinct().ToList(); } } |
ASP.NET Web Application
Switch to UnitTestingWebAPI.API web application project and add references toUnitTestingWebAPI.Core,UnitTestingWebAPI.Data and UnitTestingWebAPI.Service. You will also need to install the following Nuget packages:
- 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
Add a Global Configuration file if not exists and set the database initializer:
1
2
3
4
5
6
7
8
9
|
public class Global : System.Web.HttpApplication { protected void Application_Start( object sender, EventArgs e) { // Init database System.Data.Entity.Database.SetInitializer( new BloggerInitializer()); } } |
Also make sure you add a relevant connection string in the Web.config file:
1
2
3
|
< connectionStrings > < add name = "BloggerEntities" connectionString = "Data Source=(localdb)\v11.0;Initial Catalog=BloggerDB;Integrated Security=True" providerName = "System.Data.SqlClient" /> </ connectionStrings > |
External Controller Registration
Create an Owin Startup.cs file at the root of the Web application and paste the following code. This code will ensure to use WebApi controllers from the UnitTestingWebAPI.API.Core project (CustomAssembliesResolver) and inject the appropriate repositories and services when required (autofac configuration):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
public class Startup { public void Configuration(IAppBuilder appBuilder) { var config = new HttpConfiguration(); config.Services.Replace( typeof (IAssembliesResolver), new CustomAssembliesResolver()); config.Formatters.Add( new ArticleFormatter()); config.Routes.MapHttpRoute( name: "DefaultApi" , routeTemplate: "api/{controller}/{id}" , defaults: new { id = RouteParameter.Optional } ); // Autofac configuration var builder = new ContainerBuilder(); builder.RegisterApiControllers( typeof (BlogsController).Assembly); builder.RegisterType<UnitOfWork>().As<IUnitOfWork>().InstancePerRequest(); builder.RegisterType<DbFactory>().As<IDbFactory>().InstancePerRequest(); //Repositories builder.RegisterAssemblyTypes( typeof (BlogRepository).Assembly) .Where(t => t.Name.EndsWith( "Repository" )) .AsImplementedInterfaces().InstancePerRequest(); // Services builder.RegisterAssemblyTypes( typeof (ArticleService).Assembly) .Where(t => t.Name.EndsWith( "Service" )) .AsImplementedInterfaces().InstancePerRequest(); IContainer container = builder.Build(); config.DependencyResolver = new AutofacWebApiDependencyResolver(container); appBuilder.UseWebApi(config); } } |
At this point you should be able to fire the Web application and request articles or blogs using the following requests (port may be different in yours):
Section Two: Unit Testing
We have completed structuring our application and it’s time to unit test all of our components. Switch toUnitTestingWebAPI.Tests class library and add references to UnitTestingWebAPI.Domain,UnitTestingWebAPI.Data,UnitTestingWebAPI.Service and UnitTestingWebAPI.API.Core. Also make sure you install the following Nuget Packages:
- Entity Framework
- Microsoft.AspNet.WebApi.Core
- Microsoft.AspNet.WebApi.Client
- Microsoft.AspNet.WebApi.Owin
- Microsoft.AspNet.WebApi.SelfHost
- Micoroft.Owin
- Owin
- Micoroft.Owin.Hosting
- Micoroft.Owin.Host.HttpListener
- Autofac.WebApi2
- NUnit
- NUnitTestAdapter
As you see we are going to use NUnit to write our unit tests.
Services Unit Testing
When writing Unit tests, first you need to setup or initiate some variables to be used for the unit tests. WithNUnit this is done via a function with an attribute Setup applied on it. This very function will run before any NUnit test is executed. Unit testing the Service layer is the first thing you need to do since all the Controller’s constructors are injected with Services. Hence, you need to emulate repositories and service behavior before starting unit testing WebAPI Core components. In this example we ‘ll see how to emulate theArticleSevice. This service’s constructor is injected with instances of IArticleRepository and IUnitOfWorkso all we have to do is create two “special” instances and inject them.
1
2
3
4
5
|
public ArticleService(IArticleRepository articlesRepository, IUnitOfWork unitOfWork) { this .articlesRepository = articlesRepository; this .unitOfWork = unitOfWork; } |
I said “special” cause those instance are not going to be real instances that actually can access the database.
Attention
Unit tests must run in memory and shouldn’t access databases. All core functionality must be emulated by using frameworks such as Mock in our case. This way automated tests will be much more faster. The basic purpose of unit tests is more testing component behavior rather than testing real results.
Let’s procceed with testing the ArticleService. Create a file named ServiceTests and for start add the following code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
|
[TestFixture] public class ServicesTests { #region Variables IArticleService _articleService; IArticleRepository _articleRepository; IUnitOfWork _unitOfWork; List<Article> _randomArticles; #endregion #region Setup [SetUp] public void Setup() { _randomArticles = SetupArticles(); _articleRepository = SetupArticleRepository(); _unitOfWork = new Mock<IUnitOfWork>().Object; _articleService = new ArticleService(_articleRepository, _unitOfWork); } public List<Article> SetupArticles() { int _counter = new int (); List<Article> _articles = BloggerInitializer.GetAllArticles(); foreach (Article _article in _articles) _article.ID = ++_counter; return _articles; } public IArticleRepository SetupArticleRepository() { // Init repository var repo = new Mock<IArticleRepository>(); // Setup mocking behavior repo.Setup(r => r.GetAll()).Returns(_randomArticles); repo.Setup(r => r.GetById(It.IsAny< int >())) .Returns( new Func< int , Article>( id => _randomArticles.Find(a => a.ID.Equals(id)))); repo.Setup(r => r.Add(It.IsAny<Article>())) .Callback( new Action<Article>(newArticle => { dynamic maxArticleID = _randomArticles.Last().ID; dynamic nextArticleID = maxArticleID + 1; newArticle.ID = nextArticleID; newArticle.DateCreated = DateTime.Now; _randomArticles.Add(newArticle); })); repo.Setup(r => r.Update(It.IsAny<Article>())) .Callback( new Action<Article>(x => { var oldArticle = _randomArticles.Find(a => a.ID == x.ID); oldArticle.DateEdited = DateTime.Now; oldArticle = x; })); repo.Setup(r => r.Delete(It.IsAny<Article>())) .Callback( new Action<Article>(x => { var _articleToRemove = _randomArticles.Find(a => a.ID == x.ID); if (_articleToRemove != null ) _randomArticles.Remove(_articleToRemove); })); // Return mock implementation return repo.Object; } #endregion } } |
In the SetupArticleRepository() function we emulate our _articleRepository behavior, in other words we setup what results are expected from this repository instance when a specific function is called. Then we inject this instance in our _articleService’s constructor and we are ready to go. Let’s say that we want to test that the_articleService.GetArticles() behaves as expected. Add the following NUnit test in the same file:
1
2
3
4
5
6
7
|
[Test] public void ServiceShouldReturnAllArticles() { var articles = _articleService.GetArticles(); Assert.That(articles, Is.EqualTo(_randomArticles)); } |
Build the Tests project, run the test and make sure it passes. In the same way create the following tests:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
|
[Test] public void ServiceShouldReturnRightArticle() { var wcfSecurityArticle = _articleService.GetArticle(2); Assert.That(wcfSecurityArticle, Is.EqualTo(_randomArticles.Find(a => a.Title.Contains( "Secure WCF Services" )))); } [Test] public void ServiceShouldAddNewArticle() { var _newArticle = new Article() { Author = "Chris Sakellarios" , Contents = "If you are an ASP.NET MVC developer, you will certainly.." , Title = "URL Rooting in ASP.NET (Web Forms)" , }; int _maxArticleIDBeforeAdd = _randomArticles.Max(a => a.ID); _articleService.CreateArticle(_newArticle); Assert.That(_newArticle, Is.EqualTo(_randomArticles.Last())); Assert.That(_maxArticleIDBeforeAdd + 1, Is.EqualTo(_randomArticles.Last().ID)); } [Test] public void ServiceShouldUpdateArticle() { var _firstArticle = _randomArticles.First(); _firstArticle.Title = "OData feat. ASP.NET Web API" ; // reversed<img width="16" height="16" class="wp-smiley emoji" draggable="false" alt=":-)" src="https://s1.wp.com/wp-content/mu-plugins/wpcom-smileys/simple-smile.svg" style="height: 1em; max-height: 1em;"> _articleService.UpdateArticle(_firstArticle); Assert.That(_firstArticle.DateEdited, Is.Not.EqualTo(DateTime.MinValue)); Assert.That(_firstArticle.ID, Is.EqualTo(1)); // hasn't changed } [Test] public void ServiceShouldDeleteArticle() { int maxID = _randomArticles.Max(a => a.ID); // Before removal var _lastArticle = _randomArticles.Last(); // Remove last article _articleService.DeleteArticle(_lastArticle); Assert.That(maxID, Is.GreaterThan(_randomArticles.Max(a => a.ID))); // Max reduced by 1 } |
Web API Controllers Unit Testing
Now that we are familiar with emulating our services behavior we can procceed with unit testing Web API Controllers. First thing we need to do is Setup the variables to be used through our test, so create aControllerTests.cs file and paste the following code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
|
[TestFixture] public class ControllerTests { #region Variables IArticleService _articleService; IArticleRepository _articleRepository; IUnitOfWork _unitOfWork; List<Article> _randomArticles; #endregion #region Setup [SetUp] public void Setup() { _randomArticles = SetupArticles(); _articleRepository = SetupArticleRepository(); _unitOfWork = new Mock<IUnitOfWork>().Object; _articleService = new ArticleService(_articleRepository, _unitOfWork); } public List<Article> SetupArticles() { int _counter = new int (); List<Article> _articles = BloggerInitializer.GetAllArticles(); foreach (Article _article in _articles) _article.ID = ++_counter; return _articles; } public IArticleRepository SetupArticleRepository() { // Init repository var repo = new Mock<IArticleRepository>(); // Setup mocking behavior repo.Setup(r => r.GetAll()).Returns(_randomArticles); repo.Setup(r => r.GetById(It.IsAny< int >())) .Returns( new Func< int , Article>( id => _randomArticles.Find(a => a.ID.Equals(id)))); repo.Setup(r => r.Add(It.IsAny<Article>())) .Callback( new Action<Article>(newArticle => { dynamic maxArticleID = _randomArticles.Last().ID; dynamic nextArticleID = maxArticleID + 1; newArticle.ID = nextArticleID; newArticle.DateCreated = DateTime.Now; _randomArticles.Add(newArticle); })); repo.Setup(r => r.Update(It.IsAny<Article>())) .Callback( new Action<Article>(x => { var oldArticle = _randomArticles.Find(a => a.ID == x.ID); oldArticle.DateEdited = DateTime.Now; oldArticle.URL = x.URL; oldArticle.Title = x.Title; oldArticle.Contents = x.Contents; oldArticle.BlogID = x.BlogID; })); repo.Setup(r => r.Delete(It.IsAny<Article>())) .Callback( new Action<Article>(x => { var _articleToRemove = _randomArticles.Find(a => a.ID == x.ID); if (_articleToRemove != null ) _randomArticles.Remove(_articleToRemove); })); // Return mock implementation return repo.Object; } #endregion } } |
WebAPI Controller classes are classes just like all others so we can test them respectively. Let’s see if the_articlesController.GetArticles() does return all articles available:
1
2
3
4
5
6
7
8
9
|
[Test] public void ControlerShouldReturnAllArticles() { var _articlesController = new ArticlesController(_articleService); var result = _articlesController.GetArticles(); CollectionAssert.AreEqual(result, _randomArticles); } |
The most important line here is the highlighted one where the _articleService instance injection will ensure the service’s behavior.
In the same way we ensure that the last article is returned when invoking _articlesController.GetArticle(3)since we setup only 3 articles.
1
2
3
4
5
6
7
8
9
10
|
[Test] public void ControlerShouldReturnLastArticle() { var _articlesController = new ArticlesController(_articleService); var result = _articlesController.GetArticle(3) as OkNegotiatedContentResult<Article>; Assert.IsNotNull(result); Assert.AreEqual(result.Content.Title, _randomArticles.Last().Title); } |
Let’s test that an invalid Update operation must fail and return a BadRequestResult. Recall the Update operation setup on the _articleRepository:
1
2
3
4
5
6
7
8
9
10
|
repo.Setup(r => r.Update(It.IsAny<Article>())) .Callback( new Action<Article>(x => { var oldArticle = _randomArticles.Find(a => a.ID == x.ID); oldArticle.DateEdited = DateTime.Now; oldArticle.URL = x.URL; oldArticle.Title = x.Title; oldArticle.Contents = x.Contents; oldArticle.BlogID = x.BlogID; })); |
So If we pass an non existing article this update should fail:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
[Test] public void ControlerShouldPutReturnBadRequestResult() { var _articlesController = new ArticlesController(_articleService) { Configuration = new HttpConfiguration(), Request = new HttpRequestMessage { Method = HttpMethod.Put, } }; var badresult = _articlesController.PutArticle(-1, new Article() { Title = "Unknown Article" }); Assert.That(badresult, Is.TypeOf<BadRequestResult>()); } |
Complete the Controller Unit testing by adding the following three tests which tests that updating first article succeeds, post new article succeeds and post new article fails respectivelly.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
|
[Test] public void ControlerShouldPutUpdateFirstArticle() { var _articlesController = new ArticlesController(_articleService) { Configuration = new HttpConfiguration(), Request = new HttpRequestMessage { Method = HttpMethod.Put, } }; IHttpActionResult updateResult = _articlesController.PutArticle(1, new Article() { ID = 1, Title = "ASP.NET Web API feat. OData" , Contents = @"OData is an open standard protocol.." }) as IHttpActionResult; Assert.That(updateResult, Is.TypeOf<StatusCodeResult>()); StatusCodeResult statusCodeResult = updateResult as StatusCodeResult; Assert.That(statusCodeResult.StatusCode, Is.EqualTo(HttpStatusCode.NoContent)); } [Test] public void ControlerShouldPostNewArticle() { var article = new Article { Title = "Web API Unit Testing" , Author = "Chris Sakellarios" , DateCreated = DateTime.Now, Contents = "Unit testing Web API.." }; var _articlesController = new ArticlesController(_articleService) { Configuration = new HttpConfiguration(), Request = new HttpRequestMessage { Method = HttpMethod.Post, } }; _articlesController.Configuration.MapHttpAttributeRoutes(); _articlesController.Configuration.EnsureInitialized(); _articlesController.RequestContext.RouteData = new HttpRouteData( new HttpRoute(), new HttpRouteValueDictionary { { "_articlesController" , "Articles" } }); var result = _articlesController.PostArticle(article) as CreatedAtRouteNegotiatedContentResult<Article>; Assert.That(result.RouteName, Is.EqualTo( "DefaultApi" )); Assert.That(result.Content.ID, Is.EqualTo(result.RouteValues[ "id" ])); Assert.That(result.Content.ID, Is.EqualTo(_randomArticles.Max(a => a.ID))); } [Test] public void ControlerShouldNotPostNewArticle() { var article = new Article { Title = "Web API Unit Testing" , Author = "Chris Sakellarios" , DateCreated = DateTime.Now, Contents = null }; var _articlesController = new ArticlesController(_articleService) { Configuration = new HttpConfiguration(), Request = new HttpRequestMessage { Method = HttpMethod.Post, } }; _articlesController.Configuration.MapHttpAttributeRoutes(); _articlesController.Configuration.EnsureInitialized(); _articlesController.RequestContext.RouteData = new HttpRouteData( new HttpRoute(), new HttpRouteValueDictionary { { "Controller" , "Articles" } }); _articlesController.ModelState.AddModelError( "Contents" , "Contents is required field" ); var result = _articlesController.PostArticle(article) as InvalidModelStateResult; Assert.That(result.ModelState.Count, Is.EqualTo(1)); Assert.That(result.ModelState.IsValid, Is.EqualTo( false )); } |
Take a good look the highlighted lines and see that we can unit test several aspects of our requests, such asCodeStatus returned or routing properties.
Message Handlers Unit Testing
You can test Message Handlers by creating an instance of HttpMessageInvoker, passing the Message Handler instance you want to test and invoke the SendAsync function. Create a MessageHandlerTests.cs file and paste the Setup code first:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
[TestFixture] public class MessageHandlerTests { #region Variables private EndRequestHandler _endRequestHandler; private HeaderAppenderHandler _headerAppenderHandler; #endregion #region Setup [SetUp] public void Setup() { // Direct MessageHandler test _endRequestHandler = new EndRequestHandler(); _headerAppenderHandler = new HeaderAppenderHandler() { InnerHandler = _endRequestHandler }; } #endregion } } |
We setup the HeaderAppenderHandler’s inner handler another Handler that will terminate the request. Recall that the EndRequestHandler will end the request only if the Uri contains a test literal. Let’s write the test now:
1
2
3
4
5
6
7
8
9
10
11
|
[Test] public async void ShouldAppendCustomHeader() { var invoker = new HttpMessageInvoker(_headerAppenderHandler); var result = await invoker.SendAsync( new HttpRequestMessage(HttpMethod.Get, Assert.That(result.Headers.Contains( "X-WebAPI-Header" ), Is.True); Assert.That(result.Content.ReadAsStringAsync().Result, Is.EqualTo( "Unit testing message handlers!" )); } |
Now let’s pick up tha pace a little bit and make things quite more interesting. Let’s say you want to make an integration test that is you want to test the actual behavior of your Message Handler when a request is dispatched to a controller’s action. This would require to host the Web API and then run the unit test but is this possible here? Of course it, and this is the beauty when you create a highly loosely coupled application. All you have to do is Self host the web api and setup the appropriate configurations. In our case we are gonna host the web api and also setup Moq instances to be injected for Repositories and Services. Add the following Startup.cs file in the UnitTestingWebAPI.Tests project:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
|
public class Startup { public void Configuration(IAppBuilder appBuilder) { var config = new HttpConfiguration(); config.MessageHandlers.Add( new HeaderAppenderHandler()); config.MessageHandlers.Add( new EndRequestHandler()); config.Filters.Add( new ArticlesReversedFilter()); config.Services.Replace( typeof (IAssembliesResolver), new CustomAssembliesResolver()); config.Routes.MapHttpRoute( name: "DefaultApi" , routeTemplate: "api/{controller}/{id}" , defaults: new { id = RouteParameter.Optional } ); config.MapHttpAttributeRoutes(); // Autofac configuration var builder = new ContainerBuilder(); builder.RegisterApiControllers( typeof (ArticlesController).Assembly); // Unit of Work var _unitOfWork = new Mock<IUnitOfWork>(); builder.RegisterInstance(_unitOfWork.Object).As<IUnitOfWork>(); //Repositories var _articlesRepository = new Mock<IArticleRepository>(); _articlesRepository.Setup(x => x.GetAll()).Returns( BloggerInitializer.GetAllArticles() ); builder.RegisterInstance(_articlesRepository.Object).As<IArticleRepository>(); var _blogsRepository = new Mock<IBlogRepository>(); _blogsRepository.Setup(x => x.GetAll()).Returns( BloggerInitializer.GetBlogs ); builder.RegisterInstance(_blogsRepository.Object).As<IBlogRepository>(); // Services builder.RegisterAssemblyTypes( typeof (ArticleService).Assembly) .Where(t => t.Name.EndsWith( "Service" )) .AsImplementedInterfaces().InstancePerRequest(); builder.RegisterInstance( new ArticleService(_articlesRepository.Object, _unitOfWork.Object)); builder.RegisterInstance( new BlogService(_blogsRepository.Object, _unitOfWork.Object)); IContainer container = builder.Build(); config.DependencyResolver = new AutofacWebApiDependencyResolver(container); appBuilder.UseWebApi(config); } } |
Notice that it’s quite similar to the Startup class we wrote for the Web Application project, except that fake repositories and services are used. Now let’s return and write this integration test:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
[Test] public void ShouldCallToControllerActionAppendCustomHeader() { //Arrange using (WebApp.Start<Startup>(address)) { HttpClient _client = new HttpClient(); var response = _client.GetAsync(address + "api/articles" ).Result; Assert.That(response.Headers.Contains( "X-WebAPI-Header" ), Is.True); var _returnedArticles = response.Content.ReadAsAsync<List<Article>>().Result; Assert.That(_returnedArticles.Count, Is.EqualTo( BloggerInitializer.GetAllArticles().Count)); } } |
Since the request doesn’t contain a test literal, it will reach the controller’s action and also bring the results. Notice also that the custom header has also been appended.
Action Filters Unit Testing
Recall that we had created an ArticlesReversedFilter that when applied it reverses the order of the articles that should be returned. We can either direct unit test this filter or run an integration one. We will see how to do both of them. To direct test an action filter you need to run it’s OnActionExecuted function by passing a instance of HttpActionExecutedContext as a parameter as follow:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
[Test] public void ShouldSortArticlesByTitle() { var filter = new ArticlesReversedFilter(); var executedContext = new HttpActionExecutedContext( new HttpActionContext { Response = new HttpResponseMessage(), }, null ); executedContext.Response.Content = new ObjectContent<List<Article>>( new List<Article>(_articles), new JsonMediaTypeFormatter()); filter.OnActionExecuted(executedContext); var _returnedArticles = executedContext.Response.Content.ReadAsAsync<List<Article>>().Result; Assert.That(_returnedArticles.First(), Is.EqualTo(_articles.Last())); } |
To run an integration test you need to self host the Web API and make the appropriate request. Mind that the filter must be registered in the Startup configuration class.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
[Test] public void ShouldCallToControllerActionReverseArticles() { //Arrange using (WebApp.Start<Startup>(address)) { HttpClient _client = new HttpClient(); var response = _client.GetAsync(address + "api/articles" ).Result; var _returnedArticles = response.Content.ReadAsAsync<List<Article>>().Result; Assert.That(_returnedArticles.First().Title, Is.EqualTo(BloggerInitializer.GetAllArticles().Last().Title)); } } |
Media Type formatters Unit Testing
You have created some custom Media Type formatters and you want to test their behavior. Recall theArticleFormatter we created in the UnitTestingWebAPI.API.Core project and it’s able to return a comma separated string representation of articles. It can only write Article instances, not read ones or understand other type of classes. You need to set the Accept request header to application/article in order to apply the formatter. Let’s see the Setup configuration of our tests:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
[TestFixture] public class MediaTypeFormatterTests { #region Variables Blog _blog; Article _article; ArticleFormatter _formatter; #endregion #region Setup [SetUp] public void Setup() { _blog = BloggerInitializer.GetBlogs().First(); _article = BloggerInitializer.GetChsakellsArticles().First(); _formatter = new ArticleFormatter(); } #endregion } } |
You can test a MediaTypeFormatter by creating an instance of ObjectContent, passing the object to check if can be formatted by the respective formatter, and the formatter itself. If the formatter cannot read or write the passed object an exception will be thrown, otherwise not. For example let’s ensure that theArticleFormatter cannot understand Blog instances:
1
2
3
4
5
|
[Test] public void FormatterShouldThrowExceptionWhenUnsupportedType() { Assert.Throws<InvalidOperationException>(() => new ObjectContent<Blog>(_blog, _formatter)); } |
On the other hand it must work fine with parsing Article objects:
1
2
3
4
5
|
[Test] public void FormatterShouldNotThrowExceptionWhenArticle() { Assert.DoesNotThrow(() => new ObjectContent<Article>(_article, _formatter)); } |
And here are some other tests you can run against your custom Media type formatters:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
[Test] public void FormatterShouldHeaderBeSetCorrectly() { var content = new ObjectContent<Article>(_article, new ArticleFormatter()); Assert.That(content.Headers.ContentType.MediaType, Is.EqualTo( "application/article" )); } [Test] public async void FormatterShouldBeAbleToDeserializeArticle() { var content = new ObjectContent<Article>(_article, _formatter); var deserializedItem = await content.ReadAsAsync<Article>( new [] { _formatter }); Assert.That(_article, Is.SameAs(deserializedItem)); } [Test] public void FormatterShouldNotBeAbleToWriteUnsupportedType() { var canWriteBlog = _formatter.CanWriteType( typeof (Blog)); Assert.That(canWriteBlog, Is.False); } [Test] public void FormatterShouldBeAbleToWriteArticle() { var canWriteArticle = _formatter.CanWriteType( typeof (Article)); Assert.That(canWriteArticle, Is.True); } |
Routing Unit Testing
You want to test your routing configuration without hosting Web API. For this you ‘ll need a helper class that is able to return the Controller type or the controller’s action from an instance of a HttpControllerContext. Before this you have to create an HttpConfiguration with your routing configuration setup in it. Let’s see first the helper class:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
public class ControllerActionSelector { #region Variables HttpConfiguration config; HttpRequestMessage request; IHttpRouteData routeData; IHttpControllerSelector controllerSelector; HttpControllerContext controllerContext; #endregion #region Constructor public ControllerActionSelector(HttpConfiguration conf, HttpRequestMessage req) { config = conf; request = req; routeData = config.Routes.GetRouteData(request); request.Properties[HttpPropertyKeys.HttpRouteDataKey] = routeData; controllerSelector = new DefaultHttpControllerSelector(config); controllerContext = new HttpControllerContext(config, routeData, request); } #endregion #region Methods public string GetActionName() { if (controllerContext.ControllerDescriptor == null ) GetControllerType(); var actionSelector = new ApiControllerActionSelector(); var descriptor = actionSelector.SelectAction(controllerContext); return descriptor.ActionName; } public Type GetControllerType() { var descriptor = controllerSelector.SelectController(request); controllerContext.ControllerDescriptor = descriptor; return descriptor.ControllerType; } #endregion } |
And now the RouteTests Setup configuration:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
[TestFixture] public class RouteTests { #region Variables HttpConfiguration _config; #endregion #region Setup [SetUp] public void Setup() { _config = new HttpConfiguration(); _config.Routes.MapHttpRoute(name: "DefaultWebAPI" , routeTemplate: "api/{controller}/{id}" , defaults: new { id = RouteParameter.Optional }); } #endregion #region Helper methods public static string GetMethodName<T, U>(Expression<Func<T, U>> expression) { var method = expression.Body as MethodCallExpression; if (method != null ) return method.Method.Name; throw new ArgumentException( "Expression is wrong" ); } #endregion |
Let’s see that a request to api/articles/5 invokes the ArticlesController.GetArticle(int id) function:
1
2
3
4
5
6
7
8
9
10
11
|
[Test] public void RouteShouldControllerGetArticleIsInvoked() { var _actionSelector = new ControllerActionSelector(_config, request); Assert.That( typeof (ArticlesController), Is.EqualTo(_actionSelector.GetControllerType())); Assert.That(GetMethodName((ArticlesController c) => c.GetArticle(5)), Is.EqualTo(_actionSelector.GetActionName())); } |
We used some reflection to get controller’s action name. In the same way we can test that the post action is invoked:
1
2
3
4
5
6
7
8
9
10
|
[Test] public void RouteShouldPostArticleActionIsInvoked() { var _actionSelector = new ControllerActionSelector(_config, request); Assert.That(GetMethodName((ArticlesController c) => c.PostArticle( new Article())), Is.EqualTo(_actionSelector.GetActionName())); } |
You will probably want to test that an invalid route is not working:
1
2
3
4
5
6
7
8
9
|
[Test] public void RouteShouldInvalidRouteThrowException() { var request = new HttpRequestMessage(HttpMethod.Post, "http://www.chsakell.com/api/InvalidController/" ); var _actionSelector = new ControllerActionSelector(_config, request); Assert.Throws<HttpResponseException>(() => _actionSelector.GetActionName()); } |
Conclusion
We have seen many aspects of Unit Testing in Web API stack such as mocking the Service layer, unit testing Controllers, Message Handlers, Filters, Custom Media type Formatters and the routing configuration. Try to always writing unit tests for your application and you will never regret it. The most unit tests you write the more benefits you will get. For example a simple change in your repository may brake many aspects in your application. If the appropriate tests have been written, then in the first run you should see all broken parts of your application immediately. I hope you liked the post as much I did. You can download the source code for this project here.
In case you find my blog’s content interesting, register your email to receive notifications of new posts and follow chsakell’s Blog on its Facebook or Twitter accounts.