DDD 中的应用层单元测试是什么样子的?
How does application layer unit tests look like in DDD?
在我的工作中,我们正在编写由应用程序调用的网络服务。我们正在使用领域驱动设计以敏捷的思维方式工作。与 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,这是域责任。因此问题是:应用层单元测试应该是什么样子,它应该测试什么?我在某处读到单元测试应该在 "application services for flow control and domain models for business rules" 中测试。有人可以详细说明并举例说明应用层单元测试应该测试什么吗?
应用服务单元测试
应用服务的职责包括输入验证、安全和交易控制。所以这是你应该测试的!
以下是应用服务单元测试应提供并回答的一些示例问题:
我的应用服务是否...
- 当我传入垃圾时行为正确(例如 return 预期的错误)?
- 只允许管理员访问吗?
- 正确提交成功案例中的事务?
根据您实施这些方面的具体方式,测试它们可能有意义也可能没有意义。例如,安全性通常以声明方式实现(例如使用 C# 属性)。在这种情况下,您可能会发现代码审查方法比使用单元测试检查每个应用程序服务的安全属性更合适。但是YMMV.
此外,请确保您的单元测试是实际的单元测试,即存根或模拟所有内容(尤其是域对象)。在您的测试中不清楚是否属于这种情况(请参阅下面的旁注)。
一般应用服务测试策略
对应用服务进行单元测试是一件好事。但是,在应用程序服务级别上,我发现集成测试在长 运行 中更有价值。因此,我通常建议采用以下组合策略来测试应用服务:
- 为好的和坏的情况创建单元测试(如果你愿意,可以采用 TDD 风格)。输入验证很重要,所以不要跳过坏情况。
- 为好的案例创建集成测试。
- 如果需要,创建额外的集成测试。
旁注
您的单元测试包含一些代码味道。
例如,我总是在单元测试中直接实例化SUT(被测系统)。这样,您就可以确切地知道它有哪些依赖项,以及其中哪些是存根的、模拟的,或者使用了真实的。在您的测试中,这一点一点都不清楚。
此外,您似乎依赖字段来收集测试输出(例如 this._balances
)。如果测试 class 只包含一个测试,这通常不是问题,否则可能会出现问题。通过依赖于字段,您依赖于 "external" 测试方法的状态。这会使测试方法难以理解,因为你不能只通读测试方法,你需要考虑整个class。这与过度使用设置和拆卸方法时出现的问题相同。
在我的工作中,我们正在编写由应用程序调用的网络服务。我们正在使用领域驱动设计以敏捷的思维方式工作。与 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,这是域责任。因此问题是:应用层单元测试应该是什么样子,它应该测试什么?我在某处读到单元测试应该在 "application services for flow control and domain models for business rules" 中测试。有人可以详细说明并举例说明应用层单元测试应该测试什么吗?
应用服务单元测试
应用服务的职责包括输入验证、安全和交易控制。所以这是你应该测试的!
以下是应用服务单元测试应提供并回答的一些示例问题:
我的应用服务是否...
- 当我传入垃圾时行为正确(例如 return 预期的错误)?
- 只允许管理员访问吗?
- 正确提交成功案例中的事务?
根据您实施这些方面的具体方式,测试它们可能有意义也可能没有意义。例如,安全性通常以声明方式实现(例如使用 C# 属性)。在这种情况下,您可能会发现代码审查方法比使用单元测试检查每个应用程序服务的安全属性更合适。但是YMMV.
此外,请确保您的单元测试是实际的单元测试,即存根或模拟所有内容(尤其是域对象)。在您的测试中不清楚是否属于这种情况(请参阅下面的旁注)。
一般应用服务测试策略
对应用服务进行单元测试是一件好事。但是,在应用程序服务级别上,我发现集成测试在长 运行 中更有价值。因此,我通常建议采用以下组合策略来测试应用服务:
- 为好的和坏的情况创建单元测试(如果你愿意,可以采用 TDD 风格)。输入验证很重要,所以不要跳过坏情况。
- 为好的案例创建集成测试。
- 如果需要,创建额外的集成测试。
旁注
您的单元测试包含一些代码味道。
例如,我总是在单元测试中直接实例化SUT(被测系统)。这样,您就可以确切地知道它有哪些依赖项,以及其中哪些是存根的、模拟的,或者使用了真实的。在您的测试中,这一点一点都不清楚。
此外,您似乎依赖字段来收集测试输出(例如 this._balances
)。如果测试 class 只包含一个测试,这通常不是问题,否则可能会出现问题。通过依赖于字段,您依赖于 "external" 测试方法的状态。这会使测试方法难以理解,因为你不能只通读测试方法,你需要考虑整个class。这与过度使用设置和拆卸方法时出现的问题相同。