← Back to Clean Architecture

Clean Architecture

Testing Without Frameworks

The payoff for the rule — business logic tested with no web server, database, or UI in the room.

Every card in this notebook has been paying toward the same bill. Here is the receipt: BasketService.TransferBasketAsync, the use case that merges an anonymous cart into a signed-in buyer’s, tested in TransferBasket.cs with nothing running but the test runner.

A real business rule, tested directly

public class TransferBasket
{
    private readonly string _nonexistentAnonymousBasketBuyerId = "nonexistent-anonymous-basket-buyer-id";
    private readonly string _existentAnonymousBasketBuyerId = "existent-anonymous-basket-buyer-id";
    private readonly string _existentUserBasketBuyerId = "testuser@microsoft.com";
    private readonly IRepository<Basket> _mockBasketRepo = Substitute.For<IRepository<Basket>>();
    private readonly IAppLogger<BasketService> _mockLogger = Substitute.For<IAppLogger<BasketService>>();The test fakes IRepository, not EfRepository. No CatalogContext has to start.

    public class Results<T>
    {
        private readonly Queue<Func<T>> _values = new();
        public Results(T result) { _values.Enqueue(() => result); }
        public Results<T> Then(T value) { return Then(() => value); }
        public Results<T> Then(Func<T> value)
        {
            _values.Enqueue(value);
            return this;
        }
        public T Next() { return _values.Dequeue()(); }
    }

    [Fact]
    public async Task TransferAnonymousBasketItemsWhilePreservingExistingUserBasketItems()
    {
        var anonymousBasket = new Basket(_existentAnonymousBasketBuyerId);
        anonymousBasket.AddItem(1, 10, 1);
        anonymousBasket.AddItem(3, 55, 7);
        var userBasket = new Basket(_existentUserBasketBuyerId);
        userBasket.AddItem(1, 10, 4);
        userBasket.AddItem(2, 99, 3);The baskets are real entities with real AddItem rules. Only the store is fake.

        var results = new Results<Basket>(anonymousBasket)
            .Then(userBasket);

        _mockBasketRepo.FirstOrDefaultAsync(Arg.Any<BasketWithItemsSpecification>(), Arg.Any<CancellationToken>()).Returns(x => results.Next());
        var basketService = new BasketService(_mockBasketRepo, _mockLogger);new BasketService with those fakes is the whole setup — no WebApplicationFactory.
        await basketService.TransferBasketAsync(_nonexistentAnonymousBasketBuyerId, _existentUserBasketBuyerId);
        await _mockBasketRepo.Received().UpdateAsync(userBasket, Arg.Any<CancellationToken>());

        Assert.Equal(3, userBasket.Items.Count);
        Assert.Contains(userBasket.Items, x => x.CatalogItemId == 1 && x.UnitPrice == 10 && x.Quantity == 5);
        Assert.Contains(userBasket.Items, x => x.CatalogItemId == 2 && x.UnitPrice == 99 && x.Quantity == 3);
        Assert.Contains(userBasket.Items, x => x.CatalogItemId == 3 && x.UnitPrice == 55 && x.Quantity == 7);The assertion is the merged cart a shopper would see, proven in a method call.
    }
}

new BasketService(_mockBasketRepo, _mockLogger) is the whole setup. No WebApplicationFactory, no appsettings.json, no connection string. The test builds two real Basket entities, hands the service a fake of IRepository<Basket>, and asserts on the merged quantities directly — the same behaviour a shopper triggers by logging in with items already in an anonymous cart, run here in a method call.

What never had to start

_mockBasketRepo is a fake of IRepository<Basket>, the interface The Dependency Rule first pointed at — not EfRepository<T>. So nothing here opens a CatalogContext, nothing runs a migration, nothing needs SQL Server installed on the machine running the test. The whole file’s dependencies are xunit and NSubstitute; ASP.NET Core never enters the process.

eShopOnWeb also has IntegrationTests, PublicApiIntegrationTests, and EndToEndTests projects, and they earn their cost: they are the ones that prove EfRepository<T> really talks to SQL Server and CatalogItemGetByIdEndpoint really returns JSON. But the business rule — merge these items, keep these quantities, drop the old basket — does not need any of that machinery to be proven correct. It only needed ApplicationCore to stay ignorant of it in the first place.