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.