5

在我的工作中,我们正在编写由应用程序调用的 Web 服务。我们正在使用领域驱动设计以敏捷的思维方式工作。与 DDD 一样,我们有域和应用层。但是,我们在为这些层编写单元测试时遇到了问题,因为我们似乎在测试域逻辑两次:域单元测试和应用程序单元测试:

应用单元测试

    [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));
    }

领域单元测试

    [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);
    }

如您所见,这两个测试都检查用户余额是否为 0,这是域责任。因此问题是:应用层单元测试应该是什么样子,应该测试什么?在某处我读到单元测试应该在“用于流控制的应用程序服务和用于业务规则的域模型”中进行测试。有人可以详细说明并举例说明应用层单元测试应该测试和看起来像什么吗?

4

1 回答 1

10

应用服务单元测试

应用服务的职责包括输入验证、安全和事务控制。所以这是你应该测试的!

以下是应用服务单元测试应提供和回答的一些示例问题:

我的应用服务是否...

  • 当我传入垃圾时,行为是否正确(例如返回预期的错误)?
  • 只允许管理员访问?
  • 在成功案例中正确提交事务?

根据您实现这些方面的准确程度,测试它们可能有意义,也可能没有意义。例如,安全性通常以声明式的方式实现(例如,使用 C# 属性)。在这种情况下,您可能会发现代码审查方法比使用单元测试检查每个应用服务的安全属性更合适。但是YMMV。

此外,请确保您的单元测试是实际的单元测试,即存根或模拟所有内容(尤其是域对象)。在您的测试中尚不清楚情况是否如此(请参阅下面的附注)。

应用服务的一般测试策略

对应用服务进行单元测试是一件好事。然而,在应用服务层面,我发现集成测试从长远来看更有价值。因此,我通常建议使用以下组合策略来测试应用服务:

  1. 为好用例和坏用例创建单元测试(如果需要,可以使用 TDD 样式)。输入验证很重要,所以不要跳过不好的情况。
  2. 为好的案例创建一个集成测试。
  3. 如果需要,创建额外的集成测试。

边注

您的单元测试包含一些代码异味。

例如,我总是在单元测试中直接实例化 SUT(被测系统)。像这样,您确切地知道它有哪些依赖项,以及其中哪些是存根、模拟或使用了真正的依赖项。在您的测试中,这一点都不清楚。

此外,您似乎依赖于收集测试输出的字段(this._balances例如)。如果测试类只包含一个测试,这通常不是问题,否则可能会出现问题。通过依赖字段,您依赖于测试方法“外部”的状态。这会使测试方法难以理解,因为不能只通读测试方法,需要考虑整个类。这与过度使用设置和拆卸方法时出现的问题相同。

于 2015-12-28T10:31:06.560 回答