C
C#•3y ago
ross

[XUnit, Moq, EFCore] Unit Testing Question

Hi folks, needing some advice on this approach. This is my first real go at unit testing, so wanted to make sure I'm off in the right direction. My app works as so: Frontend calls API, API calls service class, service class has direct access to dbContext. Simples. So here's an example of one of my service classes and a method, and the unit test: Service class:
public class UserService : BaseService
{
public UserService(AppNameDbContext db, ILogger<BaseService> logger) : base(db, logger)
{
}

/// <summary>
/// Get all users async
/// </summary>
/// <returns></returns>
public async Task<IEnumerable<UserDto>> GetAllAsync()
{
try
{
var users = await _db.Users.ToListAsync();

if (users is not null)
{
// TODO: Create extension method for mapping
var dto = new List<UserDto>();

foreach (var user in users)
{
dto.Add(new UserDto
{
Id = user.Id,
Email = user.Email,
FirstName = user.FirstName,
Surname = user.Surname,
});
}

return dto;
}

throw new NullReferenceException(nameof(users));
}
catch (Exception)
{
throw;
}
}
}
public class UserService : BaseService
{
public UserService(AppNameDbContext db, ILogger<BaseService> logger) : base(db, logger)
{
}

/// <summary>
/// Get all users async
/// </summary>
/// <returns></returns>
public async Task<IEnumerable<UserDto>> GetAllAsync()
{
try
{
var users = await _db.Users.ToListAsync();

if (users is not null)
{
// TODO: Create extension method for mapping
var dto = new List<UserDto>();

foreach (var user in users)
{
dto.Add(new UserDto
{
Id = user.Id,
Email = user.Email,
FirstName = user.FirstName,
Surname = user.Surname,
});
}

return dto;
}

throw new NullReferenceException(nameof(users));
}
catch (Exception)
{
throw;
}
}
}
Test class:
public class UserServiceTest
{
[Fact]
public void GetAll_ShouldPass()
{
var dbContextOptions = SetupTestDatabase();
var mockLogger = Mock.Of<ILogger<BaseService>>();

using (var mockDb = new AppNameDbContext(dbContextOptions))
{
var userService = new UserService(mockDb, mockLogger);
var result = userService.GetAllAsync(); // not calling await here, i know. 1 step at a time
Assert.Equal(3, result.Result.Count());
}
}

private DbContextOptions<AppNameDbContext> SetupTestDatabase()
{
var dbContextOptions = new DbContextOptionsBuilder<AppNameDbContext>()
.UseInMemoryDatabase("UnitTestDatabase")
.Options;

using (var _db = new AppNameDbContext(dbContextOptions))
{
// https://www.mockaroo.com/
_db.Users.Add(new User { Id = 1, FirstName = "Cornelle", Surname = "Dyett", Email = "[email protected]" });
_db.Users.Add(new User { Id = 2, FirstName = "Christie", Surname = "Wickson", Email = "[email protected]" });
_db.Users.Add(new User { Id = 3, FirstName = "Leanna", Surname = "Burnes", Email = "[email protected]" });
_db.SaveChanges();
}

return dbContextOptions;
}
}
public class UserServiceTest
{
[Fact]
public void GetAll_ShouldPass()
{
var dbContextOptions = SetupTestDatabase();
var mockLogger = Mock.Of<ILogger<BaseService>>();

using (var mockDb = new AppNameDbContext(dbContextOptions))
{
var userService = new UserService(mockDb, mockLogger);
var result = userService.GetAllAsync(); // not calling await here, i know. 1 step at a time
Assert.Equal(3, result.Result.Count());
}
}

private DbContextOptions<AppNameDbContext> SetupTestDatabase()
{
var dbContextOptions = new DbContextOptionsBuilder<AppNameDbContext>()
.UseInMemoryDatabase("UnitTestDatabase")
.Options;

using (var _db = new AppNameDbContext(dbContextOptions))
{
// https://www.mockaroo.com/
_db.Users.Add(new User { Id = 1, FirstName = "Cornelle", Surname = "Dyett", Email = "[email protected]" });
_db.Users.Add(new User { Id = 2, FirstName = "Christie", Surname = "Wickson", Email = "[email protected]" });
_db.Users.Add(new User { Id = 3, FirstName = "Leanna", Surname = "Burnes", Email = "[email protected]" });
_db.SaveChanges();
}

return dbContextOptions;
}
}
So, am I doing anything wrong?
6 Replies
ross
rossOP•3y ago
Have just realised that I need to amend my .UseInMemoryDatabase as it just gets reused on each test. So I've passed in Guid.NewGuid().ToString() as the name instead
Becquerel
Becquerel•3y ago
Nothing stands out as wrong, but I would question the value of creating unit tests for simple CRUD operations like this - that's more the domain of integration tests in my opinion
ross
rossOP•3y ago
The service methods will eventually incorporate business logic and mapping data model > dto, so they will become more complicated or well "complicated"
Becquerel
Becquerel•3y ago
I would question mixing business logic, mapping and CRUD under the banner of 'service' those are all separate concerns personally I think unit tests are most valuable for business logic because they provide documentation, verification and scope all at once whereas testing 'does entity framework give me back the data i've just put into it' is less useful
ross
rossOP•3y ago
So would you recommend I split my layer into a DAL and BLL?
Becquerel
Becquerel•3y ago
you should still have that tested somewhere, but as an integration test i would recommend not worrying about acronyms until you need to 🙂 but yes i think business logic should be as separate from everything else as is reasonable
Want results from more Discord servers?
Add your server