Create Project
- Open git Bash and type
dotnet new sln -o CloudCustomers
- Enter the CloudCustomers folder and type
dotnet new webapi -o CloudCustomer.API
,dotnet new xunit -o CloudCustomers.UnitTests
anddotnet sln add **/*.csproj

Install Nuget Packages
- Install Moq and FluentAssertions

Test 1 : Trial
Change File Structure

- CloudCustomers.API/Controllers/UserController.cs
[ApiController]
[Route("[controller]")]
public class UsersController : ControllerBase
{
public UsersController()
{
}
[HttpGet(Name = "GetUsers")]
public async Task<IActionResult> Get()
{
return Ok("all good");
}
}
- CloudCustomers.UnitTests/Systems/Controllers/TestUsersController.cs
public class TestUsersController
{
[Fact]
public async Task Get_OnSuccess_ReturnsStatusCode200()
{
// Arragne
var sut = new UsersController();
// Act
var result = (OkObjectResult)await sut.Get();
// Assert
result.StatusCode.Should().Be(200);
}
}
Test Annotations
[Fact]
attribute declares a test method that’s run by the test runner.[Theory]
represents a suite of tests that execute the same code but have different input arguments.[InlineData]
attribute specifies values for those inputs.
Test Structure
- Arrange: inputs and targets. Arrange steps should set up the test case. For examples, any objects or special settings, to prep a database, to log into a web app. Handle all of these operations at the start of the test.
- Act: on the target behavior. Act steps should cover the main thing to be tested. This could be calling a function or method, calling a REST API, or interacting with a web page. Keep actions focused on the target behavior.
- Assert: expected outcomes. Act steps should elicit some sort of response. Assert steps verify the goodness or badness of that response. Sometimes, assertions are as simple as checking numeric or string values. Other times, they may require checking multiple facets of a system. Assertions will ultimately determine if the test passes or fails.
Open Test Exploler
- [View] - [Test Exploler]

Run Test
- Click
Run
- We call all passed test to green test.

Test 2: DIP
DIP(Dependency Inversion Principle) From SOLID
- Hihgh-level modules should not depend on low-level modules. Both should depend on abstractions.
- Abstractions should not depend on details.
- Details should depend on abstractions.
SOLID
- S: Single Responsibility Principle
- O: Open/Closed principle
- L: Liskov Substitution Principle
- I: Interface Segregation Principle
- D: Dependency Inversion Principle
Change File Structure

- CloudCustomers.API/Controllers/UsersController.cs
[ApiController]
[Route("[controller]")]
public class UsersController : ControllerBase
{
private readonly IUsersService _usersService;
public UsersController(IUsersService usersService)
{
_usersService = usersService;
}
[HttpGet(Name = "GetUsers")]
public async Task<IActionResult> Get()
{
var users = await _usersService.GetAllUsers();
if (users.Any())
{
return Ok(users);
}
return NotFound();
}
}
- CloudCustomers.API/Models/User.cs
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public Address Address { get; set; }
}
- CloudCustomers.API/Models/Address.cs
public class Address
{
public string Street { get; set; }
public string City { get; set; }
public string ZipCode { get; set; }
}
- CloudCustomers.API/Services/UsersService.cs
public interface IUsersService
{
public Task<List<User>> GetAllUsers();
}
public class UsersService : IUsersService
{
public Task<List<User>> GetAllUsers()
{
throw new NotImplementedException();
}
}
- CloudCustomers.API/Program.cs
using CloudCustomers.API.Services;
var builder = WebApplication.CreateBuilder(args);
ConfigureServices(builder.Services);
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
void ConfigureServices(IServiceCollection services)
{
services.AddTransient<IUsersService, UsersService> ();
}
- CloudCustomers.UnitTests/Systems/Controllers/TestUsersController.cs
public class TestUsersController
{
[Fact]
public async Task Get_OnSuccess_ReturnsStatusCode200()
{
// Arragne
var mockUsersService = new Mock<IUsersService>();
mockUsersService
.Setup(service => service.GetAllUsers())
.ReturnsAsync(new List<User>());
var sut = new UsersController(mockUsersService.Object);
// Act
var result = (OkObjectResult)await sut.Get();
// Assert
result.StatusCode.Should().Be(200);
}
[Fact]
public async Task Get_OnSuccess_InvokesUserServiceExactlyOnce()
{
// Arrange
var mockUsersService = new Mock<IUsersService>();
mockUsersService
.Setup(service => service.GetAllUsers())
.ReturnsAsync(new List<User>());
var sut = new UsersController(mockUsersService.Object);
// Act
var result = await sut.Get();
// Assert
mockUsersService.Verify(
service => service.GetAllUsers(),
Times.Once());
}
[Fact]
public async Task Get_OnSuccess_ReturnsListOfUsers()
{
// Arrange
var mockUsersService = new Mock<IUsersService>();
mockUsersService
.Setup(service => service.GetAllUsers())
.ReturnsAsync(new List<User>()
{
new User()
{
Id = 1,
Name = "Jane",
Address = new Address()
{
Street = "123 Main St",
City = "Madison",
ZipCode = "53704"
},
Email = "jane@example.com"
}
});
var sut = new UsersController(mockUsersService.Object);
// Act
var result = await sut.Get();
// Assert
result.Should().BeOfType<OkObjectResult>();
var objectResult = (OkObjectResult)result;
objectResult.Value.Should().BeOfType<List<User>>();
}
[Fact]
public async Task Get_OnNoUsersFound_Returns404()
{
// Arrange
var mockUsersService = new Mock<IUsersService>();
mockUsersService
.Setup(service => service.GetAllUsers())
.ReturnsAsync(new List<User>());
var sut = new UsersController(mockUsersService.Object);
// Act
var result = await sut.Get();
// Assert
result.Should().BeOfType<NotFoundResult>();
var objectResult = (NotFoundResult)result;
objectResult.StatusCode.Should().Be(404);
}
}
Dependency Injection
- For DIP, I used Dependency Injection for
UsersService
. - And then, I created
mockUserService
as a object and make it to instance forUsersService
.
Mock
- Initializes an instance of the mock with Moq.MockBehavior.Default behavior.
Arrange
- Setup(): Specifies a setup on the mocked type for a call to a method.
- ReturnAsync(): Specifies the value to return from an asynchronous method.
Assert
- Verify(): Verifies that a specific invocation matching the given expression was performed on the mock. Use in conjunction with the default Moq.MockBehavior.Loose.
- Times.Once(): Specifies that a mocked method should be invoked exactly one time.
FluentAssertions
- Should(): Returns an FluentAssertions.Numeric.NullableNumericAssertions`1 object that can be used to assert the current nullable System.Int32.
- Be(): Asserts that the integral number value is exactly the same as the expected value.
- BeOfType(): Asserts that the object is of the specified type T.
Run Test

Test 3: Fixtures
Change File Structure

- CloudCustomers.UnitTests/Fixtures/UsersFixture.cs
public static class UsersFixture
{
public static List<User> GetTestUsers() => new List<User>
{
new User()
{
Name = "Test User 1",
Email = "test.user.1@productivedev.com",
Address = new Address()
{
Street = "123 Main St",
City = "Somewhere",
ZipCode = "213124"
}
},
new User()
{
Name = "Test User 2",
Email = "test.user.2@productivedev.com",
Address = new Address()
{
Street = "900 Main St",
City = "Somewhere",
ZipCode = "213124"
}
},
new User()
{
Name = "Test User ",
Email = "test.user.3@productivedev.com",
Address = new Address()
{
Street = "108 Maple St",
City = "Somewhere",
ZipCode = "213124"
}
}
};
}
- CloudCustomers.UnitTests/Systems/Controllers/TestUsersController.cs
- On
Get_OnSuccess_ReturnsStatusCode200
andGet_OnSuccess_ReturnsListOfUsers
, addUsersFixtures
instead new object.
- On
mockUsersService
.Setup(service => service.GetAllUsers())
.ReturnsAsync(UsersFixture.GetTestUsers());
Fixture
- Fixed state of objects used as baselines for test execution
Run Test

Test 4: Request
Change File Structure

- CloudCustomers.API/Config/UsersApiOptions.cs
public class UsersApiOptions
{
public string Endpoint { get; set; }
}
- CloudCustomers.API/Services/UsersService.cs
public interface IUsersService
{
public Task<List<User>> GetAllUsers();
}
public class UsersService : IUsersService
{
private readonly HttpClient _httpClient;
private readonly UsersApiOptions _apiConfig;
public UsersService(
HttpClient httpClient,
IOptions<UsersApiOptions> apiConfig)
{
_httpClient = httpClient;
_apiConfig = apiConfig.Value;
}
public async Task<List<User>> GetAllUsers()
{
var usersResponse = await _httpClient
.GetAsync(_apiConfig.Endpoint);
if(usersResponse.StatusCode == System.Net.HttpStatusCode.NotFound)
return new List<User>();
var responseContent = usersResponse.Content;
var allUsers = await responseContent.ReadFromJsonAsync<List<User>>();
return allUsers.ToList();
}
}
- CloudCustomers.API/appsettings.json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"UsersApiOptions": {
"Endpoint": "https://jsonplaceholder.typicode.com/users"
},
"AllowedHosts": "*"
}
- CloudCustomers.API/Program.cs
- Add
UserApiOptions
inConfigurationServices
function.
- Add
void ConfigureServices(IServiceCollection services)
{
services.Configure<UsersApiOptions>(
builder.Configuration.GetSection("UsersApiOptions"));
services.AddTransient<IUsersService, UsersService> ();
services.AddHttpClient<IUsersService, UsersService> ();
}
- CloudCustomers.UnitTests/Helpers/MockHttpMessageHandler.cs
internal static class MockHttpMessageHandler<T>
{
internal static Mock<HttpMessageHandler> SetupBasicGetResourceList(List<T> expectedResponse)
{
var mockResponse = new HttpResponseMessage(System.Net.HttpStatusCode.OK)
{
Content = new StringContent(JsonConvert.SerializeObject(expectedResponse))
};
mockResponse.Content.Headers.ContentType =
new System.Net.Http.Headers.MediaTypeHeaderValue("application/json");
var handlerMock = new Mock<HttpMessageHandler>();
handlerMock
.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(mockResponse);
return handlerMock;
}
internal static Mock<HttpMessageHandler> SetupBasicGetResourceList(List<T> expectedResponse, string endpoint)
{
var mockResponse = new HttpResponseMessage(System.Net.HttpStatusCode.OK)
{
Content = new StringContent(JsonConvert.SerializeObject(expectedResponse))
};
mockResponse.Content.Headers.ContentType =
new System.Net.Http.Headers.MediaTypeHeaderValue("application/json");
var handlerMock = new Mock<HttpMessageHandler>();
handlerMock
.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(mockResponse);
return handlerMock;
}
internal static Mock<HttpMessageHandler> SetupReturn404()
{
var mockResponse = new HttpResponseMessage(System.Net.HttpStatusCode.NotFound)
{
Content = new StringContent("")
};
mockResponse.Content.Headers.ContentType =
new System.Net.Http.Headers.MediaTypeHeaderValue("application/json");
var handlerMock = new Mock<HttpMessageHandler>();
handlerMock
.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(mockResponse);
return handlerMock;
}
}
- CloudCustomers.UnitTests/Systems/Services/TestUsersService.cs
public class TestUsersService
{
[Fact]
public async Task GetAllUsers_WhenCalled_InvokesHttpGetRequest()
{
// Arrange
var expectedResponse = UsersFixture.GetTestUsers();
var handlerMock = MockHttpMessageHandler<User>
.SetupBasicGetResourceList(expectedResponse);
var httpClient = new HttpClient(handlerMock.Object);
var endpoint = "https://example.com";
var config = Options.Create(
new UsersApiOptions
{
Endpoint = endpoint
});
var sut = new UsersService(httpClient, config);
// Act
await sut.GetAllUsers();
// Assert
// Verify HTTP request is made!
handlerMock
.Protected().
Verify(
"SendAsync",
Times.Exactly(1),
ItExpr.Is<HttpRequestMessage>(req => req.Method == HttpMethod.Get),
ItExpr.IsAny<CancellationToken>());
}
[Fact]
public async Task GetAllUsers_WhenHits404_ReturnsEmptyListOfUsers()
{
// Arrange
var handlerMock = MockHttpMessageHandler<User>.SetupReturn404();
var httpClient = new HttpClient(handlerMock.Object);
var endpoint = "https://example.com";
var config = Options.Create(
new UsersApiOptions
{
Endpoint = endpoint
});
var sut = new UsersService(httpClient, config);
// Act
var result = await sut.GetAllUsers();
// Assert
// Verify HTTP request is made!
result.Count.Should().Be(0);
}
[Fact]
public async Task GetAllUsers_WhenCalled_ReturnsListOfUsersOfExpectedSize()
{
// Arrange
var expectedResponse = UsersFixture.GetTestUsers();
var handlerMock = MockHttpMessageHandler<User>
.SetupBasicGetResourceList(expectedResponse);
var httpClient = new HttpClient(handlerMock.Object);
var endpoint = "https://example.com";
var config = Options.Create(
new UsersApiOptions
{
Endpoint = endpoint
});
var sut = new UsersService(httpClient, config);
// Act
var result = await sut.GetAllUsers();
// Assert
// Verify HTTP request is made!
result.Count.Should().Be(expectedResponse.Count);
}
[Fact]
public async Task GetAllUsers_WhenCalled_InvokesConfiguredExternalUrl()
{
// Arrange
var expectedResponse = UsersFixture.GetTestUsers();
var endpoint = "https://example.com/users";
var handlerMock = MockHttpMessageHandler<User>
.SetupBasicGetResourceList(expectedResponse, endpoint);
var httpClient = new HttpClient(handlerMock.Object);
var config = Options.Create(
new UsersApiOptions
{
Endpoint = endpoint
});
var sut = new UsersService(httpClient, config);
// Act
var result = await sut.GetAllUsers();
var uri = new Uri(endpoint);
// Assert
// Verify HTTP request is made!
handlerMock
.Protected()
.Verify(
"SendAsync",
Times.Exactly(1),
ItExpr.Is<HttpRequestMessage>(
req => req.Method == HttpMethod.Get
&& req.RequestUri == uri),
ItExpr.IsAny<CancellationToken>());
}
}
HttpMessageHandler
- A base type for HTTP message handlers.
- I used this to verify HttpClient requests.
Endpoint
- IOptions
: Used to retrieve configured TOptions instances. - service.Configure
: Registers a configuration instance which TOptions will bind against. - builder.Configuration.GetSection: Get value from configuration providers for the application to compose. In this application, the configuration provider is appsetting.json.
Mock
- Protected(): Enable protected setups for the mock.
- ItExpr: Allows the specification of a matching condition for an argument in a protected member setup, rather than a specific argument value. “ItExpr” refers to the argument being matched.
Options
- Create(): Creates a wrapper around an instance of TOptions to return itself as an Microsoft.Extensions.Options.IOptions`1.
Run Test
