草庐IT

Next.js | Jest + React testing library + Typescript 单元测试框架搭建及实现

oil欧哟 2023-07-17 原文

前言

单元测试和 E2E 测试作为 前端项目健壮性的保障,在许多团队可能并没有足够的能力和资源去实现很好的自动化测试,但基本的了解还是必不可少的。

最近在上手并集成单元测试到已有的 Next.js 项目中,网上的文档虽然很多,但实际使用的时候遇到的问题可谓五花八门,这篇文章可以帮助你快速的在 Next.js + TS 的环境中构建单元测试环境及进行单测的编写。

框架搭建

Jest 是目前最主流的前端测试框架,仅通过 Jest 没办法完成前端的所有单元测试,因为前端的单元测试涉及到 Dom 和事件的模拟。因此我们还需要一些测试辅助库来为我们模拟相关的场景。DOM Testing Library 适用于任何提供 DOM API 的环境,这个库提供了包含 ReactVue, Angular 这三个主流框架的 API,使用这些 API 我们可以在执行单测时实现 类似于用户在页面上查找元素 的方式查询 DOM 中的节点的操作,帮助我们实现更加符合真实环境的测试。

下面我们在已有的 Next.js 项目的基础上引入 JestReact testing library 以及相关的一些依赖,执行以下命令:

yarn add jest jest-environment-jsdom @testing-library/react @testing-library/jest-dom -D

接下来我们需要创建一下一下 Jest 的配置文件,在项目根目录下创建 jest.config.ts ,写入以下配置:

const nextJest = require('next/jest');

const createJestConfig = nextJest({
  // Provide the path to your Next.js app to load next.config.js and .env files in your test environment
  dir: './',
});

// Add any custom config to be passed to Jest
const customJestConfig = {
  // Add more setup options before each test is run
  // if using TypeScript with a baseUrl set to the root directory then you need the below for alias' to work
  moduleDirectories: ['node_modules', '<rootDir>/'],
  testEnvironment: 'jest-environment-jsdom',
  testPathIgnorePatterns: ['<rootDir>/cypress/'],
  setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1',
  },
};

// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
module.exports = createJestConfig(customJestConfig);

export {};

然后再创建 jest.setup.ts 文件,暂时先写入以下配置:

import '@testing-library/jest-dom/extend-expect';

这个配置文件是根据 Next.js 文档中提供的配置进行修改的,我们先看看文档中对于这段配置的描述:

在后台,next/jest 会自动为我们配置 Jest,包括以下步骤:

  • 使用 SWC 设置 transform
  • 自动 mock 样式(包括.css.module.css 和它们的 scss 变量)、图片 import 以及 @next/font
  • 加载.env(所有的环境变量)到process.env
  • 在解析及编译测试时忽略 node_modules 目录
  • 在解析测试时忽略 .next 目录
  • 加载next.config.js用于启用 SWC transform 的配置

而我单独加的配置有如下几个:

  1. testPathIgnorePatterns: ['<rootDir>/cypress/']: 需要忽略的测试目录,这里我忽略了 cypres 目录,因为我们的 E2E 测试使用的是 Cypress ,如果不忽略的话,在执行 jest 的测试命令时会把 E2E 的测试也给跑起来。
  2. setupFilesAfterEnv: ['<rootDir>/jest.setup.ts']:配置 Jest 的 setup 文件位置,Jest 的 setup 文件用于在 执行测试时先执行一些固定的初始化操作,例如执行一些 import 等操作,这样就不需要在每一个测试中单独引入某些依赖了。
  3. moduleNameMapper: { '^@/(.*)$': '<rootDir>/src/$1', }:这一步是为了配置一下测试文件中 引入依赖时的别名,就像 webpack 或 vite 中 alias 配置的功能,当在引入依赖的目录中开头为 @ 时,会指向根目录下的 src 目录。
  4. 在配置文件的最后有一个 export {} ,这一步是为了解决当 tsconfig.json 中的 isolatedModules 设置为 true 时,如果 ts 文件中不存在
    importexport 时,ts 认为这个文件不是一个 ES Module,而是一个全局脚本,导致编译时报错。

接下来我们在 package.json 中新增一个指令:

"scripts": {
  //...
  "test": "jest"
}

当我们执行例如 yarn test 时,Jest 会去识别我们目录中的所有测试文件并执行。此时如果我们执行运行测试,会打印如下错误:

Error: Jest: Failed to parse the TypeScript config file

这个错误的原因是我们的配置文件是由 Node.js 去直接执行的,而 Node.js 并不能直接执行 ts 文件,我们也没有将配置文件进行编译,所以我们需要安装一下 ts-node,执行以下命令进行安装:

yarn add ts-node -D

安装后再次执行 yarn test 就不会报错了,ts-node 的作用就是让 Node.js 可以直接运行 ts 脚本,在执行的过程中自动进行编译,不需要我们自己提前编译为 js 脚本,更多详情可以参考 ts-node 官网

测试 Demo 编写

在框架搭建完成后,我们来实现一个简单的 demo 上手一下,首先我们需要设计一下测试的文件要放在哪个目录,这里我是将组件的测试目录与组件放在同一个目录中的,例如我们有一个 返回上一级的按钮组件 Back ,它的目录为 src/components/Back/index.tsx,这里我们再创建一个 src/components/Back/__test__ 目录用于放置测试文件,然后我们在目录中创建 index.test.tsx 文件,写入以下代码:

import { render, screen } from '@testing-library/react';

import Back from '../index';

describe('Back', () => {
  it('should render with default text', () => {
    render(<Back />);

    const element = screen.getByText(/back/i);

    expect(element).toBeInTheDocument();
  });

  it('should render with right test', () => {
    render(<Back text="Back to home" />);

    const element = screen.getByText(/Back to home/i);

    expect(element).toBeInTheDocument();
  });
});

describe 用于定义一个 测试用例组it 则用于定义一个 单独的测试用例,这两个 API 是全局定义的,并不需要引入。

代码中我们创建了一个测试用例组,其中包含了两个测试,it 的第一个参数用于描述测试的作用,这里就简单测试下 默认情况传入参数的情况 下组件是否能正确渲染。render 用于渲染组件,screen 大家可以理解为测试环境的 document,可以用于获取渲染的 dom 节点,expect 则是用于检查值是否满足特定条件。

因此这里的测试流程也就是:

  1. 引入并渲染组件
  2. 根据某些条件获取 dom 节点
  3. 判断组件是否正确渲染

后续更复杂的测试大致也是根据这种测试思路进行延伸,关于 API 的具体作用请参考 React-testing-library 文档Jest API 文档

配置 Github Action

在我们的测试可以顺利执行后,我们可以实现一下单测的自动化,这里以 Github Action
为例,基础的实现还是非常简单的,首先创建一个新的 workfolw 配置文件 your_project/.github/workflows/frontend-unit.yml

然后写入以下配置:

name: CI
on: push
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - name: Install modules
      run: yarn
    - name: Run tests
      run: yarn test

这个配置文件就是在 ubuntu 环境中执行 yarn test 开始测试,当然在实际的项目中肯定不止这么简单,但根据实际情况进行触发时机的修改或增加执行流程即可,当然你也可以增加测试覆盖率的打印等命令,这个会在后面我实践后单独出文章介绍。

总结

这篇文章介绍了下如何在 Next.js 搭建组件测试的环境以及最基本的测试实现,关于更多的踩坑以及一些最佳实践会在后续摸索后继续出文章,如果大家对于 E2E 测试想要了解也可以评论区留言,在 E2E 测试方面我们已经有了比较丰富的实践经验,可以期待下后续的文章。如果这篇文章对你有帮助欢迎点赞或关注

有关Next.js | Jest + React testing library + Typescript 单元测试框架搭建及实现的更多相关文章

  1. ruby-on-rails - 使用 Ruby on Rails 进行自动化测试 - 最佳实践 - 2

    很好奇,就使用ruby​​onrails自动化单元测试而言,你们正在做什么?您是否创建了一个脚本来在cron中运行rake作业并将结果邮寄给您?git中的预提交Hook?只是手动调用?我完全理解测试,但想知道在错误发生之前捕获错误的最佳实践是什么。让我们理所当然地认为测试本身是完美无缺的,并且可以正常工作。下一步是什么以确保他们在正确的时间将可能有害的结果传达给您? 最佳答案 不确定您到底想听什么,但是有几个级别的自动代码库控制:在处理某项功能时,您可以使用类似autotest的内容获得关于哪些有效,哪些无效的即时反馈。要确保您的提

  2. ruby - 使用 C 扩展开发 ruby​​gem 时,如何使用 Rspec 在本地进行测试? - 2

    我正在编写一个包含C扩展的gem。通常当我写一个gem时,我会遵循TDD的过程,我会写一个失败的规范,然后处理代码直到它通过,等等......在“ext/mygem/mygem.c”中我的C扩展和在gemspec的“扩展”中配置的有效extconf.rb,如何运行我的规范并仍然加载我的C扩展?当我更改C代码时,我需要采取哪些步骤来重新编译代码?这可能是个愚蠢的问题,但是从我的gem的开发源代码树中输入“bundleinstall”不会构建任何native扩展。当我手动运行rubyext/mygem/extconf.rb时,我确实得到了一个Makefile(在整个项目的根目录中),然后当

  3. ruby - Ruby 的 Hash 在比较键时使用哪种相等性测试? - 2

    我有一个围绕一些对象的包装类,我想将这些对象用作散列中的键。包装对象和解包装对象应映射到相同的键。一个简单的例子是这样的:classAattr_reader:xdefinitialize(inner)@inner=innerenddefx;@inner.x;enddef==(other)@inner.x==other.xendenda=A.new(o)#oisjustanyobjectthatallowso.xb=A.new(o)h={a=>5}ph[a]#5ph[b]#nil,shouldbe5ph[o]#nil,shouldbe5我试过==、===、eq?并散列所有无济于事。

  4. ruby - RSpec - 使用测试替身作为 block 参数 - 2

    我有一些Ruby代码,如下所示:Something.createdo|x|x.foo=barend我想编写一个测试,它使用double代替block参数x,这样我就可以调用:x_double.should_receive(:foo).with("whatever").这可能吗? 最佳答案 specify'something'dox=doublex.should_receive(:foo=).with("whatever")Something.should_receive(:create).and_yield(x)#callthere

  5. ruby - Sinatra:运行 rspec 测试时记录噪音 - 2

    Sinatra新手;我正在运行一些rspec测试,但在日志中收到了一堆不需要的噪音。如何消除日志中过多的噪音?我仔细检查了环境是否设置为:test,这意味着记录器级别应设置为WARN而不是DEBUG。spec_helper:require"./app"require"sinatra"require"rspec"require"rack/test"require"database_cleaner"require"factory_girl"set:environment,:testFactoryGirl.definition_file_paths=%w{./factories./test/

  6. ruby-on-rails - 迷你测试错误 : "NameError: uninitialized constant" - 2

    我遵循MichaelHartl的“RubyonRails教程:学习Web开发”,并创建了检查用户名和电子邮件长度有效性的测试(名称最多50个字符,电子邮件最多255个字符)。test/helpers/application_helper_test.rb的内容是:require'test_helper'classApplicationHelperTest在运行bundleexecraketest时,所有测试都通过了,但我看到以下消息在最后被标记为错误:ERROR["test_full_title_helper",ApplicationHelperTest,1.820016791]test

  7. ruby - 即使失败也继续进行多主机测试 - 2

    我已经构建了一些serverspec代码来在多个主机上运行一组测试。问题是当任何测试失败时,测试会在当前主机停止。即使测试失败,我也希望它继续在所有主机上运行。Rakefile:namespace:specdotask:all=>hosts.map{|h|'spec:'+h.split('.')[0]}hosts.eachdo|host|begindesc"Runserverspecto#{host}"RSpec::Core::RakeTask.new(host)do|t|ENV['TARGET_HOST']=hostt.pattern="spec/cfengine3/*_spec.r

  8. ruby-on-rails - 如何使辅助方法在 Rails 集成测试中可用? - 2

    我在app/helpers/sessions_helper.rb中有一个帮助程序文件,其中包含一个方法my_preference,它返回当前登录用户的首选项。我想在集成测试中访问该方法。例如,这样我就可以在测试中使用getuser_path(my_preference)。在其他帖子中,我读到这可以通过在测试文件中包含requiresessions_helper来实现,但我仍然收到错误NameError:undefinedlocalvariableormethod'my_preference'.我做错了什么?require'test_helper'require'sessions_hel

  9. ruby-on-rails - Cucumber 是否只是 rspec 的包装器以帮助将测试组织成功能? - 2

    只是想确保我理解了事情。据我目前收集到的信息,Cucumber只是一个“包装器”,或者是一种通过将事物分类为功能和步骤来组织测试的好方法,其中实际的单元测试处于步骤阶段。它允许您根据事物的工作方式组织您的测试。对吗? 最佳答案 有点。它是一种组织测试的方式,但不仅如此。它的行为就像最初的Rails集成测试一样,但更易于使用。这里最大的好处是您的session在整个Scenario中保持透明。关于Cucumber的另一件事是您(应该)从使用您的代码的浏览器或客户端的角度进行测试。如果您愿意,您可以使用步骤来构建对象和设置状态,但通常您

  10. ruby-on-rails - 如何调试 cucumber 测试? - 2

    我有:When/^(?:|I)follow"([^"]*)"(?:within"([^"]*)")?$/do|link,selector|with_scope(selector)doclick_link(link)endend我打电话的地方:Background:GivenIamanexistingadminuserWhenIfollow"CLIENTS"我的HTML是这样的:CLIENTS我一直收到这个错误:.F-.F--U-----U(::)failedsteps(::)nolinkwithtitle,idortext'CLIENTS'found(Capybara::Element

随机推荐