编写测试
测试可以用多种方式编写。本节试图给出一种标准,以及我们编写测试方式背后的理由。
测试必须使用 describe 以及 它 jest 提供的方法
描述和构建上下文
该 describe describe 方法在可能的情况下应被用来描述待测试代码将被执行的上下文,并且应在其作用域内构建该上下文。
describe('当标志为红色时', () => {
...
})
describe('当标志为蓝色时', () => {
...
})为了描述上下文,我们应使用以下词语之一: 当……时 用于顶层 describe,以及 具有 或 和 用于表示存在一个依赖于之前定义的其他上下文的上下文。这样做的理由是,通过组合 describe 语句来获得自描述的上下文,使其能够被轻松理解和检查。由于我们的想法是将上下文的描述连接起来, describe 语句 必须使用小写。
describe('当标志为红色时', () => {
...
describe('并且风很强时', () => {
// 上下文这里通过这些 describe 的合取来描述:
// “当红旗为红色并且风很强时”
...
})
})多个 describe 可以嵌套在同一层级和更低层级。同一层级的 describe 表示不同的上下文,而更低层级中的 describe(嵌套 describe)表示更具体的上下文。
如前所述,每个 describe 都应配有一个构建上下文的方法,或一个销毁/更改上下文的方法。必须使用的方法是 beforeEach 以及 afterEach.
使用 beforeEach 方法创建的上下文会随着我们在 describe 树中向更深层推进而变得更加明确。每个 beforeEach 都会根据其文本所述更具体地说明上下文。
变量作用域
该 变量 在上下文中设置的变量应根据它们将被使用的作用域来定义。也就是说,如果某个变量只在嵌套 describe 的特定上下文中使用,那么该变量应只在该特定上下文中定义。这样做的理由是让变量尽可能贴近使用它们的代码,从而使代码更易于理解。
……类型的变量 原始类型,其 不会被更改 在其定义的上下文或后续上下文中应定义为 const 一次,而不是在 beforeEach 作用域内重复定义。
……类型的变量 原始类型 在其定义的上下文或后续上下文中会被更改的必须定义为 let,因为 TypeScript 的特性要求如此。
……类型的变量 非原始类型 (对象、数组等)必须定义为 let 且它们的值必须在 beforeEach 中设置。 这样做的理由是,JS 中的对象是可变的,这意味着任何使用该对象的代码执行都可能发生变化,并以非自愿的方式影响其他测试。
变量应尽可能正确地进行类型标注。重复的类型应抽象为一个类型。
描述期望与执行代码
该 它 方法必须始终放在 describe 并且必须用于描述我们对将要测试的代码的期望,而且如果可能,应只包含 一个 断言。每个 它 中包含多个断言 它 如果存在性能问题,则可以这样做,因为测试将针对每个 beforeEach(因为
这种结构背后的理由是,为测试审阅者以及将来维护和修改代码的开发者提供清晰性,因为每个上下文和期望都被清楚地列出,从而更容易理解正在测试什么、如何测试,以及还剩下什么要测试。
沿着 describe 描述一直到 它,开发者就可以通过连接这些句子轻松理解正在测试什么。
编写清晰的期望
写在 它中的描述必须尽可能详细地说明对测试执行的期望。开发者绝不能使用抽象或笼统的措辞来定义期望,因为这会削弱对代码期望内容的清晰性。
需要考虑到,在指定 它中的期望,或者上下文会是什么时 describe中,开发者可以在有助于更好理解意图的情况下使用函数名或精确的错误消息,但在可能时应使用文本表示,以便让测试更易维护。
测试什么
选择测试什么会因正在执行的代码而异。不同因素,从性能到大量输入范围,都可能显著改变测试中应该测试或不应该测试什么。这里我们将给出一组单独的用例,开发者应尽可能测试这些用例。
这里的 run 函数有几个不同的执行流程。函数或代码段不应仅根据不同的可能执行流程来测试,而应根据函数的期望来测试,因为函数可能并没有按照其编写目的在工作,而测试也可能与代码过于耦合,从而无法捕捉其中的不同问题。这意味着开发者应始终测试所有可能的执行路径,并且也应根据函数语义测试其他可能路径。
对于这个特定情况,开发者应测试 至少 以下情况:
以大于 1000 公里的距离运行 run 函数
以等于 10 公里的距离运行 run 函数
以大于 10 公里但小于 1000 公里的距离运行 run 函数
对于第一种情况,开发者需要测试 run 函数会抛出异常。 必须检查异常的错误消息,因为也可能抛出其他异常,这会使测试目的失效。如果该异常是自定义异常,开发者可以检查该异常的实例。
对于第二种情况,开发者需要测试该函数返回了与传入相同的公里数。
对于第三种情况,开发者需要测试该函数返回了外部函数 beLazy 返回的内容,并且由于 doSomething (也在该方法中执行)无法通过函数的返回值来检查,开发者应 测试它是否以正确的参数被调用.
何时进行 mock
是否进行 mock 主要取决于开发者正在编写的测试类型。
单元测试应将所有外部函数都 mock,简单功能函数除外,例如字符串格式化等。
API 测试应只对与外部服务通信的部分进行 mock,也就是数据库或其他 API。如果测试中执行的某个操作影响了测试套件的性能,可以通过实现 mock 来缓解这个问题。
所有 mock 都必须使用 Jest 提供的工具完成,但以下情况除外: redux-saga-test-plan mock。
Mock 和辅助函数
任何 Jest mock 都应使用其 一次性 方法,如果可能的话,也就是
mockReturnValueOnce或mockResolvedValueOnce不推荐mockReturnValue或mockResolvedValue。这样做的理由是防止不希望执行的 mock 被调用,从而改变我们的测试执行。Jest 提供了许多可用于不同类型 mock 的类型。例如,在 mock 一个对象时,你应使用
jest.Mocked<typeof someObject>;对于类,你应使用jest.MockedClass<typeof SomeClass>;对于函数,jest.MockedFunction<typeof someFunction>.一个
afterEach应编写为在每次测试后运行的jest.resetAllMocks,以清除任何全局 mock 模块的 mock 实现,这些实现可能会泄漏到其他测试中。如果你使用mockReturnValueOnce或mockResolvedValueOnce,则可以忽略这个重置,因为它会自动为你完成。名为
mocks的目录可以创建来存放 mock,这些 mock 应放在test或spec目录中。大型 mock 必须存放在与测试不同的文件中,以避免测试文件过于冗长。小型 mock,如果数量不多,应保留在测试中。Mock 文件应以其所 mock 的实体命名。例如,如果我们在 mock 一个 profile,那么该 mock 应放在
/test/mocks/profile.ts文件中。如果需要多个大型 mock,它们应放在一个以待 mock 实体命名的目录中的不同文件里。例如,如果我们有两个 profile mock,它们会存储为:
/test/mocks/profile/profile-with-wearables.ts和/test/mocks/profile/profile-without-wearables.ts。为了便于访问它们,你应创建一个index.ts文件放在/test/mocks/profile/目录中并将它们导出。为了避免对被 mock 对象的变更,mock 必须以函数形式导出 ,并由函数返回它们。
最后更新于