具有生成字段的记录类型的相等性测试
Equality test for a record type with generated fields
如果记录类型包含生成的字段,例如自动生成的 ID 和时间戳,例如:
type UserCreated = {
Id: Guid
Name: string
CreationTime: Instant
}
为此编写单元测试的最佳方法是什么?不可能简单地断言记录等于任何特定值,因为无法提前知道 Id 和 CreationTime 值是什么。
可能的解决方案:
- 对每个字段进行单独断言
- 将一个函数传递给处理 ID 和日期生成的用户创建函数。然后单元测试可以注入一个 returns 预先确定值的存根函数。或者确实让调用者直接传入id和时间戳。
- 使用某种镜头库
- 从不自动生成任何内容,由客户提前决定所有字段
- 使用自定义相等比较器(这听起来根本不是个好主意)
- 还有别的吗?
最好的方法是什么?
您并没有真正测试记录类型本身——它只是无意义的数据。您要测试的是对该数据进行操作的函数。
当一个函数是纯函数时,它很容易测试。调用 Guid.NewGuid()
和 DateTime.Now
都是副作用,我假设您对这里的两个有问题的字段所做的。因此,将这两位重构为您作为参数显式传递的函数将是我书中的方法(您的要点 2)。这将使代码更易于测试并且更纯粹(也许可读性也更差一些,我想你需要平衡一下)。
也就是说 - 对于大多数涉及您的记录的测试用例,您应该在安排测试时知道这些字段的值。只有当您测试创建记录 'from nothing' 的函数时,您事先并不知道它们,对于这种情况,您可以只检查 Name
字段是否具有预期值(所以你的要点1 必要时,比较其他地方的记录 'as is')。
编辑:不知道大家有没有漏掉,还有一种可能是在测试项目中定义一个专门的比较函数,保证'generated' 两个记录中的值在比较它们之前是相同的:
let compareWithoutGenerated (a: UserCreated) (b: UserCreated) =
a = { b with Id = a.Id; CreationTime = a.CreationTime }
然后:
WhateverUnit.Assert.True(compareWithoutGenerated a b)
显然这很丑陋,但我会说这是公平的测试游戏。可以说有更好的方法来做到这一点,但至少,这种方法不会对您的生产代码造成测试引起的损害。
我的方法确实是将一个函数传递给处理 ID 和日期生成的用户创建函数。如果您必须传入的参数太多,那么这可能是您需要重构设计的线索。
这里是领域层,例如:
// =================
// Domain
// =================
open System
type UserCreated = {
Id: Guid
Name: string
CreationTime: DateTime
}
// the generation functions are passed in
let createCompleteRecord generateGuid generateTime name =
{
Id = generateGuid()
Name = name // add validation?
CreationTime = generateTime()
}
在应用程序代码中,您将使用部分应用程序来烘焙生成器并创建一个有用的函数 createRecord
// =================
// Application code
// =================
let autoGenerateGuid() = Guid.NewGuid()
let autoGenerateTime() = DateTime.UtcNow
// use partial application to get a useful version
let createRecord = createCompleteRecord autoGenerateGuid autoGenerateTime
let recForApp = createRecord "myname"
在测试代码中,您将使用部分应用程序烘焙其他生成器并创建不同的函数 createRecordForTesting
// =================
// Test code
// =================
let explicitGenerateGuid() = Guid.Empty
let explicitGenerateTime() = DateTime.MinValue
// use partial application to get a useful version
let createRecordForTesting = createCompleteRecord explicitGenerateGuid explicitGenerateTime
let recForTest = createRecordForTesting "myname"
Assert.AreEqual(Guid.Empty,recForTest.Id)
Assert.AreEqual("myname",recForTest.Name)
Assert.AreEqual(DateTime.MinValue,recForTest.CreationTime)
并且由于 "generated" 字段现在具有硬编码值,您还可以测试整个记录相等逻辑:
let recForTest1 = createRecordForTesting "myname"
let recForTest2 = createRecordForTesting "myname"
Assert.AreEqual(recForTest1,recForTest2)
如果记录类型包含生成的字段,例如自动生成的 ID 和时间戳,例如:
type UserCreated = {
Id: Guid
Name: string
CreationTime: Instant
}
为此编写单元测试的最佳方法是什么?不可能简单地断言记录等于任何特定值,因为无法提前知道 Id 和 CreationTime 值是什么。
可能的解决方案:
- 对每个字段进行单独断言
- 将一个函数传递给处理 ID 和日期生成的用户创建函数。然后单元测试可以注入一个 returns 预先确定值的存根函数。或者确实让调用者直接传入id和时间戳。
- 使用某种镜头库
- 从不自动生成任何内容,由客户提前决定所有字段
- 使用自定义相等比较器(这听起来根本不是个好主意)
- 还有别的吗?
最好的方法是什么?
您并没有真正测试记录类型本身——它只是无意义的数据。您要测试的是对该数据进行操作的函数。
当一个函数是纯函数时,它很容易测试。调用 Guid.NewGuid()
和 DateTime.Now
都是副作用,我假设您对这里的两个有问题的字段所做的。因此,将这两位重构为您作为参数显式传递的函数将是我书中的方法(您的要点 2)。这将使代码更易于测试并且更纯粹(也许可读性也更差一些,我想你需要平衡一下)。
也就是说 - 对于大多数涉及您的记录的测试用例,您应该在安排测试时知道这些字段的值。只有当您测试创建记录 'from nothing' 的函数时,您事先并不知道它们,对于这种情况,您可以只检查 Name
字段是否具有预期值(所以你的要点1 必要时,比较其他地方的记录 'as is')。
编辑:不知道大家有没有漏掉,还有一种可能是在测试项目中定义一个专门的比较函数,保证'generated' 两个记录中的值在比较它们之前是相同的:
let compareWithoutGenerated (a: UserCreated) (b: UserCreated) =
a = { b with Id = a.Id; CreationTime = a.CreationTime }
然后:
WhateverUnit.Assert.True(compareWithoutGenerated a b)
显然这很丑陋,但我会说这是公平的测试游戏。可以说有更好的方法来做到这一点,但至少,这种方法不会对您的生产代码造成测试引起的损害。
我的方法确实是将一个函数传递给处理 ID 和日期生成的用户创建函数。如果您必须传入的参数太多,那么这可能是您需要重构设计的线索。
这里是领域层,例如:
// =================
// Domain
// =================
open System
type UserCreated = {
Id: Guid
Name: string
CreationTime: DateTime
}
// the generation functions are passed in
let createCompleteRecord generateGuid generateTime name =
{
Id = generateGuid()
Name = name // add validation?
CreationTime = generateTime()
}
在应用程序代码中,您将使用部分应用程序来烘焙生成器并创建一个有用的函数 createRecord
// =================
// Application code
// =================
let autoGenerateGuid() = Guid.NewGuid()
let autoGenerateTime() = DateTime.UtcNow
// use partial application to get a useful version
let createRecord = createCompleteRecord autoGenerateGuid autoGenerateTime
let recForApp = createRecord "myname"
在测试代码中,您将使用部分应用程序烘焙其他生成器并创建不同的函数 createRecordForTesting
// =================
// Test code
// =================
let explicitGenerateGuid() = Guid.Empty
let explicitGenerateTime() = DateTime.MinValue
// use partial application to get a useful version
let createRecordForTesting = createCompleteRecord explicitGenerateGuid explicitGenerateTime
let recForTest = createRecordForTesting "myname"
Assert.AreEqual(Guid.Empty,recForTest.Id)
Assert.AreEqual("myname",recForTest.Name)
Assert.AreEqual(DateTime.MinValue,recForTest.CreationTime)
并且由于 "generated" 字段现在具有硬编码值,您还可以测试整个记录相等逻辑:
let recForTest1 = createRecordForTesting "myname"
let recForTest2 = createRecordForTesting "myname"
Assert.AreEqual(recForTest1,recForTest2)