What I ended up doing is to mock the interface who has the MySql methods and inject the expected data from the Lab class, then in the test I call the controller and compare result with the result get from calling just the implementation of the MySql interface separately:
Lab.cs:
public sealed class Lab
{
private readonly Faker _faker;
private readonly IdentityInfo _identity;
private readonly WebApplicationFactory<Startup> _appFactory;
private readonly IApiClient _apiClient;
private readonly AuthorizeOptions _authorize;
private static Account[]? _accounts;
private Lab(IdentityInfo identity,
WebApplicationFactory<Startup> appFactory,
IApiClient apiClient,
AuthorizeOptions authorize,
Faker faker)
{
_faker = faker;
_identity = identity;
_appFactory = appFactory;
_apiClient = apiClient;
_authorize = authorize;
}
public Faker Faker => _faker;
public IdentityInfo Identity => _identity;
public IApiClient ApiClient => _apiClient;
public AuthorizeOptions Authorize => _authorize;
public WebApplicationFactory<Startup> AppFactory => _appFactory;
public static Account[]? Accounts => _accounts;
public EventSend? InitAsync(IServiceProvider provider, bool useEvents = false, int expectedElements = 5)
{
var unitOfWork = provider.GetRequiredService<IVdcUnitOfWork>();
_accounts = PopulateAccounts(_faker, expectedElements);
EventSend? events = null;
if (useEvents)
Events.Capture(provider, events = new EventSend());
return events;
Account[] PopulateAccounts(Faker faker, int expectedElements)
{
//var accounts = new Account[expectedElements];
var accounts = new List<Account>();
for (var i = 0; i < expectedElements; i++)
{
var account = new Account
{
Id = faker.Random.Uuid(),
Name = faker.Random.AlphaNumeric(10),
AccountOrder = faker.Random.Int(),
RegionId = (Region)faker.Random.Int(1, Enum.GetNames<Region>().Length)
};
accounts.Add(account);
}
return accounts.ToArray();
}
}
public static Lab Create(ITestOutputHelper output)
{
var instance = new FakeData();
var appFactory = Vdc.Libs.AspNet.Testing.Setup.Factory<Startup, InnerDbContextRead, InnerDbContextWrite>(
out var client,
out var authorize,
out var identity,
out var faker,
role: $"{VdcSecurity.Role.Management},{VdcSecurity.Role.ManagementAdmin}",
output: output,
setup: services =>
{
services.AddSingleton<IData>(instance);
services.AddSingleton<ICloudStackCoreConnectionFactory, MySqlMock>();
}
);
return new Lab(identity, appFactory, client, authorize, faker);
}
public class FakeData : IData
{
Account[]? IData.Accounts => Accounts;
}
}
MySqlMock:
public interface IData
{
Account[]? Accounts { get; }
}
public class MySqlMock : ICloudStackCoreConnectionFactory
{
IData _data;
public MySqlMock(IData data)
{
_data = data;
}
public DbConnection CreateConnection(Vdc.Libs.Region region)
{
return new MySqlConnection();
}
public async Task<(int Count, TOut[] Data)> QuerySearchAsync<TDbItem, TOut>(
IRegionRepository regionRepository,
string fetchDataQuery,
string? countDataQuery = null, ...) where TOut : class
{
var type = typeof(TOut);
if (type == typeof(Account))
{
var accounts = _data.Accounts;
return (accounts.Length, accounts as TOut[]);
}
return (0, []);
}
public Task<int> ExecuteAsync(IRegionRepository regionRepository, string sql, object? @params = null, Func<Vdc.Libs.Region, bool>? filter = null, CancellationToken ct = default)
{
throw new NotImplementedException();
}
}
Accounts test:
[Theory]
[InlineData(0, 8)]
public async Task GetFirst8Elements_ReturnOnly8Elements(int page, int expectedElements)
{
using var scope = _lab.AppFactory.Services.CreateScope();
var provider = scope.ServiceProvider;
// Arrange
var events = _lab.InitAsync(provider, false, expectedElements);
var accountsRepository = provider.GetRequiredService<ICloudStackCoreConnectionFactory>();
var regionRepository = provider.GetRequiredService<IRegionRepository>();
// Act
var request = new SearchRequest(
null,
new Paging { Page = page, PageSize = expectedElements },
[new Vdc.Libs.Web.ColumnNameRequest(nameof(SearchResponse.Account.Name)) { Asc = true }]
);
var response = await _client.SearchAsync(_lab.Identity, request);
// Assert
var accounts = await accountsRepository.QuerySearchAsync<SearchResponse.Account, SearchResponse.Account>(
regionRepository, "", null, null, null, null, null, null, null, null, false, default);
response.Results.Should().BeEquivalentTo(accounts.Data);
response.Results.GetType().Should().Be(typeof(List<SearchResponse.Account>));
response.Results.Count().Should().Be(expectedElements);
}
I know this way at least I'm testing the flow of data from beginning to end.
What I was expecting, to mock the MySql database to do a full test, including the QuerySearchAsync method is much more complex, and maybe it does not even worth, in the end this is the way I've seen more or less it's done in other APIs.