When we're trying to write integration tests, usually it's good not to mock any class or method and execute actual code. By doing so we can make sure all code paths and different scenarios will be covered.
Assuming we have this UserService
class in our application:
public class UserService : IUserService
{
private readonly IHttpClientFactory _httpClientFactory;
public UserService(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
}
public async Task<UserDto> GetUserAsync(string username)
{
var dbUser = await GetUserFromDbAsync(username);
var githubUser = await GetUserFromGithubAsync(username);
if (githubUser is not null)
{
dbUser.GithubFollowers = githubUser.Followers;
dbUser.GithubUserId = githubUser.Id;
}
return dbUser;
}
public virtual async Task<GithubUserDto?> GetUserFromGithubAsync(string username)
{
var httpClient = _httpClientFactory.CreateClient("GithubAPI");
var response = await httpClient.GetAsync($"users/{username}");
if (!response.IsSuccessStatusCode)
{
return null;
}
return await response.Content.ReadFromJsonAsync<GithubUserDto>();
}
}
The API:
app.MapGet("/users/{username}", async (string username, [FromServices] IUserService userService) =>
{
return await userService.GetUserAsync(username);
})
.WithName("Users");
Let's break it down, there is a service for getting users by their username
. If you look at the GetUserAsync
method, for preparing the result, we are seeking in two places, first looking for the user in the database (GetUserFromDbAsync
) and after calling a third-party API which is Github here (GetUserFromGithubAsync
) to set extra properties.
Everything is perfect until we decide to write an integration test for the GetUser
API. As we know the integration test will execute the real code which means for every test we're going to call Github API!
public class UserApiTest : IClassFixture<WebApplicationFactoryTest<Program>>
{
private readonly HttpClient _httpClient;
public UserApiTest(WebApplicationFactoryTest<Program> factory)
{
_httpClient = factory.CreateClient();
}
[Fact]
public async Task GetUser_ValidUsername_ReturnsOk()
{
var username = "sa-es-ir";
var response = await _httpClient.GetAsync($"/users/{username}");
response.StatusCode.Should().Be(HttpStatusCode.OK);
var user = await response.Content.ReadFromJsonAsync<UserDto>();
user!.Name.Should().Be("Saeed Esmaeelinejad");
user.GithubFollowers.Should().BeGreaterThan(0);
}
}
The test will pass successfully, but a question comes up! What if there is a limitation
when calling third-party API? We also need to use those limitations only for actual API not by consuming by tests, right?
Here is the way we go, just skip calling that GetUserFromGithubAsync
in other words mock it! But how? The only difference with normal mocking is, that all other functionality of UserService
class must run as normal but only one method will be skipped!
There are a few ways to do it:
- New Implementation with copy/past all codes and change whatever we want (caused code duplication), as it is obvious one, no need to cover in this article.
- Using WireMock.NET Nuget package to run a local server instead of third-party API, I think is out of this article's scope.
- New implementation of IUserService which is inheriting from the main class but overrides one method
- Using
NSubstitute
to mock the method
But we have an issue here, the GetUserFromGithubAsync
method is private so we can't override it, that is the only downside of mocking part of a class and it makes sense because either we need to copy/past all code in the test one or just overriding only one method, let me show you with code.
This is an custom implementation of UserService:
public class TestServiceService : UserService
{
public TestServiceService(IHttpClientFactory httpClientFactory) : base(httpClientFactory)
{
}
// Override the method to return a fake user from Github
public override Task<GithubUserDto?> GetUserFromGithubAsync(string username)
{
return Task.FromResult<GithubUserDto?>(new GithubUserDto
{
Id = 1,
Followers = 100
});
}
}
You got the idea, right? No need to mention the method in the UserService should be changed like:
public virtual async Task<GithubUserDto?> GetUserFromGithubAsync(string username)
...
Let's do the same thing with NSubstitute
package which has a feature exactly for this purpose called ForPartOf
!
If you worked before with NSubstitute for mocking, it's easily possible to mock an interface
using For
method but here is a little different:
public class WebApplicationFactoryTest<TProgram>
: WebApplicationFactory<TProgram> where TProgram : Program
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureTestServices(services =>
{
var scope = services.BuildServiceProvider().CreateScope();
var userService = Substitute.ForPartsOf<UserService>(scope.ServiceProvider.GetRequiredService<IHttpClientFactory>());
userService.GetUserFromGithubAsync(Arg.Any<string>()).Returns(new GithubUserDto
{
Id = 1,
Followers = 100
});
services.AddScoped<IUserService>(_ => userService);
});
}
}
I put all the code for our Integration Test to get a bigger picture, basically, first, we mock the GetUserFromGithubAsync
and then register it into DI (actually replacing the main one which was added in the program.cs
in the API). Now if we run the integration tests we can be sure that GitHub API won't be called just we can cover actual logic.
Hope you enjoyed it.
Code Simple!