您应该在编写测试时重复自己
#javascript #typescript #测试 #jest

如果我说您在编写测试时应该重复代码,您将如何反应?您可能会认为我不知道我在说什么,我将打破软件开发中最受关注的原则之一,即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。希望这有助于写更多理智,可维护的测试ð