We'll work with the default Visual Studio template, just adding few minimal lines of code, with an additional project for unit testing.
First of, we add a new HomeService.cs class with an interface having 2 methods to increment and decrement a given value:
public interface IHomeService
{
Task<int> IncrAsync(int i);
Task<int> DecrAsync(int i);
}
public class HomeService : IHomeService
{
public Task<int> DecrAsync(int i)
{
return OpAsync(i, -1);
}
public Task<int> IncrAsync(int i)
{
return OpAsync(i, 1);
}
protected async Task<int> OpAsync(int i, int step)
{
return await Task.Run(async () =>
{
await Task.Delay(3000);
//Thread.Sleep(3000);
return i + step;
});
}
}
Here, we wanted to use a protected (non-public) method used by both public interface methods, for simplicity. There are many times when a protected method make sense, for various reasons.
Instead of using the blocking Thread.Sleep() I used awaitable Task.Delay(), waiting for 3 seconds. Note that without await keyword the task is not blocking, it just continue immediately. When both IncrAsync() and DecrAsync() are called the code waits for 3 + 3 seconds, which is very noticeable in unit testing.
We also add IHomeService to ASP.NET Core dependency injection, so in Startup.cs in ConfigureServices() we have this (let's say we need a scoped lifetime):
services.AddScoped<IHomeService, HomeService>();
Then, let's use it in default About action of HomeController:
public async Task<IActionResult>
About([FromServices] IHomeService homeService)
{
int n = await homeService.IncrAsync(4);
ViewData["Message"] = "Your application description page.4"
+ await homeService.DecrAsync(n);
return View();
}
As you can see, we call once the service to increment default 4 value, then call it again to decrement it to original value.
Now, let's switch to the unit testing project and after adding a reference to the MVC project let's write some tests for the service itself. In HomeServiceTests.cs we may have
public class HomeServiceTests
{
[Fact]
public async void IHomeService_Incr_Success_With_Delay()
{
IHomeService homeService = new HomeService();
int n = await homeService.IncrAsync(2);
Assert.Equal(3, n);
}
[Fact]
public async Task IHomeService_Incr_Success_No_Delay()
{
var mock = new Mock<HomeService>();
mock.Protected().Setup<Task<int>>("OpAsync", ItExpr.IsAny<int>(),
ItExpr.IsAny<int>()).Returns(Task.FromResult<int>(3));
int n = await mock.Object.IncrAsync(2);
Assert.Equal(3, n);
}
}
First test, IHomeService_Incr_Success_With_Delay(), is just calling the service methods which will wait for those 3 seconds.
For second test we want to skip that waiting by mocking OpAsync() method. Don't forget that OpAsync() is protected, so we can't just call it. For this, we use Moq.Protected, but still some changes to the code are necessary. Most importantly, we have to declare OpAsync() as virtual, so its signature in HomeService.ca will be:
protected virtual async Task<int> OpAsync(int i, int step)Also notice the way how we mock that async method (returning Task<int>) and declaring parameters with ItExpr.IsAny< >(), instead of just It.IsAny< >.
If we want to test the controller itself (HomeController) we can have in a HomeControllerTests.cs something like this:
public class HomeControllerTests
{
[Fact]
public async void HomeController_About_Success_With_Delay()
{
var homeController = new HomeController();
IHomeService homeService = new HomeService();
var x = await homeController.About(homeService) as ViewResult;
Assert.NotNull(x);
Assert.True(x.ViewData["Message"].ToString() ==
"Your application description page.4");
}
[Fact]
public async Task HomeController_About_Success_No_Delay()
{
var mock = new Mock<HomeService>();
mock.Protected().Setup<Task<int>>("OpAsync", ItExpr.IsAny<int>(),
ItExpr.IsAny<int>()).Returns(Task.FromResult<int>(4));
var homeController = new HomeController();
var x = await homeController.About(mock.Object) as ViewResult;
Assert.NotNull(x);
Assert.True(x.ViewData["Message"].ToString() ==
"Your application description page.4");
}
}
Again, first test is just calling the action waiting for 3 + 3 seconds, while the second test mock OpAsync() similarly to HomeServiceTests. For asserts we can use directly the ViewData[] value.
The alternative would be that you manually create a class inheriting from HomeService with a public method that just call the base protected OpAsync(), but using the Moq built-in functionality may be quicker to develop.
It's true this code doesn't follow strictly SOLID principles, especially the D part ("One should depend upon abstractions, not concretions") because we tested mostly here the concrete HomeService class instead of IHomeService interface, but this is an example about testing protected methods which only make sense in classes, not interfaces (which are supposed to be public).
No comments:
Post a Comment