• Home
  • About
    • Hanna's Blog photo

      Hanna's Blog

      I wanna be a global developer.

    • Learn More
    • Email
    • LinkedIn
    • Github
  • Posts
    • All Posts
    • All Tags
  • Projects

[.Net 6] TDD

13 May 2022

Reading time ~8 minutes

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 and dotnet sln add **/*.csproj
Solution Structure

Install Nuget Packages

  • Install Moq and FluentAssertions
Nuget Packages

Test 1 : Trial

Change File Structure

File Exploler
  • 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]
Test Exploler

Run Test

  • Click Run
  • We call all passed test to green test.
Test Exploler

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

File Exploler
  • 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 for UsersService.

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 Exploler

Test 3: Fixtures

Change File Structure

File Exploler
  • 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 and Get_OnSuccess_ReturnsListOfUsers, add UsersFixtures instead new object.
mockUsersService
    .Setup(service => service.GetAllUsers())
    .ReturnsAsync(UsersFixture.GetTestUsers());

Fixture

  • Fixed state of objects used as baselines for test execution

Run Test

Test Exploler

Test 4: Request

Change File Structure

File Exploler
  • 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 in ConfigurationServices function.
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

Test Exploler

Code

Download



C#.Net6TDD Share Tweet +1