AI工具赋能自动生成单测用例

流程图:

痛点:

  • ChatGPT单次对话有长度限制
  • 生成内容的不可控性,可能会生成不准确、误导性或者不合适的内容,需要进行后续处理
  • 多轮对话时,要确保有效的维护和处理对话的上下文
  • 接口吞吐过程中的错误处理和反馈

整体代码:

js 复制代码
const fs = require('fs').promises
const path = require('path')
const base = process.cwd()
const axios = require('axios')

const categoryToFilter = ['node_modules', '.git', '.husky', '.vscode'] // 要排除的类目名称
let FILE_NAME = 'packages'

let globalSystemPrompt = null
async function init(state) {
  await import('inquirer').then(async (inquirerModule) => {
    const inquirer = inquirerModule.default
    const globalSystemRolePrompt = [
      {
        name: '你是一名前端开发工程师',
        value: '你是一名前端开发工程师'
      },
      {
        name: '你是一名后端开发工程师',
        value: '你是一名后端开发工程师'
      },
      {
        name: '自定义角色',
        value: ''
      }
    ]
    const arr = [
      {
        type: 'list',
        name: 'globalSystemRolePrompt',
        message: '⚡️⚡️⚡️预制您的角色(多选)⚡️⚡️⚡️',
        choices: globalSystemRolePrompt,
        default: ''
      },
      {
        type: 'input',
        name: 'globalSystemPromptContent',
        message: '请输入您对于本次的预制Prompt:',
        // default: '接下来的对话,我们使用vue3基于antd组件进行了二次开发。在此基础上进行单元测试代码的输出。'
        default: '你的工作主要是,基于antd组件进行了二次开发组件的单元测试代码的输出。'
      }
    ]
    await inquirer.prompt(state ? arr : [arr[arr.length - 1]]).then(async (answer) => {
      if (!answer.globalSystemRolePrompt && !answer.globalSystemPromptContent) {
        return
      }
      globalSystemPrompt = ` ${globalSystemPrompt || ''} ${answer.globalSystemRolePrompt || ''} ${
        answer.globalSystemPromptContent || ''
      }`
      await use_ai('你是什么角色?你都能做什么事情?回答是否可以开始工作', false, null)
    })
  })
}

const service = axios.create({
  // 在这里可以添加其他配置
  timeout: 500000
})

// 禁用 Axios 自动重试
service.defaults.retry = 0 // 设置最大重试次数为 0

async function use_ai(question, filePath, allItem) {
  const url = API 地址

  const api_key = 'API 密钥' // 替换为实际的 API 密钥

  const headers = {
    'Content-Type': 'application/json',
    'api-key': api_key
  }

  const data = {
    temperature: 0,
    messages: [
      { role: 'system', content: globalSystemPrompt },
      { role: 'user', content: question }
    ]
  }
  console.log('\x1b[31muser:==>', `\x1b[0m${question}`)

  let spinner = null
  import('ora').then((ora) => {
    spinner = ora.default('Loading...').start()
  })
  await axios
    .post(url, data, { headers })
    .then(async (response) => {
      const mes = allItem?.name ? `${allItem?.name} 组件` : ''
      spinner.succeed(`已完成 ${mes} `)
      const response_json = response.data
      const regex = /```javascript([\s\S]*?)```/
      const content = response_json.choices[0]?.message?.content.replace(regex, (match, code) => {
        return code.trim() // 去掉代码块两端的空白字符
      })

      console.log('\x1b[32mGPT:==>', `\x1b[0m${response_json.choices[0]?.message?.content}`)
      // const content = response_json.choices[0]?.message?.content
      // console.log(content)
      if (filePath) {
        await fs.writeFile(filePath, content, 'utf-8')
      } else {
        import('inquirer').then(async (inquirerModule) => {
          const inquirer = inquirerModule.default
          await inquirer
            .prompt([
              {
                type: 'list',
                name: 'isInquire',
                // message: '⚡️⚡️⚡️选择需要的预制Prompt(多选)⚡️⚡️⚡️',
                message: '⚡️⚡️⚡️是否要继续补充Prompt⚡️⚡️⚡️',
                choices: [
                  {
                    name: '是',
                    value: 1
                  },
                  {
                    name: '否',
                    value: 0
                  }
                ],
                default: ''
              }
            ])
            .then(async (answer) => {
              console.log('\x1b[31m预制Prompt:==>', `\x1b[0m${globalSystemPrompt}`)

              console.log('\x1b[32mGPT:==>', `\x1b[0m${content}`)

              if (answer.isInquire) {
                await init(0)
              } else {
                await inquirerFun()
              }
            })
        })
      }
    })
    .catch((error) => {
      spinner.stop()
      console.error('Error:', error)
    })
}

// 通过chatgpt接口获取需要单测的内容信息及写入文件
/**
 *
 * @param {Array} vueFiles 需要单测的数据
 * @param {String} choicesQuestion 预制的prompt
 */
async function fetchDataAndWriteToFile(vueFiles, choicesQuestion) {
  for (const item of vueFiles) {
    try {
      const folderPath = `${base}/tests/${item.name}` // 文件夹路径
      const filePath = path.join(folderPath, `${item.name}.test.js`) // 文件路径
      await fs.mkdir(folderPath, { recursive: true })
      let pathMes = `单测组件的引入路径为${item.componentPath}`
      await use_ai(`${item.content} ${choicesQuestion} ${pathMes}`, filePath, item)
    } catch (error) {
      console.error('Error:', error)
    }
  }
}
// 获取需要单测的vue文件内容
/**
 * @param {String} directory 根路径
 * @returns {Array} vueFiles 获取单测目录的信息
 */
async function readVueFilesRecursively(directory) {
  try {
    const items = await fs.readdir(directory, { withFileTypes: true })

    const vueFiles = [] // 获取单测目录的信息

    for (const item of items) {
      const itemPath = path.join(directory, item.name)

      if (item.isDirectory()) {
        const nestedVueFiles = await readVueFilesRecursively(itemPath)
        vueFiles.push(...nestedVueFiles)
      } else if (item.isFile() && path.extname(item.name) === '.vue') {
        const segments = itemPath.split(path.sep)
        const index = segments.indexOf('src') + 1

        const vueFilesName = segments[index].split('.')[0]
        const fileContent = await fs.readFile(itemPath, 'utf-8')
        const withoutStyle = fileContent.replace(/<style[^>]*>[\s\S]*?<\/style>/g, '')
        /**
         * @param {String} name 文件名
         * @param {String} componentPath 组件路径
         * @param {String} content 组件vue中提出style样式的内容
         */
        vueFiles.push({ name: vueFilesName, componentPath: itemPath.split(':')[1] || '', content: withoutStyle })
      }
    }

    return vueFiles
  } catch (error) {
    console.error(`Error reading directory ${directory}: ${error}`)
    return []
  }
}
// readVueFilesRecursively(`${base}/${FILE_NAME}`)
//   .then(async (vueFiles) => {
//     console.log(vueFiles)
//   })
//   .catch((error) => {
//     console.error('Error:', error)
//   })
async function inquirerFun() {
  // 终端内选择步骤
  import('inquirer').then(async (inquirerModule) => {
    const inquirer = inquirerModule.default
    // 获取项目一级目录
    let filesChoices = null
    // 获取选择的prompt
    let choicesQuestion = null
    const questionInformation = [
      // {
      //   name: '已上代码基于ant-design-vue组件二次开发生成单测用例',
      //   value: '已上代码基于ant-design-vue组件二次开发生成单测用例'
      // },
      {
        name: '断言是否成功挂载组件',
        value: '断言是否成功挂载组件'
      },
      {
        name: '断言是否正确传递了属性',
        value: '断言是否正确传递了属性'
      },
      {
        name: '断言插槽内容是否被正确渲染',
        value: '断言插槽内容是否被正确渲染'
      },
      {
        name: '断言组件 DOM 是否包含指定的类名',
        value: '断言组件 DOM 是否包含指定的类名'
      },
      {
        name: '断言点击事件是否被触发',
        value: '断言点击事件是否被触发'
      },
      {
        name: '只输出单测代码,禁止输出文字',
        value: '只输出单测代码,禁止输出文字'
      }
    ]
    try {
      const items = await fs.readdir(base, { withFileTypes: true })
      filesChoices = items
        .filter((item) => item.isDirectory() && !categoryToFilter.includes(item.name))
        .map((item) => {
          return {
            name: item.name,
            value: item.name
          }
        })
    } catch (error) {
      console.error(`Error reading directory ${base}: ${error}`)
    }
    await inquirer
      .prompt([
        {
          type: 'list',
          name: 'ExecutionTest',
          message: '⚡️⚡️⚡️选择自动化生成单测用例方案⚡️⚡️⚡️',
          choices: [
            { name: '全量自动化用例', value: 1 },
            { name: '单文件自动化用例', value: 2 },
            { name: '放弃生成用例', value: 0 }
          ],
          default: true
        },
        {
          type: 'list',
          name: 'ExecutionTestFile',
          message: '⚡️⚡️⚡️请选择要访问的文件目录⚡️⚡️⚡️',
          choices: filesChoices,
          default: '',
          when: (answers) => {
            return answers.ExecutionTest
          }
        },
        {
          type: 'checkbox',
          name: 'ExecutionTestQuestion',
          // message: '⚡️⚡️⚡️选择需要的预制Prompt(多选)⚡️⚡️⚡️',
          message: '⚡️⚡️⚡️预制的提问信息(多选)⚡️⚡️⚡️',
          choices: questionInformation,
          default: '',
          when: (answers) => {
            return answers.ExecutionTest && answers.ExecutionTestFile
          }
        },
        {
          type: 'input',
          name: 'customQuestion',
          // message: '请输入您自定义的Prompt:',
          message: '请输入您自定义的提问信息:',
          when: (answers) => {
            return answers.ExecutionTest
          }
        }
      ])
      .then(async (answer) => {
        // 取消构建
        if (answer.ExecutionTest == 0) {
          return
        }
        // 执行的目录 base
        FILE_NAME = answer.ExecutionTestFile

        // 选择预制提问信息 + 手动输入的提问信息
        choicesQuestion = `${answer.ExecutionTestQuestion.join(',')}${
          answer.ExecutionTestQuestion.length && answer.customQuestion ? ',' : '。'
        } ${answer.customQuestion}`
        if (!choicesQuestion) {
          console.error('***请选择或者输入Prompt***')
          return
        }

        // 全链路构建
        if (answer.ExecutionTest == 1) {
          // console.log('全链路自动构建', FILE_NAME, choicesQuestion)
          readVueFilesRecursively(`${base}/${FILE_NAME}`)
            .then(async (vueFiles) => {
              if (!vueFiles.length) {
                return
              }
              await fetchDataAndWriteToFile(vueFiles, choicesQuestion)
            })
            .catch((error) => {
              console.error('Error:', error)
            })
        }

        // 选择性构建
        if (answer.ExecutionTest == 2) {
          readVueFilesRecursively(`${base}/${FILE_NAME}`)
            .then(async (vueFiles) => {
              if (!vueFiles.length) {
                return
              }
              await inquirer
                .prompt([
                  {
                    type: 'checkbox',
                    name: 'aloneFileName',
                    message: '⚡️⚡️⚡️请选择单测组件⚡️⚡️⚡️',
                    choices: vueFiles.map((item) => {
                      return {
                        name: item.name,
                        value: item.name
                      }
                    }),
                    default: ''
                  }
                ])
                .then(async (item) => {
                  if (!item.aloneFileName) {
                    return
                  }
                  const aloneFileArr = vueFiles.filter((file) => file.name == item.aloneFileName)
                  // console.log('单独构建单测', FILE_NAME, choicesQuestion, item.aloneFileName, aloneFileArr)
                  await fetchDataAndWriteToFile(aloneFileArr, choicesQuestion)
                })
            })
            .catch((error) => {
              console.error('Error:', error)
            })
        }
      })
  })
}

init(1)
相关推荐
花花鱼几秒前
vue3 基于element-plus进行的一个可拖动改变导航与内容区域大小的简单方法
前端·javascript·elementui
k09334 分钟前
sourceTree回滚版本到某次提交
开发语言·前端·javascript
EricWang135825 分钟前
[OS] 项目三-2-proc.c: exit(int status)
服务器·c语言·前端
September_ning25 分钟前
React.lazy() 懒加载
前端·react.js·前端框架
web行路人35 分钟前
React中类组件和函数组件的理解和区别
前端·javascript·react.js·前端框架
超雄代码狂1 小时前
ajax关于axios库的运用小案例
前端·javascript·ajax
长弓三石1 小时前
鸿蒙网络编程系列44-仓颉版HttpRequest上传文件示例
前端·网络·华为·harmonyos·鸿蒙
小马哥编程1 小时前
【前端基础】CSS基础
前端·css
嚣张农民1 小时前
推荐3个实用的760°全景框架
前端·vue.js·程序员
周亚鑫2 小时前
vue3 pdf base64转成文件流打开
前端·javascript·pdf