如果我说您在编写测试时应该重复代码,您将如何反应?您可能会认为我不知道我在说什么,我将打破软件开发中最受关注的原则之一,即DRY principle。让我说服您,完全可以做到这一点,这是您在编写测试时应该做更多的事情。
重复代码的情况
假设我们需要测试一个称为postStatus
的函数,该功能接受帖子并返回帖子状态。这是我们需要测试的实现:
function postStatus(post: Post): PostStatusEnum {
if (post.isDraft) {
return PostStatusEnum.Draft
}
if (post.scheduledDate > Date.now()) {
return PostStatusEnum.Scheduled
}
if (!post.authorId) {
return PostStatusEnum.MissingAuthor
}
return PostStatusEnum.Published
}
这是我经常看到的测试:
describe('postStatus()', () => {
let postData: Post
beforeEach(() => {
postData = {
isDraft: false,
scheduledDate: null,
authorId: null
}
})
it(`returns ${PostStatusEnum.MissingAuthor} if post isn't scheduled, isn't draft but doesn't have an author assigned`, () => {
expect(postStatus(postData)).toBe(PostStatusEnum.MissingAuthor)
})
it(`returns ${PostStatusEnum.Draft} if post is marked as draft`, () => {
postData.isDraft = true
expect(postStatus(postData)).toBe(PostStatusEnum.Draft)
})
// other test cases
})
在第一眼看,这个测试看起来还不错,对吗?当测试中断并且需要调试时会发生什么?您浏览了失败的测试用例,想知道postData
来自何处,定义的位置以及如何连接到失败的测试用例?对于第一个测试案例,尤其如此,因为整个测试套件只有一行,您不知道为什么postData
使函数返回PostStatusEnum.Draft
状态。
在此示例中可能很明显,但让我们想象一个测试案例,其中有200行代码和十二个测试用例,每个测试都取决于测试范围中定义的多个变量。在调试这种测试的同时,将点连接并将所有内容保持在您的心理记忆中将非常困难。
这就是我要编写相同测试的方式:
it(`returns ${PostStatusEnum.MissingAuthor} if post isn't scheduled, isn't draft but doesn't have an author assigned`, () => {
const postData: Post = {
isDraft: false,
scheduledDate: null,
authorId: null
}
expect(postStatus(postData)).toBe(PostStatusEnum.MissingAuthor)
})
it(`returns ${PostStatusEnum.Draft} if post is marked as draft`, () => {
const postData: Post = {
isDraft: true,
scheduledDate: null,
authorId: null
}
expect(postStatus(postData)).toBe(PostStatusEnum.Draft)
})
// other test cases
在这种情况下测试失败时,开发人员需要了解为什么测试失败的所有内容都包含在测试中。该测试不依赖封闭范围中定义的变量,并且很容易遵循。
我在每个测试案例中都重复自己,这不是我们应该做的,对吗?测试代码与生产代码有些不同。我们希望花费更少的时间来掌握测试代码的意图,我们希望最大程度地减少测试代码的认知负载,以便我们可以更多地专注于生产代码。哪个测试示例更难理解,干>或重复自己的一个测试示例?以我的经验,这是后者更容易理解和维护的方式。
但是我不想重复自己吗?
不用担心,因为我们将稍微清除最后一个测试示例,并且仍然具有最小的认知负载。让我们看一下替代方法:
it(`returns ${PostStatusEnum.MissingAuthor} if post isn't scheduled, isn't draft but doesn't have an author assigned`, () => {
const postData = buildPost({
scheduledDate: null,
isDraft: false,
authorId: null
})
expect(postStatus(postData)).toBe(PostStatusEnum.MissingAuthor)
})
it(`returns ${PostStatusEnum.Draft} if post is marked as draft`, () => {
const postData: Post = buildPost({ isDraft: false })
expect(postStatus(postData)).toBe(PostStatusEnum.Draft)
})
function buildPost(overrides?: Partial<Post>): Post {
return {
isDraft: false,
scheduledDate: null,
authorId: null
}
}
// other test cases
现在,采用这种方法,我们没有重复自己和我们正在将测试数据与测试进行共围。
奖励是测试数据是激光注重的在我们正在测试的情况下。在状态测试案例草案中,我们仅通过一个Post
属性,因为测试案例的关注只是为了测试状态草案。这样,了解测试实际测试以及使函数输出的差异的原因更容易,更快。
这种方法可以在每个测试场景中使用,无论是像我们在示例中一样的简单函数测试,使用playwright或cypress的React组件,快速端点或成熟的E2E。希望这有助于写更多理智,可维护的测试ð