前端CI入门攻略:使用 Github Actions + ts-jest 实现CI

2,769 阅读9分钟

CI的概念

首先,CI的全称是Continuous Integration,直译为可持续集成,而我们对其的理解是频繁地(一天多次)将代码集成到主干。针对这句话我们要理解两点:

  1. 主干指什么?
  2. 集成又是什么意思?

带着这点思考,我们先去了解下基于git分支策略,毕竟CI这概念是基于分支管理实现的。

我们来看一下github flow的分支管理策略,如图所示:

image.png

github flow在开发新特性的运行模式如下所示:

  1. 基于master创建新的分支feature进行开发。注意这需要保证master的代码和特性永远是最稳定的。
  2. 开发期间定期提交更改(commit change)到远程仓库
  3. 通过创建pull request去对master发起合并
  4. pull request在经过审核确认可行后合并到master分支

如果用github flow这个分支管理模型去理解CI,可以对上面两个问题做出回答:

  1. 主干是指包含多个已上和即将上线的特性的分支,例如github flow分支模型中的master分支,git flow分支模型中的release分支。
  2. 集成是指把含新特性的分支合并(merge)到主干

在理解CI的含义后,我们要思考的是,如何保证CI的实现。阮一峰老师的持续集成是什么?里说到过:

持续集成的目的,就是让产品可以快速迭代,同时还能保持高质量。它的核心措施是,代码集成到主干之前,必须通过自动化测试。只要有一个测试用例失败,就不能集成。

在上面的github flow分支模型里也提到:我们频繁地集成主干时,要保证主干的特性的代码和特性是稳定安全的,主干是可以随时发布部署的。那这样子就意味着,我们需要一系列措施来保证主干的安全可靠。网上教程中很多的CI保证措施是:新特性集成主干后,触发一系列自动化测试去校验主干的安全稳定。但这里有个隐患是,当主干校验不通过时,此时到修复的时间过程里,主干都是不安全的,不能够用于部署和拉取分支开发,这样子不利于多人共同开发迭代。

因此我认为,CI的保证措施应该是:要先保证将要被集成的分支的安全可靠后,才能将其集成主干上。如github flow的运行机制一样,在pull request发起后,通过一系列审核流程来保证开发分支的特性是安全可靠的、代码是符合规范的。

基于上面的思路,我们来动手学习实现下CI的保证措施。

学习实现CI

这个章节里,我会:

  1. 介绍市面上各种CI工具
  2. 快速精简地入门Github Actions
  3. 通过vite及其自带的模板快速新建一个前端项目,然后基于这个项目通过接入Github Actions实现CI

选择适合的CI工具

首先,由于这篇文章是带新手往自己的项目接入CI的,因此我们从新人的角度去筛选适合我们的CI工具:

  • Gitlab CI: 非常强大的CI工具,如今DevOps思潮下的产物,当前我司使用的CI工具,但需要在自己的服务器上安装GitlabGitlab Runner,且因为要把代码上传到自己服务器里的Gitlab而不是Github上,因此不利于开源仓库的CI接入,因此去掉。
  • Travis CI:现在只能免费使用一个月,去掉
  • Circle CI:要钱,去掉
  • Github Actions:相比于Gitlab CI比较轻量,当能满足我们新手的大部分需求,且是Github官方推出的能与其无缝接合的工具。可行。

由此,我门决定用Github Actions实现CI

精简学习Github Actions

这里要先了解下Github Actions的基础,当我们想往自己的项目里接入Github Actions时,要在项目目录里新建.github/workflows目录。然后通过编写yml格式文件定义工作流程(workflow)去实现CI。在阅读yml文件之前,我们要先搞懂两个比较重要的概念:

  • Job(作业):一个工作流程中包含一个或多个Job,这些Job默认情况下并行运行,但我们也可以通过设置让其按顺序执行。每个Job都在指定的环境(虚拟机或容器)里开启一个Runner(可以理解为一个进程)运行,包含多个Step(步骤)
  • Step(步骤)Job的组成部分,用于定义每一部的工作内容。每个Step在运行器环境中以其单独的进程运行,且可以访问工作区和文件系统。

JobStep两者的关系如下所示:

image.png

其实travis.cigitlab-ci的配置文件中也是有JobStep这两种概念的,且作用和Github Actions的一样。

这里拿Github的官方教程中的例子来展示:

# 指定工作流程的名称
name: learn-github-actions
# 指定此工作流程的触发事件。 此示例使用 推送 事件
on: [push]
# 存放 learn-github-actions 工作流程中的所有Job
jobs:
  # 指定一个Job的名称为check-bats-version
  check-bats-version:
    # 指定该Job在最新版本的 Ubuntu Linux 的 Runner(运行器)上运行
    runs-on: ubuntu-latest
    # 存放 check-bats-version 作业中的所有Step
    steps:
      # step-no.1: 运行actions/checkout@v3操作,操作一般用uses来调用,
      # 一般用于处理一些复杂又频繁的操作例如拉取分支,安装插件
      # 此处 actions/checkout 操作是从仓库拉取代码到Runner里的操作
      - uses: actions/checkout@v3
      # step-no.2: actions/setup-node@v3 操作来安装指定版本的 Node.js,此处指定安装的版本为v14
      - uses: actions/setup-node@v3
        with:
          node-version: "14"
      # step-no.3: 运行命令行下载bats依赖到全局环境中
      - run: npm install -g bats
      # step-no.4: 运行命令行查看bats依赖的版本
      - run: bats -v

整个learn-github-actions工作流程弄成流程图可如下所示:

image.png

来一个项目试试手

首先,在npm i vite -g以保证可全局调用vite后,我们通过yarn create vite cicd-study --template react-ts创建一个技术栈为ViteReactTypescript的前端项目。创建后的代码框架可看此处stackblitz。 创建且运行后页面效果如下所示:

vite-react-ts.gif

接下来我们要对这个项目实现的代码扫描自动化测试代码扫描用于保证代码的语法和样式的统一,集成测试用于保证新特性的安全可靠稳定,这些都是CI实现的前提。

完整的代码项目地址我先放在cicd-study这里了,大家可以点击查阅。

1. 代码扫描

一般公司里都会通过类似Sonar这类代码质量管理插件来保证代码质量。不过我们也可以通过前端样式三剑侠:eslint+prettier+stylelint来简单保证。这里我直接使用本人比较喜好和经常使用的umi的代码规范:@umijs/fabric来规定三剑侠的规则了,使用方式如下所示:

.eslintrc.js

module.exports = {
  extends: [require.resolve("@umijs/fabric/dist/eslint")],
};

.prettierrc.js

const fabric = require("@umijs/fabric");

module.exports = {
  ...fabric.prettier,
};

.stylelintrc.js

const fabric = require("@umijs/fabric");

module.exports = {
  ...fabric.stylelint,
};

然后在package.jsonscript上加上对应的执行命令即可:

"scripts": {
  "dev": "vite",
  "build": "tsc && vite build",
  "preview": "vite preview",
  "lint": "npm run lint:js && npm run lint:style && npm run lint:prettier",
  "lint:js": "eslint --cache --ext .js,.jsx,.ts,.tsx ./src",
  "lint:prettier": "prettier --check \"src/**/*\" --end-of-line auto",
  "lint:style": "stylelint --fix 'src/**/*.{css,scss,less}' --cache"
}

这样子就完成了代码扫描部分了。在本地运行的效果如下所示:

image.png

2. 自动化测试

前端测试主要分单元测试(Unit Test)集成测试(Integration Test)UI 测试(UI Test)。由于项目里只有一个页面组件,所以只需对齐编写单元测试即可。

为了多写点测试用例给测试代码加点内容,我给页面对应组件App.tsx加了个props,代码如下所示:

import type { FC } from 'react';
import { useState } from 'react';
import logo from './logo.svg';
import './App.css';

interface Props {
  value: string;
}

const App: FC<Props> = ({ value }) => {
  const [count, setCount] = useState(0);

  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>Hello Vite + React!!!!!!!!</p>
        <p>
          {/*
            测试代码中需要获取的DOM元素用role属性标记,这里的role属性只会在测试代码中用到,
            这样子就可以避免代码因需求改动时,因DOM属性改变导致测试不通过。有利于TDD(测试驱动开发)开发的进行
          */}
          <button role="button" type="button" onClick={() => setCount((v) => v + 1)}>
            count is: {count}
          </button>
        </p>
        <p role="props">{value}</p>
      </header>
    </div>
  );
};

export default App;

这里我采用ts-jest+@testing-library来编写测试代码(当然还有别的选择,例如ts-jest+enzyme),测试代码如下所示:

import React from "react";
import { render, screen, fireEvent } from "@testing-library/react";
import App from "./App";

test("props is avaliable", () => {
  const value = "123";
  // 为了多写点测试用例,我给App组件加了个prop
  render(<App value={value} />);
  expect(screen.getByRole("props")).toHaveTextContent(value);
});

test("click of button is avaliable", () => {
  render(<App value="123" />);
  fireEvent.click(screen.getByRole("button"));
  expect(screen.getByRole("button")).toHaveTextContent(`count is: 1`);
});

jest.config.js的配置可以从此处查看。配置好后运行yarn test后控制台输出如下所示:

image.png

3. 配置工作流程(workflow

直接在.github/workflows上新建ci.yml定义工作流程

name: CI
# 指定在main分支发生pull_request事件时才触发运行工作流程
on:
  pull_request:
    branches: main
jobs:
  Test:
    runs-on: ubuntu-latest
    steps:
      # 拉取项目代码
      - name: Checkout repository
        uses: actions/checkout@v2
      # 下载node
      - name: Use Node.js
        uses: actions/setup-node@v3
        with:
          # vite 需要在 node>=12 的环境下执行
          node-version: "12.x"
      # 安装依赖
      - name: Installing Dependencies
        run: yarn install
      # 运行代码扫描
      - name: Running Lint
        run: yarn lint
      # 运行自动化测试
      - name: Running Test
        run: yarn test

4. 来一次工作流程的触发

当要把代码集成主干时,我们在github里创建pull request,如下所示:

image.png

点击create pull request创建后,github会对.github/workflows里的工作流程进行判断后运行,此时我们可以看到下面的情况:

image.png

此时工作流程正在运行中,在其运行成功后的效果如下所示:

image.png

我们可以通过点击Details查看执行详细信息,如下所示:

image.png

确认代码安全可靠后就可以点击Merge pull request来把新代码集成主干上。从而遵循CI的原则完成一次bug 修复新特性迭代

后记

之后会写一篇关于CD的文章来描述更多开发流程的内容。