Search code examples
c#unit-testingtdddomain-driven-designapplication-layer

How does application layer unit tests look like in DDD?


In my job we are writing web services, which are called by an app. We are working in agile mind set using domain driven design. As in DDD we have domain and application layer. However, we encountered problem in writing unit tests for those layers, because it seems that we are testing domain logic twice: in domain unit tests and again in application unit tests:

Application unit test

    [TestMethod]
    public void UserApplicationService_SignOut_ForExistingUserWithBalance_ShouldClearBalanceAndSignOut()
    {
        //Arrange
        long merchantId = 1;
        long userId = 1;

        var transactionId = "001";
        var id = "122";            
        var user = Help.SetId<User>(User.Register(id, new DateTime(2015, 01, 01, 00, 00, 01)), userId);

        _usersDb.Add(user);
        var userBonusBalanceRepository = _testContext.MoqUnitOfWork.MockUnitOfWork.Object.GetUserBonusAccountRepository();

        UserBonusAccount uba = userBonusBalanceRepository.GetForUser(user);
        uba.PayTo(
            new Domain.Core.Money { TotalAmount = 10, BonusAmount = 0 },
            new Domain.Core.Outlet
            {
                BonusPercentage = 50,
                IsLoyalty = true,
                Id = id,
                OutletId = "111"
            },
            transactionId,
            DateTime.Now);
        userBonusBalanceRepository.Update(uba);          

        //Act
        _testContext.UserApplicationService.SignOut(id);

        //Assert
        var firstOrDefault = this._balances.FirstOrDefault(x => x.UserId == user.Id && x.MerchantId == merchantId);
        Assert.IsTrue(firstOrDefault != null && firstOrDefault.Balance == 0);
        Assert.IsNotNull(this._transactions.Where(x => x.Id == transactionId && x.Type == BonusTransactionType.EraseFunds));
    }

Domain unit test

    [TestMethod]
    public void UserBonusAccount_ClearBalances_shouldClearBalancesForAllMerchants()
    {
        long userId = 1;
        long firstMerchantId = 1;
        long secondMerchantId = 2;
        User user = User.Register("111", new DateTime(2015, 01, 01, 00, 00, 01));
        Shared.Help.SetId(user, userId);
        List<BonusTransaction> transactions = new List<BonusTransaction>();
        List<BonusBalance> balances = new List<BonusBalance>();

        var userBonusAccount = UserBonusAccount.Load(transactions.AsQueryable(), balances.AsQueryable(), user);

        userBonusAccount.PayTo(new Money {TotalAmount = 100, BonusAmount = 0},
            new Outlet
            {
                BonusPercentage = 10,
                IsLoyalty = true,
                MerchantId = firstMerchantId,
                OutletId = "4512345678"
            }, "001", DateTime.Now);

        userBonusAccount.PayTo(new Money {TotalAmount = 200, BonusAmount = 0},
            new Outlet
            {
                BonusPercentage = 10,
                IsLoyalty = true,
                MerchantId = secondMerchantId,
                OutletId = "4512345679"
            }, "002", DateTime.Now);

        userBonusAccount.ClearBalances();

        Assert.IsTrue(userBonusAccount.GetBalanceAt(firstMerchantId) == 0);
        Assert.IsTrue(userBonusAccount.GetBalanceAt(secondMerchantId) == 0);
    }

As you can see these both tests checks whether user balance is 0, which is domain responsibility. Thus the question is: how application layer unit tests should look like and what it should test? Somewhere I read that unit tests should test in "application services for flow control and domain models for business rules". Could someone elaborate more and give some examples what application layer unit tests should test and look like?


Solution

  • App Service Unit Tests

    The responsibilities of app services include input validation, security and transaction control. So this is what you should test!

    Here are some example questions that app service unit tests should provide and answer for:

    Does my app service...

    • behave correctly (e.g. return the expected error) when I pass in garbage?
    • allow only admins to access it?
    • correctly commit the transaction in the success case?

    Depending on how exactly you implement these aspects it may or may not make sense to test them. Security, for example, is often implemented in a declarative style (e.g. with C# attributes). In that case you may find a code review approach more appropriate than checking the security attributes of each and every app service with a unit test. But YMMV.

    Also, make sure your unit tests are actual unit tests, i.e. stub or mock everything (especially domain objects). It's not clear in your test that this is the case (see side note below).

    Testing Strategies for App Services in General

    Having unit tests for app services is a good thing. However, on the app service level, I find integration tests to be more valuable in the long run. So I typically suggest the following combined strategy for testing app services:

    1. Create unit tests for the good and bad cases (TDD style if you want to). Input validation is important, so don't skip the bad cases.
    2. Create an integration test for the good case.
    3. Create additional integration tests if required.

    Side Note

    Your unit tests contain a few code smells.

    For example, I always instantiate the SUT (system under test) directly in unit tests. Like that, you exactly know what dependencies it has, and which of them are stubbed, mocked, or the real one is used. In your tests, this is not at all clear.

    Also, you seem to depend on fields for collecting the test output (this._balances for example). While this is usually not a problem if the test class contains only a single test, it can be problematic otherwise. By depending on fields, you depend on state that is "external" to the test method. This can make the test method difficult to understand, because you can't just read through the test method, you need to consider the whole class. This is the same problem that occurs when over-using setup and tear-down methods.