Leaky abstraction and clean architecture template
Mohsen Esmailpour
Posted on June 29, 2021
According to Wikipedia in software development, a leaky abstraction is an abstraction that leaks details that it is supposed to abstract away. The term "leaky abstraction" was popularized in 2002 by Joel Spolsky. An earlier paper by Kiczales describes some of the issues with imperfect abstractions and presents a potential solution to the problem by allowing for the customization of the abstraction itself.
As systems become more complex, software developers must rely upon more abstractions. Each abstraction tries to hide complexity, letting a developer write software that "handles" the many variations of modern computing.
However, this law claims that developers of reliable software must learn the abstraction's underlying details anyway.
During the past year, I've seen several implementations of clean architecture that inspired form Jason Taylor Clean Architecture Solution Template and all of them have a common interface, IApplicationDbContext. This interface aims to hide the underlying data access technology that is being used but when you look at the interface you notice that the interface is coupled to Entity Framework Core. I also can guess Jason Taylor assumed that you are always using EF Core and it never going to change.
Let's see leaked abstraction in action. I want to implement a unit test for UpdateTodoListCommandHandler
and assume it is a real-world project and we some logic inside the Handle
method but we don't implement integration for each scenario. Here is the actual implementation of :
namespace CleanArchitecture.Application.TodoLists.Commands.UpdateTodoList
{
public class UpdateTodoListCommand : IRequest
{
public int Id { get; set; }
public string Title { get; set; }
}
public class UpdateTodoListCommandHandler : IRequestHandler<UpdateTodoListCommand>
{
private readonly IApplicationDbContext _context;
public UpdateTodoListCommandHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<Unit> Handle(UpdateTodoListCommand request, CancellationToken cancellationToken)
{
var entity = await _context.TodoLists.FindAsync(request.Id);
if (entity == null)
{
throw new NotFoundException(nameof(TodoList), request.Id);
}
entity.Title = request.Title;
await _context.SaveChangesAsync(cancellationToken);
return Unit.Value;
}
}
}
I want to ensure when the entity does not exist, NotFoundException
is thrown.
[Test]
public void UpdateTodoList_WhenEntityNotExist_ThrowsException()
{
// Arrange
var list = new List<TodoList>();
var queryable = list.AsQueryable();
var dbSet = new Mock<DbSet<TodoList>>();
dbSet.As<IQueryable<TodoList>>().Setup(m => m.Provider).Returns(queryable.Provider);
dbSet.As<IQueryable<TodoList>>().Setup(m => m.Expression).Returns(queryable.Expression);
dbSet.As<IQueryable<TodoList>>().Setup(m => m.ElementType).Returns(queryable.ElementType);
dbSet.As<IQueryable<TodoList>>().Setup(m => m.GetEnumerator()).Returns(() => queryable.GetEnumerator());
dbSet.Setup(d => d.FindAsync(It.IsAny<object[]>())).ReturnsAsync((object[] id) => list.SingleOrDefault(t => t.Id == (int)id[0]));
var dbContext = new Mock<IApplicationDbContext>();
dbContext.SetupGet(d => d.TodoLists).Returns(dbSet.Object);
var sut = new UpdateTodoListCommandHandler(dbContext.Object);
var command = new UpdateTodoListCommand { Id = 1, Title = "Test" };
// Act
var exception = Assert.ThrowsAsync<NotFoundException>(() => sut.Handle(command, new CancellationToken()));
// Assert
Assert.NotNull(exception);
}
As you can see I tried to mock IApplicationDbContext
and the interface should help to hide underlying implementation but to implement such a test you should know how to mock DbSet
. If you use the ApplicationDbContext
class instead of IApplicationDbContext
interface, the result will be the same and same amount of code is need to mock ApplicationDbContext
class.
Instead, we can use repository pattern and hide underlying technology and mock it easily.
Lets replace IApplicationDbContext
with ITodoListRepository
.
public interface ITodoListRepository
{
Task<TodoList> GetByIdAsync(int id);
Task UpdateAsync(TodoList todoList);
}
And the handler:
public class UpdateTodoListCommandHandler : IRequestHandler<UpdateTodoListCommand>
{
private readonly ITodoListRepository _repository;
public UpdateTodoListCommandHandler(ITodoListRepository repository)
{
_repository = repository;
}
public async Task<Unit> Handle(UpdateTodoListCommand request, CancellationToken cancellationToken)
{
var entity = await _repository.GetByIdAsync(request.Id);
if (entity == null)
{
throw new NotFoundException(nameof(TodoList), request.Id);
}
entity.Title = request.Title;
await _repository.UpdateAsync(entity);
return Unit.Value;
}
}
Now we can refactor the test method:
[Test]
public void UpdateTodoList_WhenEntityNotExist_ThrowsException()
{
// Arrange
var list = new List<TodoList>();
var repository = new Mock<ITodoListRepository>();
repository.Setup(r => r.GetByIdAsync(It.IsAny<int>())).ReturnsAsync((int id) => list.SingleOrDefault(t => t.Id == id));
var command = new UpdateTodoListCommand { Title = "Test", Id = 1 };
var sut = new UpdateTodoListCommandHandler(repository.Object);
// Act
var exception = Assert.ThrowsAsync<NotFoundException>(async () => await sut.Handle(command, new CancellationToken()));
// Assert
Assert.NotNull(exception);
Assert.AreEqual(typeof(NotFoundException), exception.GetType());
}
Posted on June 29, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.