Asp.Net Core 2.0 Web Api Unit Testing
There exist many kind of tests: unit tests, integration tests, acceptance test, UI tests.
The goal of this tutorial is to show how to write unit tests that gives us a better return on test investment.
UNIT TEST: Unit testing test behavior of a class and drive its implementation, run in memory and mocks the Data Access Layer
INTEGRATION TEST: test the entire system and focus on interactions between many classes, uses the real database and test the Data Access Layer and its value is to catch regressions
SUBCUTANEOUS TEST: test that operates just under the UI of an application. This is particularly valuable when doing functional testing of an application: when you want to test end-to-end behavior, but it’s difficult to test through the UI itself. Martin Fowler ( https://martinfowler.com/bliki/SubcutaneousTest.html)
UI TEST: tests the user interface and verifies that the entire application is working properly
Let’s consider the following testing pyramid
The further we go to the top of the pyramid
- tests are fewer
- the tests are less stable
- More complex
- Narrower scope
- Slower in execution time
The further we go down the pyramid
- Less we have tests
- the tests are more stable,
- Less complex
- Broader reach
- Faster in execution times
Let us consider the following project dependencies :
For the goal of this tutorial, I will not write unit test of all my projects even if all projects must be unit tested but I will proceed as follow:
- Unit Test my business Projet: LogCorner.BlogPost.Business
- Integration Test of my WebAPI project: LogCorner.BlogPost.Api
- UI Test of my web application : LogCorner.BlogPost.Mvc
Lets talk about Unit Testing TDD or BDD?
BDD approach: in absolute terms, TDD plus natural language expression using Gherkin.
Using TDD approach, we must write the tests first using the red-green-refactor cycle.
RED: Write the test first, it will fail
GREEN: Write tested code as simple as possible to make it pass
REFACTOR: Refactor your code; your test may fail, so make it pass again
But if you have already a production code, how to test it?
public class BlogService : IBlogService { private readonly IUnitOfWorkCore<LogcornerBlogpostContext> _unitOfWork; public BlogService(IUnitOfWorkCore<LogcornerBlogpostContext> unitOfWork) { _unitOfWork = unitOfWork; } public async Task UpdateAsync(Blog blog, string email) { if (blog == null || string.IsNullOrWhiteSpace(email)) { throw new ArgumentNullException(); } var old = await GetAsync(blog.BlogId); if (old == null) { throw new Exception($"blog : {blog.BlogId} must existe in database"); } if (old.Owner != email) { throw new AuthenticationException($"You are not authorized to update the blog of someone else : {blog.BlogId}"); } old.Url = blog.Url; old.Description = blog.Description; _unitOfWork.GetRepository<Blog>().Update(old); await _unitOfWork.SaveAsync(); } public async Task CreateAsync(Blog blog, string owner) { if (blog == null || string.IsNullOrWhiteSpace(owner)) { throw new ArgumentNullException(); } blog.Owner = owner; _unitOfWork.GetRepository<Blog>().Create(blog); await _unitOfWork.SaveAsync(); } public async Task<List<Blog>> GetAsync() { var result = await _unitOfWork.GetRepository<Blog>().All().ToListAsync(); return result; } public async Task<Blog> GetAsync(int blogId) { return await _unitOfWork.GetRepository<Blog>().GetByIdAsync(blogId); } public async Task<bool> OwnedByAsync(int id, string owner) { var blog = await _unitOfWork.GetRepository<Blog>().GetByIdAsync(id); if (blog == null) { return false; } return blog.Owner == owner; } public async Task DeleteAsync(int blogId, string email) { if (blogId <= 0) { throw new ArgumentNullException(nameof(blogId), $"blog : {blogId} cannot be null"); } if (string.IsNullOrWhiteSpace(email)) { throw new ArgumentNullException(nameof(email), $"email : {email} cannot be null"); } var old = await GetAsync(blogId); if (old == null) { throw new Exception($"blog : {blogId} cannot be null"); } if (old.Owner != email) { throw new AuthenticationException($"You are authorized to update the blog : {blogId}"); } _unitOfWork.GetRepository<Blog>().Delete(blogId); await _unitOfWork.SaveAsync(); } }
Given the previous class, I would like to unit test my UpdateAsync method.
To update a blog post, I must verify this:
- You must provide a non nullable blog post objet and your email
- The blog post , you want to update must exist on database
- You must be owner of the blog post you want to update
- The system must replace blog post description and url, with the values provider by the user
- The system must call GetRepository<Blog>().Update(old) to perform update
- The system must call SaveAsync() to save updated blog post
TEST 1 : To update a blog post, the user must provide a non nullable blog post objet and his email adress
Here, I verify, if the user provide a null blog or an empty or null email address then an ArgumentNullException is thrown
[Theory(DisplayName = "To update a blog post you must provide a valid blog object and a userName")] [ClassData(typeof(BadRequestBlogTestData))] public async Task UpdateBlogTestWitheInvalidParameters(Blog blog, string userName) { await Assert.ThrowsAsync<ArgumentNullException>(() => _blogService.UpdateAsync(blog, userName)); }
public class BadRequestBlogTestData : IEnumerable<object[]> { public IEnumerator<object[]> GetEnumerator() { yield return new object[] { null, null }; yield return new object[] { null, string.Empty }; yield return new object[] { null, "jean.dupont" }; yield return new object[] { new Blog(), null }; yield return new object[] { new Blog(), string.Empty }; } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); }
TEST 2: The blog post , you want to update must exist on database
I want to test the following:
I call GetAsync to retrieve the blog post by Id and I verify if the result is null or not
As you can see GetAsync make a call to _unitOfWork.GetRepository<Blog>().GetByIdAsync(blogId);
So, I must mock the call of GetByIdAsync(blogId); to return an existing blogpost.
Here I mock IUnitOfWorkCore<LogcornerBlogpostContext> because my blogService need it and I don’t want to use the real implementation of my repository.
Do not mock unitOfWork, means I’m doing integration testing but not unit testing.
Let’s go ahead and write the test
[Theory(DisplayName = "To update a blog post you must provide an existing blogId")] [ClassData(typeof(UpdateBlogTestData))] public async Task UpdateBlogTestWithNoExistingBlogThrowsException(Blog blog, string userName) { _unitOfWorkMock.Setup(r => r.GetRepository<Blog>().GetByIdAsync(It.IsAny<int>())).Returns(Task.FromResult((Blog)null)).Verifiable(); await Assert.ThrowsAsync<Exception>(() => _blogService.UpdateAsync(blog, userName)); _unitOfWorkMock.Verify(r => r.GetRepository<Blog>().GetByIdAsync(blog.BlogId)); }
public class UpdateBlogTestData : IEnumerable<object[]> { public IEnumerator<object[]> GetEnumerator() { yield return new object[] { new Blog { BlogId = 0, Url = "http://www.jean.dupont.com", Description = "lorem" }, "jean.dupont" }; yield return new object[] { new Blog { BlogId = -1, Url = "http://www.jean.dupont.com", Description = "lorem" }, "jean.dupont" }; yield return new object[] { new Blog { BlogId = 18, Url = "http://www.jean.dupont.com", Description = "lorem" }, "jean.dupont" }; } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); }
I verify, if the blog does not exist in database, then an Exception is thrown
TEST 3 : To update a blog post , you must be owner of that blog post : you cannot update a blog post of someone else
So, I want to test the following:
if (old.Owner != email) { throw new AuthenticationException($"You are not authorized to update the blog of someone else : {blog.BlogId}"); }
I test if the owner field of the blog I want to update is different from the current user email, if so the system throws an exception.
Let’s go ahead and test it
[Theory(DisplayName = "When No Owner Of Blog Update Blog Must Throws Authentication Exception")] [ClassData(typeof(UpdateBlogNotOwnerTestData))] public async Task WhenNoOwnerOfBlogUpdateBlogMustThrowsAuthenticationException(Blog blog, string userName) { var outputBlog = new Blog { BlogId = 1, Url = "http://www.jean.dupont.com", Description = "lorem", Owner = "mathias.fererra" }; _unitOfWorkMock.Setup(r => r.GetRepository<Blog>().GetByIdAsync(It.IsAny<int>())).Returns(Task.FromResult(outputBlog)); _unitOfWorkMock.Setup(r => r.GetRepository<Blog>().Update(blog)).Verifiable(); await Assert.ThrowsAsync<AuthenticationException>(() => _blogService.UpdateAsync(blog, userName)); _unitOfWorkMock.Verify(r => r.GetRepository<Blog>().GetByIdAsync(blog.BlogId)); Assert.NotEqual(blog.Owner, userName); }
public class UpdateBlogNotOwnerTestData : IEnumerable<object[]> { public IEnumerator<object[]> GetEnumerator() { yield return new object[] { new Blog { BlogId = 1, Url = "http://www.jean.dupont.com", Description = "lorem", Owner = "mathias.fererra" }, "jean.dupont"}; yield return new object[] { new Blog { BlogId = 2, Url = "http://www.jean.dupont.com", Description = "lorem", Owner ="yves.kerghal" }, "jean.dupont" }; yield return new object[] { new Blog { BlogId = 3, Url = "http://www.jean.dupont.com", Description = "lorem", Owner ="marc.laplace" },"jean.dupont" }; } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); }
TEST 4: To update a blog post, you must set the url and description of the blog post you want to update with new values of url and description
In this test I would like to verify the following
old.Url = blog.Url;
old.Description = blog.Description;
[Theory(DisplayName = "When owner of a given Blog update it with new values of URL and Description then the URL and Description of the blog must be ovverriden with new values")] [ClassData(typeof(UpdateBlogOwnerTestData))] public async Task WhenOwnerOfBlogUpdateBlogUrlAndDescriptionMustBeSetWithNewValues(Blog blog, string userName) { var blogToUpdate = new Blog { BlogId = 1, Url = "http://www.jean.dupont.com", Description = "lorem", Owner = "jean.dupont" }; _unitOfWorkMock.Setup(r => r.GetRepository<Blog>().GetByIdAsync(It.IsAny<int>())).Returns(Task.FromResult(blogToUpdate)).Verifiable(); _unitOfWorkMock.Setup(r => r.GetRepository<Blog>().Update(blog)).Verifiable(); await _blogService.UpdateAsync(blog, userName); _unitOfWorkMock.Verify(r => r.GetRepository<Blog>().GetByIdAsync(blog.BlogId), Times.Once); _unitOfWorkMock.Verify(r => r.GetRepository<Blog>().Update(blogToUpdate), Times.Once); _unitOfWorkMock.Verify(m => m.GetRepository<Blog>().Update(It.Is<Blog>(n => n.Url.Equals(blog.Url, StringComparison.InvariantCultureIgnoreCase) && n.Description.Equals(blog.Description, StringComparison.InvariantCultureIgnoreCase) && n.BlogId.Equals(blogToUpdate.BlogId) && n.Owner.Equals(blogToUpdate.Owner, StringComparison.InvariantCultureIgnoreCase) ))); }
public class UpdateBlogOwnerTestData : IEnumerable<object[]> { public IEnumerator<object[]> GetEnumerator() { yield return new object[] { new Blog { BlogId = 1, Url = "http://www.new1.jean.dupont.com", Description = "new lorem 1", Owner = "jean.dupont" }, "jean.dupont" }; yield return new object[] { new Blog { BlogId = 2, Url = "http://www.new2.jean.dupont.com", Description = "new lorem 2", Owner ="jean.dupont" }, "jean.dupont" }; yield return new object[] { new Blog { BlogId = 3, Url = "http://www.new3.jean.dupont.com", Description = "new lorem 3", Owner ="jean.dupont" }, "jean.dupont" }; } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); }
I use a Verify call to check if the url and the description of the old blog are updated with new values
_unitOfWorkMock.Verify(m => m.GetRepository<Blog>().Update(It.Is<Blog>(n => n.Url.Equals(blog.Url, StringComparison.InvariantCultureIgnoreCase) && n.Description.Equals(blog.Description, StringComparison.InvariantCultureIgnoreCase) && n.BlogId.Equals(blogToUpdate.BlogId) && n.Owner.Equals(blogToUpdate.Owner, StringComparison.InvariantCultureIgnoreCase) )));
TEST 5 : When the owner of a given blog try to uptade it , then the blog must be updated
[Theory(DisplayName = "When owner of a given blog try to update it then the corresponding Blog is successfully updated")] [ClassData(typeof(UpdateBlogOwnerTestData))] public async Task WhenOwnerOfBlogUpdateBlogThenTheCorrespondingBlogIsUpdated(Blog blog, string userName) { var blogToUpdate = new Blog { BlogId = 1, Url = "http://www.jean.dupont.com", Description = "lorem", Owner = "jean.dupont" }; _unitOfWorkMock.Setup(r => r.GetRepository<Blog>().GetByIdAsync(It.IsAny<int>())).Returns(Task.FromResult(blogToUpdate)).Verifiable(); _unitOfWorkMock.Setup(r => r.GetRepository<Blog>().Update(blog)).Verifiable(); await _blogService.UpdateAsync(blog, userName); _unitOfWorkMock.Verify(r => r.GetRepository<Blog>().GetByIdAsync(blog.BlogId)); _unitOfWorkMock.Verify(r => r.GetRepository<Blog>().Update(blogToUpdate), Times.Once, "update must be called only once"); _unitOfWorkMock.Verify(r => r.SaveAsync(), Times.Once, "update must be called only once"); }
public class UpdateBlogOwnerTestData : IEnumerable<object[]> { public IEnumerator<object[]> GetEnumerator() { yield return new object[] { new Blog { BlogId = 1, Url = "http://www.new1.jean.dupont.com", Description = "new lorem 1", Owner = "jean.dupont" }, "jean.dupont" }; yield return new object[] { new Blog { BlogId = 2, Url = "http://www.new2.jean.dupont.com", Description = "new lorem 2", Owner ="jean.dupont" }, "jean.dupont" }; yield return new object[] { new Blog { BlogId = 3, Url = "http://www.new3.jean.dupont.com", Description = "new lorem 3", Owner ="jean.dupont" }, "jean.dupont" }; } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); }
Test if SaveAsync is called
_unitOfWorkMock.Verify(r => r.SaveAsync(), Times.Once, "update must be called only once");
Regards