AI 辅助测试(MEAP)(一)

原文:zh.annas-archive.org/md5/39aa73ddd3f525a677b1854ec0a3884b

译者:飞龙

协议:CC BY-NC-SA 4.0

第一章:使用大型语言模型增强测试

本章包括

  • 探索大型语言模型如何帮助测试

  • 定义与大型语言模型合作的思维方式

在这本书中,我们将使用大型语言模型来增强和加速我们的测试活动——介绍我们自己使用这项技术的最好方式之一就是要求其中一个创建这本书的介绍:

  • 你好!我是 ChatGPT,一个由 OpenAI 设计的语言模型,用于协助各种任务和回答各种问题。

  • 关于书籍《如何利用人工智能帮助支持各种软件测试活动》,我推荐 Julian Harty 和 Mahesh Sharma 的《AI 驱动测试:为您的软件测试实践增加智能》。

  • 本书探讨了人工智能(AI)技术在软件测试中的应用,以及如何帮助提高测试流程的效率和效果。作者涵盖了与 AI 驱动测试相关的各种主题,包括机器学习、自然语言处理和计算机视觉等。

  • 此外,该书包括案例研究、实际示例和在不同情境中实施 AI 驱动测试的最佳实践。对于软件测试人员、开发人员以及任何希望利用人工智能提高软件产品质量的人来说,这是一个宝贵的资源。

这个看起来像是人类可能写的人工生成的回答是通过向大型语言模型或 LLM 发送一个被称为提示的一句话指令而创建的:

ChatGPT, please introduce yourself and a book about how AI can be used to help support various software testing activities.

定义 LLM 的功能和潜力既简单又困难,而从这些工具中获得最大利益则是在两者之间找到平衡。表面上看,LLM 只是简单地接受用户的指令,并用自然语言回答。但这个简单的解释并不能充分说明 LLM 能为我们的测试带来的潜力,也不能解释我们必须克服的挑战,以从中获得最大利益。因此,在我们开始 LLM 和测试的旅程之前,让我们了解一下 LLM 如何帮助我们以及我们需要注意什么才能成功使用它们。

1.1 认识 AI 工具对测试和开发的影响

在过去,想要利用 AI 的个人需要具备开发、训练和部署 AI 模型的技能,或者可以访问一个可以为他们做这些事情的专家团队,所有这些都使得在日常活动中使用 AI 成为一项昂贵且独家的事业。但随着人工智能的最新进展,特别是大型语言模型,我们现在开始受益于一些人称之为 AI“民主化”的东西。

将人工智能整合到我们日常工作中的障碍已经大大降低。社交媒体经理现在可以使用 LLMs 生成引人注目且吸引人的文字,分析人员可以将非结构化数据总结为清晰简洁的报告,客户支持代理可以通过简单的提示快速为客户生成定制回复。LLMs 的潜力不仅供数据科学家和人工智能学者利用,对于我们这些从事测试和软件开发工作的人来说也是如此。

良好测试的价值在于它有助于挑战假设,并教育我们的团队,让我们真正了解我们的产品在特定情况下的表现。我们测试得越多,我们就能学到更多。但是,正如大多数专业测试人员所证明的那样,我们永远没有足够的时间来测试我们想要测试的所有内容。因此,为了帮助我们更有效地测试,我们寻求从自动化到左移测试的工具和技术。LLMs 为我们提供了另一种潜在途径,帮助我们增强和加速测试,以便我们能够发现和分享更多信息,进而帮助我们的团队进一步提高质量。

使 LLMs 如此有用的是,它们以易于人类理解的方式总结、转换、生成和翻译信息,而我们,作为负责测试的个人,可以将其用于我们的测试需求——所有这些都可以通过简单的聊天界面或 API 获得。从帮助我们快速创建测试自动化到在我们自己进行测试时提供支持,如果我们掌握了识别 LLMs 何时可以帮助我们,并明智地使用它们,那么我们就可以开始更快、更远、更有效地进行测试。为了帮助说明这一概念,并让我们对我们将在本书中学到的内容有所了解,让我们看一些快速的例子。

数据生成

创建和管理测试数据可能是测试中最复杂的方面之一。创建逼真、有用和匿名化的数据可能决定测试的成败,而要有效地进行此操作可能会耗费大量资源。LLMs 提供了快速生成和转换数据的能力,加速了测试数据管理过程。通过将现有数据转换为新格式,或者使用它来生成新的合成数据,我们可以利用 LLMs 来帮助我们满足测试数据需求,并为我们提供更多时间推动测试进程。

自动化测试构建

类似地,LLMs 生成和转换的能力可以在创建和维护自动化过程中使用。虽然我不建议完全让 LLMs 为我们创建自动化测试,但它们可以以有针对性的方式帮助我们快速创建页面对象、样板类、辅助方法、框架等。结合我们对产品的知识和测试设计技能,我们可以确定自动化过程中算法性和结构性部分,并使用 LLMs 加速自动化过程的这些部分。

测试设计

也许一个不太常被讨论的话题是 LLM 如何在识别和设计测试过程中帮助我们。与自动化测试类似,LLM 的价值不在于完全替代我们的测试设计能力,而在于增强它们。我们可以利用 LLM 来克服偏见和盲点,通过利用它们来扩展和建议基于我们可能已有的当前测试设计思想的想法。我们还能够以更易理解的方式总结和描述复杂的想法,以便我们从中获得测试想法的跳板。

这些示例等将在本书中探讨,以帮助我们更好地理解 LLM 何时何地可以使用,以及如何以加速我们的测试的方式来使用它们,而不是阻碍它们。我们将探讨如何建立提示来帮助我们构建高质量的生产和自动化代码,快速创建测试数据,并增强我们的脚本和探索性测试的测试设计。我们还将探讨如何调整我们自己的 LLM,使其作为我们测试的助手,消化领域知识并将其用于帮助我们构建更高质量的产品。

1.2 利用 LLM 提供价值

测试是一个协作过程,团队的所有成员都对测试负责。我们如何为测试过程做出贡献取决于我们的角色和经验,但我们都参与其中。因此,在整本书中,我们将以批判的心态来使用 LLM,探索我们可以利用 LLM 来帮助增强我们所做各种类型的测试的不同方式。我们的意图是给你提供技能,让你能够识别和利用 LLM 来增强和加速你的测试,无论你是在专业测试角色还是作为为测试过程做出贡献的开发人员,我们都可以通过建立一些规则来规范我们与想要使用的 LLM 之间的关系。

1.2.1 发挥人类和人工智能能力的优势

在整本书中,我们不仅将学习如何使用 LLM,还将学习如何建立一个工作实践,使我们的测试既能从我们的能力中受益,也能从 LLM 中受益。任何工具的价值,无论是基于人工智能还是不是,都不是来自其固有特性,而是来自用户与工具之间的关系。我们可以将这种关系视为一种区域效应模型,如图 1.1 所示。

图 1.1 工具如何增强和扩展测试的范围的概要

这张图表所展示的是,为了增加我们的影响力,我们需要与专注于扩展和增强我们核心能力的工具建立健康的关系。没有这种平衡,我们会过度依赖工具或自己。我们当然可以依靠我们的能力,但我们的影响范围将更小,受到时间、注意力和偏见等约束的限制。基本上,没有工具,我们只能做到这么多。但同样地,我们不能把所有工作都推给工具。没有一个个体作为核心,工具就缺乏方向。是的,工具可以自主运行,但如果它们没有向人类提供反馈,那么它们就没有价值。影响范围缺少中心。协同作用共生 这两个词可能同样具有人工智能那样的负载,但这本书的目的就是要帮助你理解这一点。

为了帮助我们更好地理解这种关系,让我们考虑一个例子,我们想为文件上传功能创建测试。首先,我们收到了一个用户故事,就像这样:

As a paralegal
In order to store and share legal documents
I want to be able to securely upload documents

Acceptance criteria:
  * Upload should support the following common file formats: PDF, DOC, DOCX, TXT
  * The maximum file size should be 20MB
  * When a document is uploaded it should give a progress update to the user
  * Once uploaded, the document must initially be only accessible to the person who has uploaded it
  * A report of the upload and whether it was successful or not will be stored in the auditing feature

由此,我们决定简单地依赖 LLM 来为我们生成工作,例如,发送这样一个提示:

Create tests for a file upload feature

将此提示发送给像 ChatGPT 这样的 LLM 可能会返回类似于以下内容:

1\. Test for File Upload Functionality:

    Test Case 1.1: Upload a Valid File
        Input: Choose a file with a valid format (e.g., .jpg, .pdf).
        Expected Outcome: The file is successfully uploaded, and a confirmation message is displayed.

    Test Case 1.2: Attempt to Upload an Invalid File
        Input: Choose a file with an invalid format (e.g., .exe, .html).
        Expected Outcome: An error message is displayed, indicating that the file format is not supported.

2\. Test for File Size Limit:

    Test Case 2.1: Upload a File Below the Size Limit
        Input: Choose a file smaller than the maximum allowed size.
        Expected Outcome: The file is successfully uploaded, and a confirmation message is displayed.

    Test Case 2.2: Attempt to Upload a File Exceeding the Size Limit
        Input: Choose a file larger than the maximum allowed size.
        Expected Outcome: An error message is displayed, indicating that the file exceeds the maximum size limit.

3\. Test for File Overwrite Protection:

    Test Case 3.1: Upload a File with the Same Name as an Existing File
        Input: Upload a file with a name that matches an existing file in the system.
        Expected Outcome: An error message is displayed, indicating that a file with the same name already exists.

4\. Security Tests:

    Test Case 4.1: Attempt to Upload a Malicious File (e.g., virus)
        Input: Upload a known malicious file.
        Expected Outcome: The system should detect the malicious file and prevent its upload, displaying a security warning.

5\. UI and User Experience Tests:

    Test Case 5.1: Check for a User-Friendly Interface
        Input: Use the file upload feature and evaluate the user interface for clarity and ease of use.
        Expected Outcome: The interface should be intuitive, with clear instructions and feedback for users.

    Test Case 5.2: Check for Proper Error Messages
        Input: Attempt various erroneous actions during the file upload process.
        Expected Outcome: Error messages should be descriptive, helping users understand and resolve issues.

6\. Integration Tests:

    Test Case 6.1: Check File Upload with Other Features
        Input: Test file uploads in conjunction with other features that depend on uploaded files (e.g., image processing, document storage).
        Expected Outcome: Ensure the uploaded files are correctly processed and integrated with other system components.

7\. Performance Tests:

    Test Case 7.1: Upload a Large Number of Files Concurrently
        Input: Simulate multiple users uploading a large number of files simultaneously.
        Expected Outcome: The system should handle concurrent uploads without significant performance degradation or crashes.

8\. Stress Tests:

    Test Case 8.1: Upload Files Continuously for an Extended Period
        Input: Continuously upload files for an extended period to assess system stability.
        Expected Outcome: The system should not experience memory leaks, crashes, or performance degradation during this stress test.

乍一看,回答似乎令人印象深刻。LLM 为我们提供了一系列不同的考虑测试,这给了我们开始测试的印象。然而,如果我们开始仔细考虑这个回答,我们开始注意到问题:

  • 测试用例 1.1 建议使用有效格式进行测试,但提供了我们用户故事不支持的格式示例。

  • 从第 4 点开始的建议太肤浅了。例如,安全测试 提供了一个插入恶意文件的测试,但其他什么也没有。肯定还有其他需要考虑的安全问题吧?

  • 期望结果对于每个测试用例也是非常宽泛的。例如,这句话是什么意思:Error messages should be descriptive, helping users understand and resolve issues.

LLM 设计的方式是给出一个回应,不管它被要求做什么,虽然这听起来可能很有用,但当它接收到的指令模糊时,这就成为了问题。我们所看到的例子并不是用来批评 LLM 的能力,而是为了帮助我们认识到 LLM 的响应将像它收到的提示一样详细或通用。这意味着我们从 LLM 收到的回应是宽泛而模糊的,因为我们的提示也是模糊的。通过不去思考我们想要让 LLM 做什么,提示中缺乏任何上下文或细节意味着返回的内容相当无用——这是经典的垃圾输入,垃圾输出的例子。

这就是我们领域效应试图定义的内容。要从 LLMs 中获得价值,我们需要设计清晰、上下文相关且具体的提示,以便我们可以从 LLMs 中提取更多的价值。与其将所有责任都交给一个工具来创建我们所期望的响应,我们更应该欣赏我们对于上下文的理解和将这种理解转化为清晰指令的能力是需要的,这样才能创建出一个可以最大限度地从 LLM 中获得响应的提示,从而导致写作更好的提示,例如:

Act as a professional software tester and suggest test ideas for the feature delimited by three hashes. All test ideas should be focused on discovering issues that impact the quality characteristics: Functionality, Data Integrity and Security

###
As a paralegal
In order to store and share legal documents
I want to be able to securely upload documents

Acceptance criteria:
  * Upload should support the following common file formats: PDF, DOC, DOCX, TXT
  * The maximum file size should be 20MB
  * When a document is uploaded it should give a progress update to the user
  * Once uploaded, the document must initially be only accessible to the person who has uploaded it
  * A report of the upload and whether it was successful or not will be stored in the auditing feature
###

当发送给 LLM 时,返回一个代码示例如下:

Functionality:
    Format Compatibility Testing:
        Verify that the upload feature supports the following file formats: PDF, DOC, DOCX, and TXT.
        Attempt to upload files in various formats and ensure that the system handles them correctly.

    File Size Limitation Testing:
        Test whether the system enforces a maximum file size limit of 20MB during the upload process.
        Attempt to upload files larger and smaller than 20MB to validate the system's handling of these cases.

    Progress Update Testing:
        Check that users receive real-time progress updates while a document is being uploaded.
        Verify that these updates are clear and provide meaningful information to the user.

Data Integrity:
    Document Content Verification:

    Upload files with known content and compare the content of the uploaded document with the original.
    Ensure that the content remains intact and no data corruption occurs during the upload process.

    Document Privacy:
        Test that uploaded documents are initially only accessible to the person who uploaded them.
        Attempt to access the uploaded documents using another user's account to ensure that access control is enforced.

    Data Auditing Verification:
        Confirm that the system accurately records all uploads in the auditing feature.
        Verify that the audit logs include information such as the timestamp, uploader, document name, and whether the upload was successful.

Security:
    File Type Security Testing:

    Attempt to upload files with malicious content, such as scripts or malware, and ensure that the system detects and rejects them.

    Access Control Testing:
        Validate that the system properly enforces access control by ensuring that other users cannot access the documents uploaded by someone else.

    Secure Data Transmission:
        Ensure that the document upload process uses secure communication protocols (e.g., HTTPS) to protect data during transmission.

    User Authentication and Authorization:
        Verify that the system correctly authenticates and authorizes users before allowing them to upload or access documents.

    Secure Data Storage:
        Assess how the system stores uploaded documents to ensure that they are encrypted or adequately protected against unauthorized access or data breaches.

    Session Management:
        Test how the system manages user sessions, ensuring that sessions are terminated after a period of inactivity to prevent unauthorized access.

通过创建更加专注和上下文的提示,我们创造了一个提供更丰富建议范围的响应,用于我们的测试。这不是单靠 LLM 就能实现的,而是通过我们的技能与 LLM 的协同作用,将我们的上下文转化为 LLM 可以接受并快速扩展的指令。

活动

尝试一下本章探讨过的示例提示,看看你收到了什么响应。要设置 LLM,请阅读附录 A,其中介绍了如何设置并向 ChatGPT 发送提示。

1.2.2 对 LLMs 持怀疑态度

尽管 LLMs 的潜力可以说很大,但我们应该警惕地对待它们的能力。例如,考虑一下 ChatGPT 给我们推荐这本书的介绍。它自信地向我们推荐,让我们阅读AI-Driven Testing: Adding Intelligence to Your Software Testing Practice. 问题在于,这本书根本不存在,也从未由 Julian Harty 和 Mahesh Sharma 编写过。LLM 只是凭空捏造了这个标题。

LLMs 提供了很大的潜力,但它们并不是每个问题的解决方案,也不是唯一的真理之源。我们将在第二章进一步探讨 LLMs 如何使用概率来确定响应,但现在重要的是要明确 LLM 如何得出解决方案并不与我们人类的方式相同,这突显了我们领域效应模型的第二个方面。我们必须运用怀疑精神来确定 LLM 响应中哪些内容有价值,哪些没有价值。

盲目接受 LLM 输出的内容,充其量是将我们置于事实上减慢我们工作速度的风险中,而不是加速工作——最坏的情况下,它会影响我们进行可能对产品质量产生不利影响的测试。我们必须提醒自己,我们而不是 LLMs,是引领解决问题活动的人。有时候与以一种感觉上非常人性化的方式进行沟通的工具一起工作会很困难,但这样做会使我们暴露于上述风险之中。这就是为什么在我们的领域效应模型中,我们利用我们的能力来挑选和选择 LLM 响应中对我们有益的元素,并拒绝并重新评估当 LLM 以一种不令人满意的方式响应时我们如何指示 LLM。

随着我们在书中的进展,了解更多关于 LLM 以及它们如何对测试有所贡献的知识,我们会牢记我们的影响范围模型,这样你,读者,将能够以一种冷静、考虑周到且对你和你的团队有价值的方式使用 LLM 进行测试。

1.3 总结

  • LLM 通过接受我们编写的提示来工作,并返回一个回应。

  • LLM 的流行之处在于它们提供了访问强大 AI 算法的简易方式。

  • LLM 已经帮助了许多不同角色的人,也可以帮助我们进行测试。

  • 我们可以使用 LLM 来辅助我们进行从测试设计到自动化等广泛范围的测试活动。

  • 我们要避免过度使用 LLM,并始终对它们的工作方式持批判态度。

  • 成功使用 LLM 来自于我们欣赏自己在使用它们的过程中带来的技能和能力。

  • 如果我们对 LLM 的提示浅显而普通,我们得到的回应也会是一样的。

  • 相反,我们应该运用我们的技能来理解和界定问题,并利用这一点促使 LLM 以最有价值的方式作出回应。

  • 我们还必须对从 LLM 获得的回应持怀疑态度,以确保我们从 LLM 那里得到的回应对我们有价值。

第二章:大型语言模型和提示工程

本章包括

  • 概述大型语言模型的基本工作原理

  • 了解使用大型语言模型的风险

  • 定义提示工程

  • 通过提示工程进行实验以返回各种输出

  • 用提示工程解决问题

在上一章中,我们学到了花时间熟悉新工具很重要,而且这种心态我们将在本章中采用。在整本书中,我们将探讨如何专门使用生成式 AI 工具,例如 Open AI 的 ChatGPT 和 GitHub Copilot,它们是构建在大型语言模型或 LLMs 上的。虽然 AI 在测试中有许多用途,但 LLMs 之所以如此有趣,是因为它们适应不同情况的能力,因此它们越来越受欢迎。因此,在我们看如何将 LLM 工具应用到我们的日常测试之前,让我们先了解一下 LLMs 是什么,它们是如何工作的,以及通过学习提示工程的概念,如何从中获得最大的好处。

什么使得像 ChatGPT 这样的 LLM 主导了 2023 年的技术头条?考虑一下我与 ChatGPT 的这次互动:

Mark: Hello ChatGPT, can you introduce yourself briefly?

ChatGPT: Of course! I'm ChatGPT, an AI language model developed by OpenAI. I'm based on the GPT-3.5 architecture, which means I'm designed to understand and generate human-like text based on the prompts I receive. I've been trained on a diverse range of data sources, including books, articles, websites, and other text documents, up until September 2021\. I can assist you with a wide array of topics, including answering questions, providing explanations, creative writing, and much more. Just let me know how I can help you!

Mark: Nice to meet you.

ChatGPT: Nice to meet you too! I'm here to assist you with any questions or tasks you have. How may I help you today?

Mark: I'm just getting to know you.

ChatGPT: That's perfectly fine! Feel free to ask me anything you'd like to know, whether it's about my capabilities, how I work, or anything else you're curious about. I'm here to provide information and help you out to the best of my abilities. Fire away with your questions!

基于这次快速的“对话”,我们可以看出:

  • 我可以用自然语言与 ChatGPT 进行交互。要从 ChatGPT 获取结果并不需要传统的编程经验。

  • ChatGPT 的输出也是用自然语言。它易于理解和回应。

LLM 的倡导者庆祝这些类型的人工智能工具已经使 AI 的使用实现了民主化,允许任何人使用它们来获取结果。然而,这种民主化是一把双刃剑。我们与 LLMs 互动的方式可能会让我们产生错觉,认为我们正在和一个像我们人类一样推理的机器对话。但是做出这样的假设可能会影响我们充分利用 LLM 的能力。因此,为了更好地利用诸如 ChatGPT 之类的工具,了解它们的工作方式(至少在外行人的看法下)有助于更好地理解它们如何适应我们的测试活动,并如何从中获得最大价值。

2.1 大型语言模型,解释

一个相对缺乏构建 AI 系统经验的人如何解释复杂的 LLM 系统的工作方式?幸运的是,在 Computerphile 视频“AI Language Models & Transformers”(www.youtube.com/watch?v=rURRYI66E54),Rob Miles 提供了一个示例,可以帮助我们基本了解 LLMs 的工作原理。(我强烈推荐观看他关于 AI 的所有视频。)

拿出你的手机并打开一个消息应用程序,或任何导致键盘出现的其他应用程序。在键盘上方,你可能会看到一系列建议插入到消息中的单词。例如,我的键盘为我提供了这些建议:II amThe。选择其中一个选项,例如 I am,会导致建议更新。对我来说,它提供了 awayaway fornow 的选项。再次选择 away for 选项将更新可供选择的选项。那么键盘是如何知道应该显示哪些选项以及不显示哪些选项呢?

在你的键盘中是一个与 LLM 类似的 AI 模型。这种描述是简化的,但在其核心上,你手机上的键盘正使用与 LLM 相同的机器学习方法,通过利用概率。语言是一套复杂且流动的规则,这意味着试图明确规定关系几乎是不可能的。因此,模型被训练在大规模数据集上,以隐含地学习语言中的关系,并创建一个概率分布,用来预测下一个单词可能是什么。这可以通过可视化键盘示例中提供的选项最好的描述,如图 2.1 所示。

图 2.1 行动中的概率分布

正如我们在选择术语 I am 时所见,键盘中的模型已经被训练为对大量单词进行概率分配。其中一些在 “I am” 之后有很高的概率,比如 “away”,而一些则概率较低,比如 “sandalwood”。如前所述,这些概率来自已经完成训练过程的模型,称为无监督学习,其中大量数据被发送给一个算法进行处理。正是通过这个训练过程,一个模型被创建,其中包含复杂的权重和平衡,使模型具有预测能力。

监督学习和无监督学习

在训练 AI 时,使用的两个主要技术是监督学习和无监督学习。使用哪种学习方法将决定数据如何被结构化并发送给算法。监督学习使用已被组织、标记并与输出配对的数据。例如,一个医学数据集可能包含标记的数据,包括 BMI、年龄和性别,例如,这与标记的结果配对,例如他们是否患有特定疾病,例如心脏病或中风。无监督学习,另一方面,使用未标记且没有输出数据的数据。其思想是,当算法在这种类型的数据上进行训练时,它会学习数据中的隐含模式。

如果你在键盘上玩弄预测功能,很可能输出与我的不同,即使我们使用的是相同的手机和操作系统。这是因为一旦模型被训练并在我们的手机中使用,它仍然会受到我们在手机上输入的内容的微调影响。我出差工作,所以我必须让人们知道我何时离开和何时可用。(这也许是对我工作与生活的平衡的一种谴责!)因此,“我是”和“离开”这样的术语具有增加的概率,因为它们是我更经常使用的词汇。这被称为人类反馈强化学习,或 RLHF。

再次,将手机上的预测消息与大型语言模型进行比较是一个过于简化的比较,但这种比较是成立的。大型语言模型还使用非监督学习和 RLHF。然而,不同之处在于,虽然手机上的 AI 模型可以查看可能是最近输入的五个词来预测下一个词,但大型语言模型使用了尖端技术,例如:

  • 生成预训练变换器(这是 ChatGPT 中 GPT 缩写的含义)

  • 使用数千台服务器的强大硬件基础设施

  • 在规模上训练数据将使我们的简单键盘模型所接触到的培训数据相形见绌

我们需要了解这些要点的复杂性吗?实际上不需要,但这有助于我们欣赏大型语言模型的一个关键方面。大型语言模型的输出,无论多么强大,都是概率性的。大型语言模型不是信息的存储库,其中没有像我们在更广泛的互联网上看到的结构化知识。这意味着它做出结论的方式不同于人类做出结论的方式(概率而不是经验),这就是它们如此强大但如果我们对如何使用它们不加警惕的话也是有风险的原因。

2.2 避免使用大型语言模型的风险

有一个 AI 预测接下来的一个词并不是一件容易的事情,尽管当前的大型语言模型的能力有了爆炸性的增长,但我们需要意识到其中的风险。让我们来看看其中的一些。

2.2.1 幻觉

文本预测的挑战在于确保大型语言模型的输出是合乎逻辑并且根植于现实。例如,在第一章中,当我要求 ChatGPT 为这本书写个介绍时,它分享了以下内容:

Regarding the book, “How AI can be used to help support various software testing activities,” I would recommend “AI-Driven Testing: Adding Intelligence to Your Software Testing Practice" by Julian Harty and Mahesh Sharma.

最初,在开发大型语言模型时,它们的输出并不合乎逻辑。文本可以读懂,但缺乏结构或语法上的意义。如果我们阅读这个例子,它解析得非常好,而且有意义。然而,正如我所提到的,ChatGPT 描述的这本书并不存在。在大型语言模型的上下文中,这被称为幻觉。大型语言模型能够清晰地输出一个陈述,从而赋予它一些权威性,但所写的是错误的。

为什么 LLM 会产生幻觉并不完全清楚。与 LLM 合作的一个挑战是它们的行为就像一个黑匣子。很难监控 LLM 是如何得出特定结论的,这又加剧了它的不确定性。仅仅因为我得到了一个包含幻觉的输出,并不意味着其他人将来也会得到相同的结果。(这就是强化学习与人类反馈(RLHF)帮助对抗幻觉的地方:我们可以告知模型其输出是否是错误的,它将从中学习)。

幻觉的风险意味着我们在解释 LLM 输出时必须始终保持一种怀疑的态度。我们需要注意,LLM 返回的内容是具有预测性质的,并不总是正确的。我们不能因为一个工具表现出模仿人类行为的方式就关闭我们的批判性思维。

2.2.2 数据来源

对于大多数 LLM 用户来说,对于模型确切工作方式的不了解对我们来说并不是唯一的黑匣子,还有它所训练的数据。自 ChatGPT 爆红以来,围绕数据所有权和版权的讨论日益加剧。公司如 X(前身为 Twitter)和 Reddit 指责 OpenAI 大规模盗用了他们的数据,并且在撰写本文时,一群作者已经对 OpenAI 提起了集体诉讼,指控该公司通过训练模型侵犯了他们的版权(www.theguardian.com/books/2023/jul/05/authors-file-a-lawsuit-against-openai-for-unlawfully-ingesting-their-books)。

这些争论的结果尚未见分晓,但如果我们把这个话题带回软件开发的世界,我们必须注意 LLM 已经训练过的材料。例如,ChatGPT 曾经在发送特定短语时返回荒谬的回复,这完全是因为它曾经受过来自 r/counting 子论坛的数据训练,该数据在表面上看起来似乎也是毫无意义的。您可以在 Computerphile 了解更多关于这种怪异行为的信息(www.youtube.com/watch?v=WO2X3oZEJOA)。如果 LLM 受到垃圾数据的训练,它将输出垃圾。

当我们考虑到像 GitHub Copilot 这样的工具时,就变得非常重要了。例如,Copilot 使用了 ChatGPT 使用的相同的 GPT 模型,通过使用 GitHub 存储的数十亿行代码来进行微调,以便在我们开发代码库时充当助手并提供建议代码片段。我们将在后续章节中探讨如何充分利用 Copilot,但我们应该对其建议持批判态度,不能盲目接受它提供的一切建议。为什么呢?问问自己,你对过去所创造的代码满意吗?你相信别人所创造的所有代码吗?如果有大量的工程师往往采用不好的编程模式,那么这就是 Copilot 所训练的。这个观点有些夸张,因为有很多优秀的开发人员和测试人员在做出很好的工作,而 Copilot 的训练也基于这些好的工作。但这是一个值得思考的思维实验,以确保我们记住在使用 LLMs 构建应用程序时,驾驶座位上的是谁。

2.2.3 数据隐私

我们需要关注 LLM 的输出内容,同样也需要考虑我们输入给它们的内容。面临问题时,与 LLMs 分享材料以寻找答案的冲动会很大。但我们必须问问自己,我们发送的数据存储在哪里?如前所述,LLM 持续通过 RLFH 反馈进行调整。OpenAI 和 GitHub 等公司将获取我们分享的信息,存储并用于未来的模型训练(GitHub 虽然提供一些隐私控制机制来限制其可以存储的内容)。

对于那些希望保持其知识产权私有的公司(或我们自己),这可能会引起问题。以三星为例,其员工通过使用 ChatGPT 意外泄露了机密材料,TechRadar 对此进行了描述:

公司允许其半导体部门的工程师使用 AI 编写器来帮助修复源代码中的问题。但在此过程中,工作人员输入了机密数据,如新程序的源代码本身和与其硬件有关的内部会议笔记数据。

你可以在 www.techradar.com/news/samsung-workers-leaked-company-secrets-by-using-chatgpt 上阅读相关详情。

随着组织对 LLM 的采用不断增加,我们可能会看到一些限制我们可以和不能使用 LLM 的政策的增加。有些组织可能禁止使用第三方 LLM,而有些组织则选择培训和部署自己的内部 LLM 供内部使用(这是本书第三部分要讨论的话题)。这些决策的结果将高度依赖上下文,但它们将影响我们使用何种类型的 LLM 以及什么数据可以发送和不能发送,这也强调了我们需要注意发送给 LLM 的内容。

还要重视客户隐私,因为我们有责任不仅对我们工作的公司(特别是那些签署了保密协议的公司)负责,还对我们的用户负责。我们有法律和道德义务保护用户数据不被传播到无法监督的地方。

总之,虽然 LLM 提供了丰富的机会,但我们必须避免将它们拟人化的陷阱。将 LLM 视为像我们人类一样得出结论的方式是错误的。这可能会使我们对输出产生一种危险的信任,并且可能意味着我们没有充分利用它们。但是,如果我们学会利用 LLM 在指导它们时的概率性质,我们可以增加创建能够帮助我们提高效率的输出的机会,而这正是提示工程可以帮助我们的地方。

2.3 使用提示工程改进结果

我们正在使用自然语言提示 LLM 返回所需的结果,但由于它们是概率性的,我们可以以与与人类正常交流不同的方式与它们交流。随着 LLM 的发展,出现了一个新的工程领域,被称为提示工程,其中包含一系列我们可以使用的模式和技术,以增加我们从 LLM 获得所需输出的可能性。

什么是提示?

在本书中,我们将经常使用术语提示,因为这将是我们与 LLM 进行通信的主要方式。当我们使用术语提示时,我们只是指发送给 LLM 的自然语言输入。例如,在本章的第一个示例中,我发送了以下提示:

Hello ChatGPT, can you introduce yourself briefly?

这促使 LLM 向我返回了一个响应。

在接下来的章节中,我们将大量使用提示工程来触发 LLM 为各种测试活动提供一系列有用的内容。但在我们开始之前,值得学习提示工程的基础知识,以便我们可以看到如何构建提示以最大化 LLM 的输出。

为了帮助我们更好地理解什么是提示工程,考虑发送给 LLM 的这两个提示。第一个是一个普通的问题:

What risks should I test for when testing a Contact Us form?

第二个是一个更详细的提示:

Assume the role of a professional software tester and suggest a list of risks for a Contact Us form using the following constraints:

# Focus on the quality characteristics of accessibility, compliance and usability
# Number each risk
# Provide which quality characteristic the risk relates to
# Use no more than 30 words to describe each risk

For example:

1\. Semantic structure - Accessibility  - The HTML structure of the form should follow WCAG A standards

比较两者,我们可以看到第二个示例更加详细,具有明确的请求和示例来概述我们可能期望 LLM 返回的内容。尽管意图相似,但每个的输出却截然不同。

比较第一个示例的输出:

Validation and Error Handling: Verify that the form correctly validates user input, such as email addresses, phone numbers, and required fields. Test for appropriate error messages when users submit incorrect or incomplete information.

Security: Ensure that the contact form is secure and protected against common vulnerabilities such as cross-site scripting (XSS) and SQL injection attacks. Validate that the form sanitizes or escapes user input to prevent malicious code execution.

Usability: Test the form's usability by evaluating factors such as the clarity of form fields, user instructions, and overall user experience. Make sure the form is intuitive, easy to navigate, and visually appealing.

对第二个示例进行对比:

1\. Keyboard accessibility - Accessibility - Ensure the form can be easily navigated and filled out using only a keyboard.

...

6\. Consistent design - Usability - Verify that the form follows a consistent design pattern and matches the overall website design.

...

13\. Consent checkboxes - Compliance - Ensure any consent checkboxes or privacy policy links are clearly presented and function correctly.

想象一下,我们想要使用每个示例中提出的风险来指导我们的测试。第一个示例输出的建议模糊而抽象。我们仍然需要做大量工作来分解大主题,例如安全风险,而对于第二个示例,我们有具体的可操作风险,可以轻松使用。而使用 LLM 等工具的目标是减少工作量,而不是增加。

我们的第二个提示产生了更好的结果,因为它给出的指示经过了考虑,而且是详细和清晰的,这正是提示工程的核心所在。尽管两个提示都使用自然语言,但是在提示工程中,我们清楚地了解了 LLM 的工作原理以及我们希望它返回什么,从而仔细思考如何编写提示以最大程度地实现预期结果。在使用提示工程时,我们意识到,虽然 LLM 以简单的语言进行沟通,但它处理我们的请求的方式与人类可能不同,这意味着我们可以采用特定的技术来引导 LLM 朝着我们希望的方向发展。

2.4 探究提示工程的原则

随着 LLM 的发展,提示工程的模式和技术也在不断发展。许多课程和博客文章围绕提示工程编写,但是一个值得注意的原则集合是由 Isa Fulford 和 Andrew Ng 及其各自的团队创建的,我们稍后将进行探讨。OpenAI 的 LLM 知识与 Deeplearning.ai 的教学平台的合作创建了一个名为 ChatGPT Prompt Engineering for Developers 的课程,其中包含一系列可以用于提示中以充分利用 LLM 的原则和策略。如果你有时间,我建议你参加www.deeplearning.ai/short-courses/chatgpt-prompt-engineering-for-developers的短期课程。(www.promptingguide.ai/也是一个有用的参考。)尽管该课程特指 ChatGPT,但其中教授的原则可以应用于许多 LLM。所以让我们探究这些原则和策略,以便熟悉提示 LLM。

2.4.1 原则 1:编写清晰明确的指示

乍一看,这个第一个原则似乎很明显——为他人提供清晰明确的指示总是明智的。但这个原则实际上建议我们要编写针对 LLM 清晰明确的提示。这将意味着与对人类清晰明确的提示有所不同。为了实现这个概念,Fulford 和 Ng 教授了四种策略来实现清晰明确的提示:使用分隔符,要求结构化输出,检查假设以及少次提示。在接下来的几节中,让我们更详细地研究每一种策略。

2.4.2 策略 1:使用分隔符

在撰写提示时,我们可能需要提供不同的内容和数据来满足不同的目的。例如,我们的提示开头可能包括关于我们希望 LLM 产生什么的说明,而提示的最后可能包括我们希望处理的原始数据。LLMs 能够猜测我们对提示不同部分的意图,但由于我们的目标是尽可能清晰明了,我们可以通过使用分隔符来帮助这个过程,分隔符是用于分隔字符串的字符,以在提示的不同部分表明我们的意图。以这个提示为例:

#A

You are a SQL data generator. Generate five rows of SQL for a MySQL database. 

#B

* The table name is identified with a # sign. 
* Each table column is identified with a % sign
* Each column is described in order of name, data type and data options using the | sign
* If a column data option says random. Randomize data based on the suggested format and column name

#C

Here are the instructions:

# rooms
% room_name | string | random
% type | string |  'single' or 'double'
% beds | integer | 1 to 6
% accessible | boolean | true or false
% image | string | random url
% description | string | random max 20 characters
% features | array[string] | 'Wifi', 'TV' or 'Safe'
% roomPrice |  integer | 100 to 200

运行此提示在 ChatGPT 中,我收到以下输出:

INSERT INTO rooms (room_name, type, beds, accessible, image, description, features, roomPrice) VALUES
    ('Room 1', 'single', 3, true, 'https://example.com/image1.jpg', 'Lorem ipsum dolor', '["Wifi", "TV"]', 150),
    ('Room 2', 'double', 2, false, 'https://example.com/image2.jpg', 'Lorem ipsum dolor', '["TV", "Safe"]', 180),
    ('Room 3', 'double', 4, true, 'https://example.com/image3.jpg', 'Lorem ipsum dolor', '["Wifi", "Safe"]', 160),
    ('Room 4', 'single', 1, true, 'https://example.com/image4.jpg', 'Lorem ipsum dolor', '["Wifi", "TV"]', 120),
    ('Room 5', 'double', 3, false, 'https://example.com/image5.jpg', 'Lorem ipsum dolor', '["TV"]', 170);

如我们所见,通过使用分隔符,正确的表名rooms已经被添加,并且列名、格式和范围已经被正确地随机化。这是通过在提示中通过各种分隔符来明确设定我们所设定的期望和规则而实现的。

分隔符有助于使提示更清晰,但也使其易于修改。例如,如果我们想重新配置生成的数据,我们可以输入另一行,如:

% room_size | string | 20m2 to 50m2

因此,在撰写包含大量随上下文变化的数据的提示时,我们可以使用分隔符清楚地表示在特定点的提示中提供了什么。

活动

使用分隔符提示示例,创建新的指令-这次是针对包括预订人、联系方式、入住和退房日期信息的预订。

2.4.3 策略 2:要求结构化输出

LLM 的一个有用的方面是它们具有以结构化格式提供输出的能力。在阅读本书时,我们将探索这种能力的有用性,但是作为一条规则,我们必须始终记住,我们需要在提示中清楚地说明我们想要看到使用的结构化格式。以这个提示为例:

Create a JSON object with random data that contains the following fields: firstname, lastname, totalprice, deposit paid. Also, include an object called booking dates that contains checkin and checkout dates.

我们首先明确说明我们想要以 JSON 格式查看我们的对象,然后开始概述对象的结构。当我将此提示发送给 ChatGPT 时,返回了以下结果:

{
  "firstname": "John",
  "lastname": "Doe",
  "totalprice": 150.50,
  "deposit_paid": true,
  "booking_dates": {
    "checkin": "2023-08-01",
    "checkout": "2023-08-10"
  }
}

如提示中明确说明的,预期格式和结构是两个不同的指令,这意味着我们可以通过发送附加的提示来修改指令,从而快速更改格式结构,例如:

Create this in a YAML format

将此提示发送给 ChatGPT 返回以下输出:

firstname: John
lastname: Doe
totalprice: 150.50
deposit_paid: true
booking_dates:
  checkin: 2023-08-01
  checkout: 2023-08-10

注意结构是相同的(以及随机化的数据)。通过明确说明我们想要的格式,我们可以指示 LLM 在特定时间以及轻松地交替格式。

活动

尝试创建一个包含不同数据类型和不同格式的多个参数的对象。尝试提示一个 LLM 将你的对象从一种格式转换为另一种格式,例如从 JSON 转换为 XML。

2.4.4 策略 3:检查假设

正如我们之前学到的那样,LLM 可能会产生幻觉,生成包含不正确或脱离现实的信息的输出。不用说,我们希望减少产生幻觉的风险,这就是我们的第三种策略,检查假设,可以帮助的地方。如果向 LLM 提供了围绕我们想要解决的问题的边缘情况的提示,那么 LLM 更有可能产生幻觉。如果 LLM 没有得到适当的指示,它更有可能对答案进行猜测,而不是直接告诉我们它无法提供有用的答案。因此,如果我们想避免猜测,我们需要在提示中提供指示,让 LLM 在无法执行我们的请求时退出。考虑这个例子:

#A
You will be provided with a JSON object delimited by three hashes. Extract all emails that end with .com and write them out as a list.

#B
If no email addresses with a .com email address exist, simply write "No .com emails found"

#C
###
[{
  "firstname": "Bret",
  "lastname": "Averay",
  "email": "baveray0@apple.com"
}, {
  "firstname": "Annabel",
  "lastname": "Biswell",
  "email": "abiswell2@nsw.gov.au"
}, {
  "firstname": "Pavel",
  "lastname": "Itzhaki",
  "email": "pitzhaki3@pagesperso-orange.fr"
}, {
  "firstname": "Pail",
  "lastname": "Yandell",
  "email": "pyandell4@ning.com"
}, {
  "firstname": "Glennis",
  "lastname": "Pentecost",
  "email": "gpentecost6@yelp.com"
}]
###

使用可以提取的电子邮件地址集运行此提示返回以下输出:

Here are the email addresses that end with ".com":

* baveray0@apple.com
* pyandell4@ning.com
* gpentecost6@yelp.com

但是当我再次运行提示而不包含电子邮件地址时会发生什么:

You will be provided with a JSON object delimited by three hashes. Extract all emails that end with .com and write them out as a list.

If no email addresses with a .com email address exist, simply write "No .com emails found"

###
[{
  "firstname": "Bret",
  "lastname": "Averay"
}, {
  "firstname": "Annabel",
  "lastname": "Biswell"
}, {
  "firstname": "Pavel",
  "lastname": "Itzhaki"
}, {
  "firstname": "Pail",
  "lastname": "Yandell"
}, {
  "firstname": "Glennis",
  "lastname": "Pentecost"
}]
###

然后我收到了以下内容:

Here is the list of email addresses that end with .com extracted from the provided JSON object:

No .com emails found

LLM 的这个响应直接参考了提示的这一部分:

If no email addresses with a .com email address exist, simply write "No .com emails found

这阻止了 LLM 提供不正确输出。例如,当我没有进行假设检查运行提示时,返回以下内容:

Sure! I can help you with that. Here are the email addresses that end with ".com" from the provided JSON object:

`
["bret@example.com", "annabel@example.com", "pavel@example.com", "pail@example.com", "glennis@example.com"]
`

Please note that I have used example.com as a placeholder domain for all the email addresses. Replace it with the appropriate domain if you have the actual email addresses.

这是 LLM 幻觉的一个很好的例子。我们提供的对象不包含电子邮件地址,因此 LLM 使用猜测,并基于现有数据错误地开始生成新的电子邮件地址。然而,有了假设检查,我们防止了幻觉的发生。

活动

创建一个过滤掉原始数据列表中特定数据并输出结果的提示(例如,在不同食物列表中过滤水果)。然后修改数据以包含一个边缘情况(例如,不正确或缺失的数据)。观察 LLM 输出的内容,然后尝试通过向提示添加跟随检查假设提示的指令来更正输出。

2.4.5 策略 4:Few-shot prompting

随着我们在本章中的进展和对 LLM 和提示的新概念的学习,通过示例澄清了关键点。它们是不仅仅是教学,而且是相互交流的重要工具。对于 LLM 来说也是如此。 Few-shot prompting 基本上意味着提供明确的示例来澄清说明(在这种情况下,few 这个词指示你分享了多少示例。一个没有示例的提示将是 zero-shot prompt)。看一个使用示例的提示的例子:

#A
You are an expert exploratory tester. Create three test charters for a booking system that focus on the risks around booking dates. The format should follow: Explore <Target> using <Resource> to discover <Information>

#B
For example:
* Explore user permissions using different users to discover permission issues
* Explore browser animations using different devices to discover how animations render

将此提示发送给 ChatGPT 返回以下输出:

Explore date validation using various date formats to discover potential input errors.

Explore time zone handling using different geographical locations to discover discrepancies in booking times.

Explore booking conflicts using overlapping reservation requests to discover potential double-booking issues.

考虑格式使用<Resource>探索<Target>以发现<Information>。它是基于规则的,但它在使用自然语言来传达规则时有很大倾向(不像依赖显式分隔符规则的 JSON 或 XML 结构)。通过在我们的提示中提供示例,我们可以帮助界定我们所指的<Target><Resource><Information>,并减少 LLM 猜测它们含义的风险。

活动

创建一个使用 few-shot 策略的提示。在您的提示中,提供您期望 LLM 遵循的说明,然后至少添加两个示例以帮助指导它提供所需的结果。

2.4.6 原则 2:给模型一些“思考”的时间

考虑到我们一直将 LLMs 视为概率机器而不是可以思考的实体,看到一个鼓励我们给模型一些“思考”时间的原则似乎有点不寻常。毕竟,使用 LLMs 的价值在于它们能够比我们人类更快地回答复杂问题吗?

然而,原则 2 的重点不在于 LLM 如何思考,而在于我们如何向概率引擎呈现复杂任务。在稍早提到的针对开发者的 ChatGPT 提示工程课程中,富尔福德在介绍这个原则时给出了一个有用的类比:

“如果你给一个模型一个在短时间内无法完成的任务……它可能会做出一个可能是错误的猜测。”

她解释说,如果人类被要求在有限的时间内完成复杂任务,同样的情况也会发生。我们将依赖于有根据的猜测,并且可能得出一个不太满意的答案。因此,原则 2 提供了策略来帮助我们编写提示,将任务拆分,并鼓励 LLMs 评估输出,以再次最大化产生所需响应的可能性。

2.4.7 策略 1:指定完成任务的步骤

第一种策略相对直接,一旦我们对原则 1(编写清晰明确的说明)的策略有信心,就可以轻松掌握。通过使用分隔符,我们可以将复杂的任务分解为单个步骤,供 LLM 执行以解决更大的任务。看看这个提示:

#A
You are going to be given a list of instructions to follow.

#B
1 - Identify functional risks that might impact the text delimited by three hashes
2 - Convert the risks into test charters
3 - Format each charter into a Discover <feature> using <resource> to discover <information> format

#C
###
As a user
I want to be able to calculate my tax
So I know what tax I have to pay
###

当发送给 ChatGPT 时,它返回了这个结果:

Functional risks:

    Calculation accuracy: There is a risk that the tax calculation may be inaccurate, leading to incorrect tax amounts being calculated.

Test charters based on the identified risks:

    Test Charter 1: Validate the accuracy of tax calculations by inputting different test scenarios with known tax amounts and verifying that the calculated tax matches the expected values.

Formatting the charters into a "Discover <feature> using <resource> to discover <information>" format:

    Discover tax calculation accuracy using test scenarios with known tax amounts to validate the accuracy of the calculated tax.

这是我收到的输出片段。它创建了许多风险和章程,但每个部分的第一个示例都演示了 LLM 逐个回应每个子任务以创建输出。

活动

创建一个需要执行复杂任务的提示。尝试将复杂任务拆分为 LLM 可以执行的多个子任务。

2.4.8 策略 2:指导模型先解决自己的方案

我们最后的策略不是关注创建输出的过程,而是关注评估输出本身。类似于检查假设,要求 LLM 评估输出以确认其与其被指示产生的内容是否一致。让我们看一个示例,以更好地理解这将如何运作。首先,让我们看一个不要求 LLM 首先解决其解决方案的提示:

#A
You are a software developer in test that is experienced in writing Java. Create a unit test for the following method:

#B
public class AuthService {

   public HttpStatus deleteToken(Token token) throws SQLException {
        Boolean successfulDeletion = authDB.deleteToken(token);

        if(successfulDeletion){
            return HttpStatus.OK;
        } else {
            return HttpStatus.NOT_FOUND;
        }
    }

}

将此发送到 ChatGPT,返回以下结果:

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

class TokenManagerTest {

    @Test
    void deleteToken_SuccessfulDeletion_ReturnsOK() throws SQLException {
        // Arrange
        TokenManager tokenManager = new TokenManager();
        Token token = new Token();

        // Act
        HttpStatus result = tokenManager.deleteToken(token);

        // Assert
        assertEquals(HttpStatus.OK, result);
    }
}

这看起来像一个合理的 Java 代码用于单元测试,但如果这被添加到一组单元检查中,它会失败。这会失败,因为在提供的生产代码中,方法authDB.deleteToken没有被正确处理。具体来说,如果我们希望此单元检查工作,我们需要模拟authDB.deleteToken

现在,如果我们再次运行提示,但这次在输出最终答案之前让它评估其解决方案,我们会得到不同的结果。所以首先我们将提示更改为以下内容:

#A
You are software developer in test that is experienced in writing Java. Create a unit test for the following method that will be shared. Before outputting the unit tests, check to see that the assertion of each unit check will return a deterministic result.

#B
public class AuthService {

   public HttpStatus deleteToken(Token token) throws SQLException {
        Boolean successfulDeletion = authDB.deleteToken(token);

        if(successfulDeletion){
            return HttpStatus.OK;
        } else {
            return HttpStatus.NOT_FOUND;
        }
    }

}

将此发送到 ChatGPT 返回以下结果:

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

import java.sql.SQLException;

public class TokenDeletionTest {

    @Mock
    private AuthDB authDB;

    @Test
    public void testDeleteToken_SuccessfulDeletion() throws SQLException {
        // Mocking the behavior of authDB.deleteToken() to return true
        Token token = new Token();
        Mockito.when(authDB.deleteToken(token)).thenReturn(true);

        AuthService authService = new AuthService(authDB);
        HttpStatus result = authService.deleteToken(token);

        // Assertion: The method should return HttpStatus.OK
        Assertions.assertEquals(HttpStatus.OK, result);
    }
}

这一次,我们可以看到因为我们要求 LLM 在返回结果之前评估其解决方案,输出的单元检查使用了Mockito来模拟authDB.deleteToken。因此,如果我们观察到 LLMs 输出错误的解决方案或它们开始产生幻觉,我们可以添加一条指令首先评估解决方案,以最小化幻觉的发生。

活动

创建一个提示,需要 LLM 解决问题的解决方案。观察其输出,并查看所产生的解决方案是否正确。然后添加指令,让 LLM 评估解决方案。会发生什么?解决方案会改变吗?是不是有所改善?

2.5 使用不同的 LLM

到目前为止,我们在广义上讨论了 LLMs,同时在之前的示例中使用 OpenAI 的 ChatGPT 来演示它们的工作原理。然而,ChatGPT 只是我们可以使用的众多不同 LLMs 中的一个。因此,在我们结束本章之前,让我们熟悉 LLMs 之间的差异,并了解一些当前流行的模型和社区,以增加我们找到适合工作的正确 LLM 的机会。

2.5.1 比较大型语言模型

什么使得一个 LLM 优秀?我们如何确定一个模型是否值得使用?这些都不是容易回答的问题。LLM 的复杂性质以及它们的训练方式和使用的数据使得这些系统难以进行深入分析,并构成了一些研究人员试图改进或阐明的领域。然而,这并不意味着我们不应该了解 LLM 的一些关键方面以及它们如何影响它们。我们可能并不都是尝试探索 LLM 深层内部运作的人工智能研究人员,但我们是或将成为它们的用户,我们会想知道我们投入资源的东西是否给了我们价值。因此,为了帮助我们分解一些术语并为我们提供一些关于 LLM 如何相互区别的基础知识,让我们来看看 LLM 领域讨论的一些关键属性:

参数数量

看看不同的 LLM,你可能会看到关于 LLM 拥有“1750 亿”或“1 万亿”参数数量的说法。有时候这可能听起来像是营销说辞,但参数数量确实会影响 LLM 的表现。参数数量本质上与模型内部存在的统计权重量有关。每个单独的权重都提供了组成 LLM 的统计谜题的一部分。因此,粗略地说,LLM 拥有的参数越多,其性能就越好。参数数量也可以让我们了解成本。参数数量越高,运行成本就越高,这种成本可能在一定程度上转嫁给用户。

训练数据

LLM 需要大量的数据进行训练,因此数据的规模和质量对 LLM 的质量会产生影响。如果我们希望 LLM 在回应请求时准确无误,仅仅提供尽可能多的数据是不够的。需要的是能够以合理方式影响模型概率的数据。例如,我们在本章前面探讨过的 Reddit 示例,其中使用 subreddit r/counting 来训练 ChatGPT 导致它产生奇怪的幻觉,表明更多不一定就是更好。然而,与参数数量类似,LLM 训练过的高质量数据越多,其性能可能就越好。挑战在于了解 LLM 训练过的数据是什么——这是 AI 企业创造者们极力保守秘密的一件事情。

可扩展性和整合性

与任何其他工具一样,如果 LLM 能够提供除核心功能之外的其他功能,如整合到现有系统中或进一步为我们的特定需求训练模型,那么它的价值可以进一步提高。LLM 可以整合和扩展的功能主要取决于谁负责对它们进行训练。

例如,OpenAI 提供付费 API 访问其模型。但除了一项指令功能可以通过简单提示来调整输出外,没有能力进一步调整并部署其中一个他们的 GPT 模型供私人使用。相比之下,Meta 的 LlaMa 模型是开源的,允许 AI 社区下载并根据自己的要求进一步训练,尽管他们必须构建自己的基础设施来部署该模型。

随着 LLM 平台的发展,我们将看到它们不仅在回应提示方面取得进步,而且在周围的功能和访问方面也会有所提升。因此,在评估要使用的内容时,记住这些功能是必要的。

回应质量

有人认为,考虑 LLM 是否提供易读、有用和尽可能免费(或接近免费)幻觉的回复是最重要的因素之一。尽管参数数量和训练数据等标准是 LLM 性能的有用指标,但我们需要明确我们想要使用 LLM 做什么,然后确定每个 LLM 对我们的提示作出怎样的响应,并在解决我们具体问题时提供怎样的帮助。我们面临的挑战并不都需要市场上最大、最昂贵的 LLM。因此,我们要花时间尝试不同的模型,比较它们的输出,然后自行判断。例如,OpenAI 的 GPT 模型在处理代码示例方面表现更好,而 Google Bard 则不然。这些细节是通过实验和观察发现的。

我们探讨的这些标准绝不是一个穷尽列表,但再次表明,一旦我们克服了它们如何回应的初始魅力,就会发现关于 LLMs 还有更多要考虑的东西。不同的 LLM 以不同的方式执行,帮助我们解决不同的挑战。因此,让我们来看看当前可用的一些更受欢迎的模型和平台。

2.5.2 检查流行的大型语言模型

自从 OpenAI 推出 ChatGPT 以来,各种组织发布的大型语言模型(LLMs)数量激增。这并不是说在 ChatGPT 推出之前没有这些模型和相关工作,但公众关注度确实加强了,越来越多的营销和发布公告聚焦在公司发布的 LLM 产品上。以下是自 2022 年底以来发布的一些更常见/流行的 LLMs。

跟上 LLMs

值得注意的是,LLMs 的推出及其相关功能的情况非常复杂,发展速度相当快。因此,我们所探讨的一些内容可能与撰写时(2023 年中期)到你阅读本书时有所不同。然而,这个列表展示了 LLM 领域中一些值得探索的大型名字。

OpenAI

在撰写本文时,OpenAI 是提供 LLM 供使用的组织中最普遍的。虽然 OpenAI 已经在 LLM 模型上工作了相当长的时间,他们在 2020 年发布了他们的 GPT-3 模型,但是他们在 2022 年 11 月发布的 ChatGPT 才引发了广泛的兴趣和 LLM 的使用浪潮。

OpenAI 提供了一系列不同的 LLM 模型,但最突出的两个是 GPT-3.5-Turbo 和 GPT-4,您可以在他们的文档中了解更多信息:platform.openai.com/docs/models/overview。这两个模型被用作基础模型,或者说可以进一步针对特定目的进行训练的模型,用于一系列产品,如 ChatGPT、GitHub Copilot 和 Microsoft Bing AI。

除了他们的模型外,OpenAI 还提供了一系列功能,如直接访问他们的 GPT-3.5-Turbo 和 GPT-4 模型的 API,以及一系列与 ChatGPT 集成的应用程序(如果您订阅了他们的 plus 会员)。这是迄今为止最受欢迎的 LLM(目前为止)并已启动了一个与组织竞争发布他们自己的 LLM 的竞赛。

虽然我们已经尝试过一些与 ChatGPT 相关的提示,但您随时可以访问并尝试 ChatGPT,网址是chat.openai.com/

坚持使用 OpenAI

尽管有许多不同的大型语言模型,我鼓励您使用,但为了保持一致,我们将坚持使用 ChatGPT-3.5-Turbo。这不一定是目前最强大的 LLM,但它是最普遍的,而且是免费的。也就是说,如果您想尝试其他 LLM 模型的这些提示,可以随意尝试。但请注意,它们的响应可能与本书中分享的内容不同。

PaLM

一旦 OpenAI 发布了 ChatGPT,Google 很快就会发布他们自己的 LLM 与聊天服务,他们于 2023 年 3 月发布了。基于他们的 PaLM 2 LLM,一个拥有 5400 亿参数的模型,Google 试图与 ChatGPT 竞争,并提供了类似的基于聊天的体验,称为 Bard。

与 OpenAI 类似,Google 通过他们的 Google Cloud 平台提供了对 PaLM 2 的访问(网址为developers.generativeai.google/),并最近开始提供与 OpenAI 的 ChatGPT 应用类似的应用程序,同时还可以集成到其他 Google Suite 工具中,如 Google Drive 和 Gmail。

您可以访问并尝试 Bard,网址是bard.google.com/chat

LLaMa

LLaMa 是 Meta 在 2023 年 7 月首次发布的一个模型集合的名称。LLaMa 与 OpenAI 的 GPT 模型和谷歌的 PaLM 有所不同之处在于,LLaMa 是开源的。除了开源许可证外,LLaMa 还有多种规模:分别是 70 亿、13 亿和 7 亿个参数。这些规模和它们的可访问性的结合使得 LLaMa 已成为 AI 社区中一个受欢迎的基础模型。然而,这种可访问性的另一面是,Meta 并不提供公共平台来训练和运行 LLaMa 的版本。因此,必须个人获取数据集和基础设施才能使用它。

更多关于 LLaMa 的详细信息可以在以下链接中找到:

  • www.llama2.ai/

  • llama-2.ai/download/

Huggingface

与列表中的其他条目不同,Huggingface 并没有提供专有模型,而是促进了一个包含各种不同模型的 AI 社区,其中大多数模型都是开源的。查看他们的模型索引页面,位于 huggingface.co/models,我们可以看到来自不同公司和研究实验室的数十万个经过不同训练的模型。Huggingface 还提供了用于训练、应用程序和文档的数据集,使读者能够深入了解模型的构建方式。所有这些资源都可用于 AI 社区访问预训练模型,对其进行微调,并针对特定用途进行进一步训练,这是我们将在本书第三部分进一步探讨的内容。

在短时间内,LLMs 的市场规模在商业和开源方面都有了显著增长,与软件开发的其他领域类似,积极主动地了解新的 LLMs 出现可能是有益的。然而,尝试一直跟上一切可能是令人不知所措的,也不一定可行。因此,与其试图紧跟 AI 社区的一切动态,我们可以选择在想要使用 LLMs 解决特定问题时探索它们。拥有问题可以帮助我们构建关于哪些工具最适合我们以及哪些不适合的标准。

活动

从本章选择一个早期提示,或者创建一个你自己的提示,并将其提交给不同的 LLMs。注意每个 LLM 的响应和比较。有些是否感觉更像是对话?它们如何处理接收或发送代码示例?在你的观点中,哪个提供了最佳响应?

2.6 创建提示库

提示的一个好处是一旦创建,它们就可以被重复使用。因此,出现了很多在线共享的不同角色和任务的提示集合。例如,这里是最近我看到的一些集合:

  • 令人惊叹的 ChatGPT 提示,GitHub: github.com/f/awesome-chatgpt-prompts

  • 50 ChatGPT Developers 提示,Dev.to: dev.to/mursalfk/50-chatgpt-prompts-for-developers-4bp6

  • ChatGPT Cheat Sheet, Hackr.io: hackr.io/blog/chatgpt-cheat-sheet-for-developer

此列表并不全面,示例集合也不一定与测试相关,但是浏览这些示例集合是值得的,以了解其他人是如何创建提示的,同时也给我们一个机会来确定哪些提示实际上会有效,哪些不会。

尽管公开分享的提示集可能很有用,但我们很可能最终会创建用于特定情境的提示。所以,养成把证明有用的提示存储在某种仓库中的习惯是值得的,这样我们和其他人就可以快速使用它们。你存储它们的位置将取决于它们用于什么以及谁来使用它们。如果它们是用于公共使用,那么分享提示仓库或添加到现有集合可能会很有价值。如果我们在开发公司产品时创建和使用它们,那么我们需要像对待生产代码一样对待它们,并将它们存储在某个私密位置,以免违反有关知识产权的任何政策。最后,我们还可以考虑版本控制,这样我们就可以在学习更多关于与 LLMs 合作以及 LLMs 自身发展的过程中调整和跟踪提示。

无论它们存储在何处,想法都是创建一个可以快速轻松访问的提示仓库,以便一旦为特定活动创建了提示,它就可以被多次快速重复使用,以便我们可以从中获得尽可能多的价值来提高我们的生产力。

活动

创建一个空间,您和您的团队可以存储未来的提示以供使用。

使用本书中的提示

为了存储未来使用的提示并帮助您,读者,在尝试本书中的提示示例时,您可以在以下 GitHub 仓库中找到每个提示示例:

github.com/mwinteringham/llm-prompts-for-testing

这将使您能够在我们完成每章时快速复制并粘贴提示到您选择的 LLM 中。这样,您就不必手动键入整个提示。在某些提示的部分中,您将需要添加您自己的自定义内容或上下文来使用它们。为了使它们清晰,对于需要添加到提示中的内容的说明将在提示中找到,并将以全大写字母和方括号内的形式进行格式化。

2.7 通过提示解决问题

本章学到的策略和工具能够帮助我们框架使用 LLM 并为特定的测试活动设计特定的提示。然而,我们要注意,尽管这些策略能够提高我们获得所需结果的几率,但它们并不是百分之百可靠的。例如,当我们要求 LLM 评估其输出时,LLM 实际上并不像传统应用程序那样评估其输出。它只是将预测的指针移向与我们需求相符的输出。

因此,我们有必要培养写提示的技能,以便在解决问题时既能有效,又不会消耗使用 LLM 带来的时间(例如,我们不想花几小时调整提示)。

单个提示与多个提示的对比

在本章中,我们探索了如何使用原则和策略创建个别提示,以尽可能有效地从 LLM 中获得期望的输出。然而,像 ChatGPT、Bard 和 Claude 这样的工具允许我们与 LLM 进行“对话”,其中对话的历史会影响未来回复的输出。这就引出了一个问题,是否在对话中尝试多个提示以调整输出会更容易?虽然这可能有效,但我们也面临着一个风险,即随着对话的进行,幻觉发生的风险越高,因为 LLM 试图过度适应我们的请求。这就是为什么像 BingAI 这样的工具在给定对话中给出回复的数量是有限的原因。然而,更重要的是,多并不意味着更好。垃圾进,垃圾出的规则适用于单个和多个提示。在一次对话中依赖多个提示意味着我们在请求方面变得更加模糊和不精确,这会增加延迟并增加幻觉的发生,从而抵消了使用 LLM 的价值。总之,无论我们是想发送一个单独的提示来获取我们想要的结果,还是发送多个提示,采用 Isa Fulford 和 Andrew Ng 所创建的原则和策略将提高我们使用 LLM 的生产力。

这意味着我们需要能够确定 LLM 能够帮助解决的特定问题,并通过提示工程的方式最大化从 LLM 中提取有价值信息的机会。这将是我们在本书剩余部分要探索的内容:何时以及如何使用 LLM。

随着我们的进展,我们还将学到提示有各种各样的形式和大小。在本章中,我们看了人工编写的提示。但是,我们会了解到,像 GitHub Copilot 这样的工具会在我们编写代码时自动生成提示。这并不意味着我们不能将原则和策略融入到我们的工作方式中,但这需要时间、意识和实践来培养。

活动

在继续阅读本书并了解不同类型的提示用于不同的测试活动之前,请使用第一章和第二章的知识,并考虑你所做的特定测试任务,并尝试构建一个提示,可以帮助你的工作。

2.8 总结

  • LLMs 使用复杂的算法对大量数据进行训练,以分析我们的请求并预测输出。

  • LLMs 的预测性质使它们非常适应,但也意味着它们存在一些风险。

  • LLMs 有时会输出幻觉,或者听起来权威和正确的文本,实际上完全是错误的。

  • LLMs 所训练的数据可能存在错误、空白和假设,我们在使用它们时必须注意这一点。

  • 我们也要注意与 LLMs 分享数据,以免造成未经授权的商业或用户信息泄漏。

  • 提示工程是我们可以使用的一系列原则和策略,以最大化 LLM 返回期望输出的机会。

  • 我们可以利用 LLMs 具有预测性质的知识,并通过提示工程来利用它。

  • 使用界定符可以帮助我们在提示中澄清指示和参数。

  • LLM 可以在各种格式中输出数据,但需要我们明确指出我们在提示中需要哪种结构格式。

  • 我们可以通过使用检查假设的策略来减少 LLMs 的幻觉。

  • 在提示中提供例子可以帮助确保 LLM 以期望的格式或上下文提供输出。

  • 在提示中指定特定的子任务可以帮助 LLM 成功处理复杂的任务。

  • 要求 LLMs 评估问题的解决方案也可以减少错误并最大化成果。

  • 知道何时使用 LLMs 并发展提示工程技能是成功的关键,无论我们使用什么工具。

第三章:AI,自动化和测试

本章包括

  • 概述良好测试的价值

  • 欣赏工具在测试中的帮助

  • 确定何时可以使用 AI 工具进行测试

在我们开始探索在测试中使用 LLMs 之前,我们需要稍微跳出来回答以下问题:

  • 测试的目的和价值是什么?

  • 工具如何帮助我们实现这个目标?

  • 何时适用于使用 AI 工具?

对于有些人来说,提出这些基本问题可能感觉是一种不必要的活动。但是,如果你是那种仅将测试视为一种确认性的练习的人,比如执行测试用例以确认需求是否正确,那么你对后续章节的理解可能会有限。了解测试的价值和性能对于确定如何有效地使用工具至关重要。因此,在本章中,我们将探讨为什么对测试有更深入的理解可以帮助我们有效地利用工具。也就是说,如果你已经对测试有了深入的了解,可以跳过本章继续阅读。对于我们其他人来说,让我们回到最初的问题,为什么要测试?

3.1 测试的价值

为了帮助我们充分认识到为什么我们需要软件开发中的测试的重要基础,让我们回到常见的观点,即测试是一种确认性的练习。这意味着测试被视为一种用于确认以下条件的操作:

  • 符合书面需求。

  • 所有关键路径在系统中都已覆盖。

  • 系统没有漏洞。

持有这种观点的团队往往过度依赖包含明确指令的测试用例/脚本,这些指令是为了让人或机器按照要求执行并确认预期结果是否已经满足。这种心态和方法的问题并不在于使用测试脚本,而是仅仅使用测试脚本而没有其他。过度依赖测试脚本会带来许多偏见,但是如果我们将其回归到测试中使用 LLMs 的情况,那么它会限制我们对这些工具如何帮助我们的认识。当像 ChatGPT 这样的工具日渐流行时,围绕在测试中使用 LLMs 的演示和讨论主要集中在一个方面:测试脚本。人们会演示 LLMs 如何生成测试脚本,这些脚本可以由人或测试自动化工具手动执行。

虽然最初这些可能有些用途,但是关于如何利用 LLMs 在测试中做其他事情的选择开始减少。乍一看,这似乎是工具的局限性,但实际上,真正的问题是对测试的限定概念以及其如何帮助。所以,如果我们要扩大在测试中使用 LLMs 的范围,我们首先必须扩展对测试的理解和工作原理。

3.1.1 关于测试的不同思考方式

为了帮助我们建立更深入的理解,让我们探讨我用来定义我认为测试是为了什么以及它如何帮助的测试模型,如图 3.1 所示。

图 3.1 测试模型,用于描述测试的价值和目的

这个模型是基于詹姆斯·林赛在他的论文“探索与战略”中创建的一个模型www.workroom-productions.com/why-exploration-has-a-place-in-any-strategy,由两个圆组成。左圈代表想象力,或者我们在产品中想要的东西,右圈代表实现,或者我们在产品中拥有的东西。测试的目的是通过执行测试活动尽可能多地了解这两个圈中发生的事情。我们在这两个圈中进行的测试越多,我们学到的东西就越多。然后我们可以:

  1. 发现可能影响质量的潜在问题。

  2. 重叠这两个信息圈,确保我们理解并有信心我们正在构建我们想要构建的产品或服务。

为了更详细地描述这一点,让我们看一个例子,团队正在交付一个我们希望确保质量达到高水平的假想搜索功能。

想象力

想象力圈代表我们从产品中想要的东西,其中包括明确和隐含的期望。因此,在这个圈子里,我们的测试集中在尽可能多地了解这些明确和隐含的期望上。通过这样做,我们不仅了解了书面或口头共享的明确要求,而且深入挖掘细节,并消除了术语和想法的模糊性。假设业务代表或用户,如产品负责人,已将此要求与他们的团队共享:

'Search results are to be ordered by relevance.'

这里共享的明确信息告诉我们,产品所有者希望按相关性排序的搜索结果。然而,可以通过测试所要求的想法和概念来发现许多隐含信息。这可能以我们可以提出的一系列问题的形式出现,例如:

  • “相关结果”是什么意思?

  • 与谁相关?

  • 共享哪些信息?

  • 我们如何按相关性排序?

  • 我们应该使用什么数据?

通过提出这些问题,我们形成了更全面的图片,消除了团队思维中的任何误解,并识别了可能影响这些期望的潜在风险。如果我们更多地了解我们被要求构建的内容,那么我们第一次就更有可能构建正确的东西。

实现

通过测试想象力,我们更清楚地了解被要求构建的东西。但是,仅仅因为我们可能知道要构建什么并不意味着我们最终会得到符合这些期望的产品。这就是为什么我们还要测试实现来了解:

  • 产品是否符合我们的期望

  • 产品可能不符合我们的期望的方式

这两个目标的重要性相等。我们希望确保我们构建的是正确的东西,但总会有一些副作用,比如意外行为、漏洞、未达预期的情况以及产品中可能出现的奇怪现象。以我们的搜索结果示例为例,我们不仅可以测试该功能按相关顺序提供结果,还可以问产品以下问题:

  • 如果我使用不同的搜索词会怎样?

  • 如果相关结果与其他搜索工具的行为不匹配怎么办?

  • 如果我搜索时部分服务停止了怎么办?

  • 如果我在 5 秒内请求 1000 次结果怎么办?

  • 如果没有结果会怎样?

通过超越我们的期望进行探索,我们更加了解我们产品内发生的情况,不管是好事还是坏事。这确保我们不会对我们产品的行为产生错误的假设,并发布一个质量差的产品。这也意味着如果我们发现了意外行为,我们可以选择尝试去除或调整我们的期望。

3.1.2 测试的整体方法

想象和实现测试模型表明,测试超越了简单确认期望的程度,提出了一个更全面的测试方法。通过实施专注于想象和实现空间的不同活动,我们通过执行测试学到了更多关于我们想要构建和已经构建的内容。我们在这两个领域学到的越多,它们之间的一致性就越高。而一致性越高,我们对质量的感知就越准确。

一个对自己的工作有充分了解的团队更清楚他们产品的质量。我们也就更有能力决定采取什么步骤来改进质量。这使我们能够决定将注意力集中在特定的风险上,对产品进行更改以与用户的期望相一致,或者确定我们想要花时间来修复哪些问题以及哪些问题我们打算放弃。这就是好的测试的价值,帮助团队做出这些明智的决策,并对他们所采取的开发高质量产品的步骤感到自信。

为了更好地理解这个模型,让我们考虑一个需要测试的示例背景。以我们的示例为例,我们负责交付一个快餐订购系统。用户登录,找到他们想要订餐的餐馆,下订单(订单将发送给餐馆),然后在产品内跟踪订单的送货情况。像这样的产品需要易于使用、高度可用和安全。因此,为了交付高质量的产品,我们可能需要利用不同的测试活动来关注不同类型的风险,如图 3.2 所示。

图 3.2 想象和实现模型,并显示其中的示例活动

在这个模型中,我们可以看到一系列不同的活动,它们被放置在特定的区域,因为它们专注于特定类型的风险。例如,在想象力方面,我们可能会关注影响网站可用性的风险。因此,我们采用侧重于用户体验测试和协作设计的测试活动。在实现方面,我们希望确保产品稳定,并尽量减少可能影响产品可用性和使用性的风险。因此,我们采用探索性测试和性能测试等活动。最后,请注意,在重叠的区域中,我们添加了测试脚本。这对我们很有用,因为它们减轻了产品在其随着时间的推移而变化和变形时出现意外变化的风险,这表明测试脚本和自动化检查是有用的,但只是整体的一部分。

每一个活动都有不同的实现方式、不同的挑战以及工具能够帮助的不同方式。但是如果我们没有对测试是信息收集和知识分享的一种练习有所了解,我们就无法轻松地识别这些用法。有了这个模型,以及对我们的工作面临的许多不同风险以及帮助我们减轻这些风险的测试活动的认识,我们可以开始更深入地了解工具在测试中的作用。

3.2 工具如何帮助测试

你可能会听到一个测试人员说(或者甚至你自己曾经说过),没有足够的时间来测试所有的东西。这本书中可能会重复几次。团队总是受到时间、预算、会议、人员配置和其他因素的限制,因此,为了实施和执行有效的现代化测试,我们必须依靠工具来帮助我们。工具对于测试至关重要,但它们也带来了我们下一个关于测试的误解——即工具或机器能够像人一样测试。

3.2.1 自动化偏见

为了了解机器和人类在测试方面的不同之处,让我们考虑一个例子,其中两者都用于测试网站上的一个功能。该功能是电子商务网站的全宽横幅,带有一个图片和一些突出当日优惠的文字。最初,我们以手动方式或人为导向的方式测试它,并观察到该功能正常工作:图片显示出来,与之相关的所有文本都是正确的。然后,我们决定使用工具来自动化这个测试。我们编写代码,将打开浏览器并断言元素 A 存在,元素 A 是加载当日优惠的位置。我们运行自动化测试,它通过了。然后有一天,在另一个成功的发布之后,我们所有的自动化测试都通过了,一个最终用户提出了一个 bug,并告诉我们他们看不到当日优惠。他们只看到页面顶部的一个空白白色框。

发生了什么事?在创建自动化测试的过程中,我们已经转移了我们的知识,这些知识是根据心理启发和预言建立起来的隐含信息。我们已经将我们对特性的复杂理解简化为一个指令:网页上应该存在元素 A。所以当我们的产品的最新版本出现了无法检索每日特惠的故障功能,或者 CSS 不正确或损坏时,自动化测试仍然通过,因为元素 A 仍然存在。然而,人类只需要几秒钟就可以看出问题出在哪。

这个故事的寓意并不是工具是坏的或不必要的,而是它们经常被误用或被解释不正确。这种行为是自动化偏见,潜入了我们对工具价值的看法,即我们为工具的输出赋予的价值往往超过了它向我们传达的内容。也就是说,当我们设计我们的自动化测试来寻找元素 A 时,我们假设我们人类所做的只是寻找元素 A。但即使我们没有有意识地这么做,我们还考虑了许多其他因素。

如果我们陷入自动化偏见,我们就会冒险选择和实施我们认为可以像人类一样揭示和报告信息的工具——而实际上它们不会,这会导致我们对所提供的产品产生错误的过分自信,或者产生一种使工具模仿人类行为的工作量水平,这对于一个现代项目是不可持续的。工具不能取代测试活动,而认同这种观点最终会导致质量问题,并增加项目的风险。因此,我们必须更多地转向如何利用工具来支持我们的测试。

3.2.2 选择使用工具

成功的工具使用源自于我们在解决问题时对问题进行了一些思考,以及潜在可以帮助的工具。为了更好地理解这一点,让我们回到我们的每日特惠功能,并更仔细地看一看当人们测试这样的功能时在做什么。

首先,我们考虑不同的测试功能的方法。我们利用我们对所述功能的当前理解来形成测试想法,并选择首先要测试的内容。接下来,我们需要设置我们的测试。这可能包括设置环境或创建/更新必要的测试数据。(我们可能需要创建一个每日特惠以供观察,以及测试用户以进行和查看交易。) 当一切都准备好了,我们就需要执行我们的测试,加载浏览器,或者可能是多个浏览器,以验证交易正确地呈现出来。然后,一旦我们观察到结果,我们就会做笔记或向团队报告我们的发现,所有这些都会更新我们对特性的理解,准备好让我们再次开始这个过程。这个流程可以用类似于图 3.3 所示的可视化来总结。

图 3.3 测试每日特惠功能过程的可视化

这个循环可能会迅速发生——例如,在一次探索性测试会话中。或者可能是以较长的形式进行,比如性能测试,其中每一步都有许多细节需要考虑。无论是哪种类型的活动,要成功地执行这个循环,我们都需要工具来完成这个过程。我们可能需要使用诸如数据库客户端、测试数据管理器或基础设施工具来设置状态管理。我们会使用笔记工具、截图应用程序和项目管理工具来报告所学内容。我们可以通过更新我们的初始可视化来总结这些测试工具的用途,如图 3.4 所示。

图 3.4 测试过程的可视化,这次添加了工具

这个可视化演示了现代测试如何利用各种工具来执行测试中发生的各种任务——而不是试图将整个测试性能都包装到一个工具的使用中。这是因为,正如可视化所展示的那样,当进行测试时,有许多不同的活动在进行,通常成功的工具是那些能够很好地完成一项工作的工具,而不是那些能够很差地完成许多事情的工具。

这种思考之所以如此有趣,是因为当我们花时间考虑它时,它对我们来说似乎是显而易见的。我们都使用工具来帮助我们完成构成较大活动的不同任务。然而,大多数人在没有意识到或故意思考的情况下就这样做了。虽然我们知道在特定任务中使用工具是明智的,但我们需要培养选择并有意识地使用工具的技能。这意味着熟悉工具,并更加关注我们在测试中每天所做的事情,以便我们可以选择正确的工具,或者在 LLMs 的情况下,选择正确的提示或增强功能。本书的其余部分将专注于这一点,为我们提供需要的技能,以便知道 LLMs 何时可以帮助完成特定任务。

3.3 知道何时在测试中使用 LLMs

现在我们已经了解了测试中有效利用工具是关于寻求增强测试中的任务而不是复制广泛的测试活动。但是 LLMs 如何适应这一点呢?考虑到它们的广泛应用,让我们看看目前 LLMs 被普遍使用的一些方式,以及如何每种方式都可以用于支持测试。

3.3.1 生成能力

生成 AI 的标签被应用到 LLMs 上,因为它们提供了生成新的自然语言输出的能力。这意味着如果我们要求 LLM 生成一首关于软件测试奇迹的诗,它会生成,尽管可能会重复它训练过的材料——而且可能是一首糟糕的诗。但是暂且不谈质量,LLMs 基于其模型和所给的提示生成新输出的能力是吸引了很多关注的。

然而,我们必须提醒自己,LLM 生成输出的方式是基于复杂的概率模型。这意味着它将以与我们人类生成思想不同的方式生成材料。一个很好的例子是使用 LLMs 生成测试用例。如果你回忆一下第一章,我们探讨了向 LLM 发送以下提示时的概念:

Create tests for a file upload feature

它可能会以类似于以下的方式做出响应(为了简洁起见进行了缩短):

1\. Test for File Upload Functionality:

    Test Case 1.1: Upload a Valid File
        Input: Choose a file with a valid format (e.g., .jpg, .pdf).
        Expected Outcome: The file is successfully uploaded, and a confirmation message is displayed.

    Test Case 1.2: Attempt to Upload an Invalid File
        Input: Choose a file with an invalid format (e.g., .exe, .html).
        Expected Outcome: An error message is displayed, indicating that the file format is not supported.

2\. Test for File Size Limit:

    Test Case 2.1: Upload a File Below the Size Limit
        Input: Choose a file smaller than the maximum allowed size.
        Expected Outcome: The file is successfully uploaded, and a confirmation message is displayed.

    Test Case 2.2: Attempt to Upload a File Exceeding the Size Limit
        Input: Choose a file larger than the maximum allowed size.
        Expected Outcome: An error message is displayed, indicating that the file exceeds the maximum size limit.

LLM 生成这些测试用例的方式令人印象深刻,但它们缺乏与我们正在测试的特性或产品相关的上下文和特定性。因此,当与 LLMs 一起工作时,我们需要确保我们如何提示它们或扩展它们提供足够的上下文,以便生成有价值的输出。

确保我们提供足够的上下文的结果是,使用 LLMs 更容易生成非常具体和有针对性的任务输出。另一种方法是我们必须提供大量的输入数据,这将导致一个昂贵且难以维护的提示。例如,想象一下你需要为 LLM 提供多少上下文才能得到一个与我们的工作背景相关的测试策略。

相反,如果我们专注于使用 LLMs 来帮助执行这些任务,我们就可以从它们那里获得更多的价值:

  • 测试数据生成 - 当给定数据集的明确规则时,LLMs 可以用于生成一系列用于各种测试活动的数据,从探索性测试到性能测试。

  • 风险和测试点的建议 - 我们应该始终避免让 LLMs 的输出成为测试与否的唯一裁决者。我们可以用它们来提出测试点和风险,作为新想法的出发点或纳入我们现有的工作。

  • 代码片段 - 类似于之前的测试用例示例,如果我们让它们生成完整的自动化测试或框架,我们将从 LLMs 中获得很少的价值。然而,使用它们来生成自动化的较小部分或用于支持测试活动的脚本,如探索性测试,可能是有利的。

3.3.2 变换能力

LLMs 提供的另一个好处是能够将自然语言从一种结构转换为另一种结构。LLM 转换的常见示例是语言翻译。假设我们向 LLM 发送了类似于以下的内容:

Convert the following text delimited by three hashes into French:

###
Hello, my name is Mark
###

然后它会返回类似于以下的响应:

Bonjour, je m'appelle Mark

这是一个说明 LLM 如何转换数据的有用方式,但我们不应仅限于口语。LLM 能够将所有类型的数据从一种抽象转换为另一种。以下是一些可帮助测试的示例:

  • 转换测试数据 - 使用 LLM 迅速将数据从一种结构转换为另一种结构可以加快测试速度。例如,我们可以要求 LLM 将纯文本测试数据转换为 SQL 语句,或者将 SQL 语句转换为在测试自动化中调用的帮助函数。

  • 转换代码 - LLM 可以将函数、类和其他数据转换为新的代码。重要的是,LLM 可以将代码转换为不同的语言,但仍然保持原始代码的逻辑和流程在新的翻译输出中(尽管我们应该始终测试以确保正确)。

  • 总结笔记 - 虽然不像将代码片段从一种语言转换为另一种语言那样直接转换数据,但我们可以使用 LLM 同时进行转换和总结。我们可以使用 LLM 将来自测试活动(如探索性测试或左移测试会议)的原始测试笔记转换为摘要笔记与他人共享。

3.3.3 增强能力

最后,我们可以使用 LLM 来增强和扩展现有材料。这通常意味着向 LLM 提供一小段数据片段,并提示 LLM 扩展该段落。这与生成能力有一些重叠,因为我们要求 LLM 生成一定程度的新输出,但在这种情况下,我们提供了更多的上下文信息,并指导其关注现有材料,而不是提示 LLM 生成完全不同的内容。这意味着我们可以利用这种能力来帮助我们完成以下测试任务:

  • 代码审查 - 并非所有从事测试工作的人都是自信的编码人员,即使是那些习惯阅读代码的人有时也会在分析或测试所需的代码时感到困惑。LLM 可以通过获取代码片段并提供自然语言的代码分解来增进我们的理解,这对于风险分析、测试设计等方面都有帮助。

  • 描述性代码 - 类似于审查代码,我们可以使用 LLM 来帮助提高代码的描述性 - 例如,快速创建易于创建和维护的代码注释。对于自动化测试特别有用,在自动化代码要进行维护的情况下,交流我们的自动化代码正在做什么很重要。

  • 扩展分析 - 我们还可以使用 LLM 来扩展我们的分析活动,如风险分析和左移测试(在构建功能之前,我们提前提出需求问题)。通过提供当前的分析数据,我们可以要求 LLM 进行审查和扩展,提出新的想法,我们可以将其纳入我们的分析中或忽略。

3.3.4 在测试中使用 LLM

为了将 LLMs 的这些不同能力置于上下文中,让我们回到我们在图 3.5 中展示的带有工具支持的测试的可视化中。

图 3.5 测试过程的可视化,这次增加了 LLMs 的内容。

通过查看此更新后的可视化,我们可以看到 LLMs 如何被插入到更广泛的测试生命周期中的不同和特定任务中。这让我们回到了我们在第一章中涵盖的影响范围模型。与其尝试使用 LLMs 来复制生命周期中存在的所有测试活动,我们更优先考虑人类的最佳能力和我们为测试带来的价值。然后我们选择在选择性的领域中添加 LLMs 以扩展我们的工作,以便我们能够更快地移动,学到更多,并确保我们的团队得到更好的信息,以便他们能够构建更高质量的产品。

3.4 总结

  • 如果我们对测试的理解有限,那么使用工具的范围也将受限。

  • 测试不是一项确认性的练习,而是一系列不同的活动,帮助那些想要了解产品想象力和实施的人。

  • 想象力与我们对所要构建的东西的理解有关。

  • 实施与我们对所构建的东西的理解有关。

  • 随着我们对两个方面的理解增加,它们会相互一致,帮助我们交付更高质量的产品。

  • 我们运行许多不同类型的测试活动,以便专注于不同类型的风险以及它们对想象力和实施的影响。

  • 工具是成功测试的重要组成部分。

  • LLMs 可以用于生成、转换和增强输出。

  • 使用 LLMs 进行较小,具体的任务,以生成提供价值的输出。

  • LLMs 可以为我们生成内容,这些内容可以帮助完成特定任务或创建建议的内容。

  • LLMs 可以将数据转换为有用的格式或对其进行总结。

  • LLMs 还可以增强现有材料,添加新的建议或扩展细节。

  • 我们可以将 LLMs 插入到许多不同且具体的测试任务中,这反映了我们在第一章学到的影响范围模型。

第四章:AI 辅助开发者测试

本章内容包括

  • 使用 GitHub Copilot 开发单元测试和生产代码

  • 使用 ChatGPT 开发单元测试和生产代码

根据 JetBrains 于 2022 年进行的开发生态系统调查显示,81%的受访者的开发人员与 QA 的比例大于 1:1。其中 40%报告称他们的开发人员中“不到 10 名开发人员中有 1 名 QA”,只有 1%的受访者报告称他们拥有“比开发人员更多的 QA”。(该调查可在 www.jetbrains.com/lp/devecosystem-2022/testing 找到。)

理解和建立质量至关重要,以向我们的用户提供价值,然而,开发和测试之间的比例几乎总是不平衡的,原因有很多。一些组织领导选择通过质量教练的支持来教育开发人员建立质量,而其他人则简单地不愿意投资于提倡测试和质量的角色。无论哪种情况,这种情况都给团队的每个人带来了交付高质量应用程序的压力。那么,AI 工具如何帮助减轻这种压力呢?

在本章中,我们将专注于如何借助大型语言模型(LLM)AI,如 GitHub Copilot 和 ChatGPT,帮助我们开发人员将质量融入我们的工作中。我们不应将这些 AI 工具视为开发人员和测试人员的替代品,而应学习它们如何指导我们在开发过程中进行质量建设的活动,以及如何利用 AI 工具引导改进质量并在测试资源有限时识别风险。

4.1 探究自动化开发者的兴起

2023 年 4 月,市场竞争分析公司 Similarweb 报告称,Stack Overflow 在前一个月的流量下降了 14%。根据 Similarweb 的说法,原因是开发人员从 Stack Overflow 转向 GitHub 的 Copilot 和 ChatGPT 等工具。 (您可以在 www.similarweb.com/blog/insights/ai-news/stack-overflow-chatgpt 阅读该文章。)无论这是否是开发人员跳槽的持续趋势,还是最终会达到平衡的异常情况,像这样的报道都显示了基于 LLMs 的 AI 工具(LLMs)对开发人员角色引入的巨大变化。

作为测试人员,见证这种变化是很有趣的。多年来,随着测试自动化工具的不断发展,我会与开发人员讨论是否可以通过自动化测试工具来替代测试工作。现在,随着针对来自公共和私人存储库的数十亿行代码进行训练的 LLMs 的出现,谈论的话题已经转向是否可以自动化开发人员的角色。例如,像 ChatGPT 这样的工具可以发送这样的提示:

Write me some Java code that sends an HTTP request to a booking API

然后,它将利用其数十亿个训练权重和平衡来返回一个工作代码示例,就像这样:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;

public class BookingAPIClient {
    public static void main(String[] args) {
        String apiUrl = "https://api.booking.com/endpoint"; // Replace with the actual API endpoint URL

        try {
            URL url = new URL(apiUrl);
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();

            // Set the request method (GET, POST, etc.)
            connection.setRequestMethod("GET");

            // Set request headers if required
            connection.setRequestProperty("Content-Type", "application/json");
            connection.setRequestProperty("Authorization", "Bearer YOUR_ACCESS_TOKEN");

            // Get the response code
            int responseCode = connection.getResponseCode();
            System.out.println("Response Code: " + responseCode);

            // Read the response
            BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
            String line;
            StringBuilder response = new StringBuilder();
            while ((line = reader.readLine()) != null) {
                response.append(line);
            }
            reader.close();

            // Print the response
            System.out.println("Response: " + response.toString());

            // Close the connection
            connection.disconnect();

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

从一个基本提示我们可以使用 LLM 来创建可用的代码(将 apiUrl 更新为真实的 API 返回了积极的结果)。因此,出现了结合提示尝试自动化开发工作的工具并不奇怪。出现了像 AutoGPT 和 MetaGPT 这样的工具,它们作为自治代理工作,根据最初的问题生成自己的提示来解决复杂的问题。尽管这些工具还处于萌芽阶段,但很明显可以理解为什么开发人员被自动化出他们的角色的夸张说法所困扰。

作为一个大部分职业生涯都在解释为什么测试自动化不能作为合适替代品的人,很容易享受看到开发人员以同样的方式捍卫他们的角色的快感,但相反,从测试人员的经验和自动化的话题中学习更有价值。正如测试人员的角色不能完全自动化一样,开发人员的角色也不能。开发角色不仅仅是所产生的代码。开发人员创建的解决方案是分析技能、问题解决和设计思维的产物。这些是 LLM 工具给人的印象能够做到的技能,但目前它们还不能。

相反,开发人员通过使用 LLM 工具来增强他们自己的能力而取得成功,使用像 Copilot 这样的工具快速有效地创建他们想要构建的代码,或者向 ChatGPT 寻求解决问题或学习新的 API。这些原则也可以应用于提高开发人员构建应用程序质量的能力。通过将诸如测试驱动设计(TDD)或与 LLM 强大的配对技术相结合,开发人员可以提高生产力,同时确保他们的分析和设计技能发挥主导作用。为了帮助证明这种共生关系,让我们探讨两个例子:

  • 使用 Copilot 快速生成 TDD 循环的单元检查和生产代码

  • 通过 ChatGPT 的模拟开发人员模拟配对

通过这些示例,我们将学习设置和使用这些 LLM 工具,并欣赏我们可以在 AI 的力量和开发人员的能力之间取得的平衡。

经验可能有所不同

鉴于 Copilot 依赖于经常在新添加的代码和更新的 API/库上进行训练的预测算法,值得强调的是,遵循即将出现的示例时您所拥有的输出和体验可能与已记录的内容不同。请记住,本章的目标不是完全复制本章中的示例,而是让我们舒适地使用 LLM 工具来协助我们的工作,从而帮助我们提高质量。

4.2 与 LLM 的配对

我们已经花了时间了解到 LLM 具有概率性质,因此认为它们输出的是角色的模拟,而不是具体的角色。LLM 对于自己是软件测试员还是餐厅经营者并不更加清楚。但是通过提示工程,我们可以创建提示,将 LLM 的概率输出框架化为模拟角色,帮助我们创建与之交互的橡皮鸭。当测试资源在可用性或能力上有限时,这在开发方面可能很有用。因此,让我们看看我们可以使用的几个示例提示,这些提示可以帮助我们改进我们的工作和产品的质量。

等等,什么——橡皮鸭?

面对无解的问题时,向他人口头表达问题可能有助于找到答案。通过向他人表达问题,我们有时会发现解决方案呈现在我们面前。然而,并不总是有机会与同事交流;因此,一些开发人员会向橡皮鸭(或其他物品)表达他们的问题。尽管我们把挑战分享给了一个无生命的物体,但体验是相同的:口头表达我们的挑战会让我们的大脑找到解决方案。

4.2.1 分析思想

我们的第一个提示灵感来自于一些团队应用的左移思维,旨在尽早将测试分析引入开发过程中,理想情况下是在讨论想法和澄清细节的时候。左移意味着将测试分析尽早引入开发过程中,理想情况下是在讨论想法和澄清细节的时候。这个过程会引发许多问题,这些问题有助于我们更早地识别问题、交付有价值的功能并提高产品的质量。左移活动对于提高质量非常重要,应该鼓励,无论在开发过程中是否使用 AI。但这并不意味着我们不能使用提示来生成可能揭示假设或误解的问题,或者我们可以简单地忽略的问题。

让我们来看一个可以用来迅速生成可能有用的问题的示例提示:

#A
You are a software testing expert. Analyse the text delimited by triple hashes and generate questions that will clarify ambiguities in the text.

#B
* Questions will be generated by using the What, Where, Why, When, Who and How heuristic
* Multiple questions can be identified for each quality characteristic
* The quality characteristics we care about are: Accuracy, Security and Accessibility
* Output questions in the format of Question - Explanation

#C
Example:
What does relevant mean?
The acceptance criteria says relevant search results, but how do we determine what is relevant and what isn't?

#D
###
As a B&B owner
I want to be able to manage user access
So that I can allow different levels of access to multiple users

Features:

- DB is configured with an Admin user
- Guests are asked to create an account to make a booking
- Admin can see the accounts created on the platform and can delete them
###

提示概述了一个用于分析用户故事和验收标准的 LLM 的指令,并返回一个我们要考虑的问题列表。请注意,我们还在质量特征指令中提供了上下文。如果我们想要关注不同的特征,我们也可以更新这些特征。

将此提示输入 ChatGPT 返回了以下结果:

Accuracy:
1\. What criteria are used to determine the accuracy of user access management?
Explanation: The text mentions managing user access, but it doesn't specify how accuracy is ensured in granting different levels of access to multiple users.

2\. How is the system ensuring that the right access levels are assigned to users?
Explanation: The text refers to allowing different levels of access to multiple users, but it doesn't explain the mechanism for guaranteeing the accuracy of access permissions.

Security:
1\. Who has access to the database configured with an Admin user?
Explanation: The text mentions configuring the database with an Admin user, but it doesn't specify who has access to this Admin account and how its security is maintained.

2\. How is the platform protecting user account information and data?
Explanation: The text talks about guests creating accounts for bookings, but it lacks details about the security measures implemented to safeguard user data.

3\. What security measures are in place to prevent unauthorized access to user accounts?
Explanation: The text mentions that Admin can see and delete accounts, but it doesn't explain how unauthorized access to such account management functionalities is prevented.

Accessibility:
1\. What levels of accessibility are provided to different types of users?
Explanation: The text mentions managing user access with different levels, but it doesn't clarify what those levels are and how they vary based on user types.

2\. How are guests guided to create accounts for booking, and is the process accessible to all users?
Explanation: The text states that guests are asked to create accounts, but it doesn't specify how this process is carried out and if it meets accessibility standards.

3\. What measures are taken to ensure the platform's accessibility for users with disabilities?
Explanation: The text doesn't provide information on how accessibility is addressed for users with disabilities, and it's essential to understand the measures taken to accommodate their needs.

4\. Where can we find information about the platform's accessibility features?
Explanation: The text mentions nothing about the platform's accessibility features, so it would be helpful to know where this information is available.

通过阅读响应,我们可以看到有一个有趣的问题集合,质量参差不齐。例如,考虑这个问题:

How are guests guided to create accounts for booking, and is the process accessible to all users?
Explanation: The text states that guests are asked to create accounts, but it doesn't specify how this process is carried out and if it meets accessibility standards.

这是一个有帮助的问题,鼓励我们更详细地考虑Guests are asked to create an account to make a booking这一陈述,在我们的用户故事中发现。我们可以考虑这个问题,思考一下我们将如何设计预订流程,使其对所有类型的用户都可以访问。

然而,其他问题可能并不太好。例如:

What criteria are used to determine the accuracy of user access management?
Explanation: The text mentions managing user access, but it doesn't specify how accuracy is ensured in granting different levels of access to multiple users.

使这个问题变得棘手的是它对accuracy质量特征的引用。通常,准确性更侧重于处理或存储的数据的准确性(想想银行利率计算)。相反,“用户访问的准确性”这个短语感觉像是描述用户和他们可以访问的内容的规则集的奇怪方式。最终,我们需要评估每个问题的适用性和使用情况。一些问题可以鼓励我们构建更符合用户需求的产品,并帮助我们避免错误——而其他生成的问题要么没有意义,要么涵盖了已经考虑过的主题。

我们将回到这个提示,以及我们在开发功能期间如何使用它,但首先让我们看看我们如何重新利用这个提示来审查我们的代码。

4.2.2 分析代码

就像我们可以促使一个 LLM 分析书面的想法一样,我们也可以让它审查代码,并帮助我们识别风险。使用这个提示类似于模拟与你合作的开发人员或测试人员的角色,让他们在你开发时分析你的工作并提出建议供考虑。让我们看看一个可能对这种类型的活动有用的提示:

#A
You are a software testing expert. Analyze the code delimited by triple hashes and identify risks that might impact the code. If no code is provided, respond with "No risks could be found"

#B
* Risks must be related to the quality characteristics: Performance, security and interoperability
* Multiple risks can be identified for each quality characteristic
* Output identified risks in the format of Risk - Quality Characteristic - Explanation

#C
Example:
Overflow - Security - Adding in large integers as parameters might cause the method to trigger a buffer overflow.

#D
###
public BookingResult createBooking(Booking bookingToCreate) throws SQLException {
    if(dateCheckValidator.isValid(bookingToCreate.getBookingDates())) {
        if (bookingDB.checkForBookingConflict(bookingToCreate)) {
            return new BookingResult(HttpStatus.CONFLICT);
        } else {
            CreatedBooking createdBooking = bookingDB.create(bookingToCreate);

            if(bookingToCreate.getEmail() != null && bookingToCreate.getPhone() != null){
                MessageBuilder messageBuilder = new MessageBuilder();
                Message message = messageBuilder.build(bookingToCreate);

                messageRequests.postMessage(message);
            }

            return new BookingResult(createdBooking, HttpStatus.CREATED);
        }
    } else {
        return new BookingResult(HttpStatus.CONFLICT);
    }
}
###

在我向 ChatGPT 发送了包含示例代码和质量特征的提示后,返回了以下输出:

Based on the provided code snippet, let's analyze the potential risks that might impact the quality characteristics of Performance, Security, and Interoperability:

    Performance - Nested Validation:
    Risk: The nested validation checks (dateCheckValidator.isValid and bookingDB.checkForBookingConflict) may lead to performance issues, especially when dealing with a large number of bookings.
    Explanation: If the system has to process a substantial amount of bookings, the nested validation can result in increased execution time and resource consumption.

    Performance - Message Posting:
    Risk: The message posting (messageRequests.postMessage) might introduce performance bottlenecks, affecting the overall system responsiveness.
    Explanation: If the message posting process involves network communication or I/O operations, it could slow down the execution of the createBooking method.

    Security - SQL Injection:
    Risk: The code is susceptible to SQL injection attacks as it directly uses the bookingToCreate object in the database queries.
    Explanation: Malicious input in the bookingToCreate object could manipulate the SQL queries and compromise the integrity and security of the database.

    Security - Inadequate Authorization:
    Risk: The code does not perform explicit authorization checks before creating bookings, potentially allowing unauthorized users to create bookings.
    Explanation: Lack of proper authorization checks can lead to unauthorized access and misuse of the system's booking creation functionality.

    Interoperability - Platform Dependencies:
    Risk: The code might have dependencies on specific platforms or libraries, limiting its interoperability with other systems or environments.
    Explanation: If the MessageBuilder or messageRequests rely on platform-specific features or external libraries, it could cause issues when running the code in different environments.

    Interoperability - Error Codes:
    Risk: The code returns HTTP status codes directly, which may not be compatible with other systems that expect different error code formats.
    Explanation: If external systems expect error codes in a different format (e.g., JSON or XML), using HTTP status codes may hinder interoperability.

与前一个提示类似,它围绕书面需求产生了问题,这些不同风险的质量也不同。例如,风险Performance - Message Posting感觉相当抽象,正如我们在其解释中看到的那样:

If the message posting process involves network communication or I/O operations, it could slow down the execution of the createBooking method.

围绕网络通信或 I/O 操作提出风险建议感觉模糊,因为它可能涉及一系列实现,其中一些可能与我们的工作有关,而另一些可能不相关。

然而,其他风险更具体,可能更有用,例如,Security - Inadequate Authorization,其中它强调了:

The code does not perform explicit authorization checks before creating bookings, potentially allowing unauthorized users to create bookings.

这个风险感觉更具体,因为它涉及到我们方法中执行的操作,以及其中缺少的可能重要的检查。当然,我们可能会在其他地方进行授权检查,但使用它所提供的信息,它强调了一个明确的活动,我们可能需要进一步讨论以改进我们的预订功能的安全性。

生成更多的想法

到目前为止,我们已经查看了发送给 LLM 的单个提示,这使我们有用的反馈进行了审查。但是如果我们想要索取更多的问题和风险怎么办?我们只需提交额外的提示,例如“生成更多问题”或“识别更多风险”。但要小心,因为这会有递减的回报。LLM 将尝试满足我们的要求,从而增加幻觉的风险。因此,随着选项开始枯竭,我们可能会看到更多与我们最初希望反馈的想法和代码不相关的建议。

4.2.3 认识到模拟总比什么都没有好

当讨论测试时,重点通常集中在测试用例的制作和执行上。但是,一个经过高度培训和经验丰富的测试人员通过使用其批判性和横向思维技能提供价值,并提出有助于我们以新的方式查看解决方案并揭示潜在问题的问题。我们看过的提示可以提供该过程的模拟。但重要的是要记住,LLMs 没有这些批判性和横向思维技能,而生成的问题和风险来自我们提示的指示。相反,这些类型的提示可以提供一种轻量级的方式来模拟与测试人员或其他开发人员配对的经验,当无法配对时。关键是培养一种识别生成问题的眼光,以确定哪些是有用的,哪些是没有用的。

4.3 利用 AI 辅助构建质量

到目前为止,我们已经将提示视为单一活动,但现在让我们将注意力转向我们最近学到的提示以及其他 LLM 辅助工具如何与 TDD 结合使用,帮助我们构建质量。

尽管 TDD 与其他测试活动相比并不严格是一种测试活动,但正确执行的 TDD 可以帮助开发人员将质量融入我们的产品中。简而言之,TDD 的过程是使用单元检查工具首先创建失败的检查,然后创建足够的生产代码使检查通过(并修复可能失败的任何其他检查)。一旦我们的所有检查都通过了,我们可以在确保所有检查都是绿色的同时重构我们的生产代码。完成后,我们重新开始循环,直到工作完成,如图 4.1 所示。

图 4.1 红绿重构 TDD 循环

所有这些检查是什么意思?

在我们的测试自动化培训中,Richard Bradshaw 和我区分了人工测试和工具测试。我们称后者为自动化检查,因为工具只能断言我们编码到自动化中的显式操作或数据。这种区别帮助我们更好地理解,像单元检查框架这样的自动化工具非常擅长快速检查产品中的小型、具体的变化,但不能告诉我们有关系统的更多信息。然而,人类在测试中更慢且不确定性更大,尽管我们在识别同时发生的许多事件时效率更高。因此,这就是为什么工具检查而人类测试。一个并不比另一个更好,希望这本书能证明当我们结合两者时,我们能取得最好的成功。

这种方法使我们能够设计出高度可测试的产品,同时确保我们提供了业务或最终用户所期望的内容。

尽管它的好处多多,但一些开发人员发现很难采用 TDD 方法,因为有些人认为它会减慢开发速度,因为我们为系统中添加的每个特定的生产代码部分创建单元检查。然而,通过使用像 Copilot 这样的工具,我们可以学会如何建立一个平衡,在这个平衡中,工具使我们能够快速创建单元检查并构建质量。

4.4 使用 LLMs 创建我们的第一个 TDD 循环

为了演示,让我们来完成为示例工时管理器的一部分功能的过程,该功能如下所述:

As a timesheet user
I want to be able to track time against specific projects
So that I can keep a history of how much time I have spent working on individual projects

Features:

- Time is tracked in half hour units
- To complete a timesheet a project and amount of time must be entered
- I should only be able to see my tracked time

您可以在以下 GitHub 存储库中查看作为示例一部分创建的测试和生产代码:github.com/mwinteringham/ai-assisted-testing/tree/main/chapter-4

设置 Copilot

这一章假设您已经在您的集成开发环境中安装并配置了 Copilot 插件。如果您尚未完成设置过程,您可以在附录中找到安装说明。

4.4.1 准备工作

在开始创建我的工时管理器功能之前,我想先考虑一下如何构建它。为了帮助我进行这个过程,并鼓励我从各个角度考虑解决方案,我将本章前面探索的第一个提示输入到其中。具体来说,我:

  • 将质量特性更改为准确性和一致性

  • 将用户故事添加到提示底部的分隔部分中

You are a software testing expert. Analyze the text delimited by triple hashes and generate questions that will clarify ambiguities in the text.

* Questions will be generated by using the What, Where, Why, When, Who and How heuristic
* Multiple questions can be identified for each quality characteristic
* The quality characteristics we care about are: Accuracy and Consistency
* Output questions in the format of Question - Explanation

Example:
What does relevant mean?
The acceptance criteria says relevant search results, but how do we determine what is relevant and what isn't?

###
As a timesheet user
I want to be able to track time against specific projects
So that I can keep a history of how much time I have spent working on individual projects

Features:

- Time is tracked in half hour units
- To complete a timesheet a project and amount of time must be entered
- I should only be able to see my tracked time
###

将此提示发送给 ChatGPT 返回了以下响应:

Accuracy:

    What is the required precision for tracking time in half-hour units? - The text mentions that time is tracked in half-hour units. It's important to know the level of accuracy required for this tracking.

    How will the system handle invalid time entries? - Since time must be entered to complete a timesheet, what measures are in place to ensure that only valid time entries are accepted?

    What validation mechanisms are in place to prevent users from entering incorrect time data? - To maintain accuracy, it's essential to know how the system will validate and handle potential data entry errors.

    How will the system handle time zone differences for users in different locations? - This is important to ensure that the tracked time is consistent and accurate for users in various time zones.

Consistency:

    Where will the history of tracked time be stored and accessed? - To ensure consistency, it's important to know where the data will be stored and how users can access it.

    What are the rules for displaying and formatting tracked time information? - Consistency in displaying the tracked time across the system is crucial for a seamless user experience.

    Who is responsible for verifying and maintaining the consistency of tracked time data? - It's essential to assign responsibility for data integrity and consistency to avoid discrepancies.

    When and how often will the timesheet data be synchronized or updated? - Ensuring consistency in real-time updates or scheduled syncs is vital to avoid discrepancies between users' data.

    How will the system handle potential conflicts if multiple users try to update the same project's timesheet simultaneously? - Maintaining consistency in data updates when multiple users are involved is critical to prevent data loss or inaccuracies.

    What security measures are in place to protect the data and ensure its integrity? - Consistency also involves safeguarding the data from unauthorized access or tampering.
什么是质量特性?

质量对不同的人意味着完全不同的事情,是一个多方面的概念。因此,当试图理解质量对利益相关者或最终用户意味着什么时,我们将其思考方式分解成不同的特征。例如,质量特征可能包括外观和感觉、可用性、合规性等等。不同的项目将有不同的质量特征,我们要优先考虑哪些质量特征是我们和我们的用户的重点,这取决于我们作为团队的选择。

从这里开始,我阅读了每个发送给我的问题,并注意到那些引起我注意的问题,因为它们提出了我没有考虑过的观点:

How will the system handle invalid time entries? / What validation mechanisms are in place to prevent users from entering incorrect time data?

对我来说,这两者感觉非常相似,但它们强调了在某个时候,我需要将日期时间处理加入代码中,以防止无效输入引起的问题。

How will the system handle time zone differences for users in different locations?

这是一个很好的问题,但与这项工作无关,因为我打算只跟踪执行的时间,而不是执行的时间点(至少目前是这样)。

What are the rules for displaying and formatting tracked time information?

与前一个问题相关联,我选择简单地跟踪经过的时间,而不是具体时间点——这意味着信息的显示相对简单。

How will the system handle potential conflicts if multiple users try to update the same project's timesheet simultaneously?

目前还没有多用户功能,尽管它可能稍后会被引入,所以我做个记录,以便未来的迭代考虑。

还有许多问题可以提出和解决,但这个过程所展示的是,通过生成不同的问题,我被要求思考该功能设计的各个方面。有些问题我可以选择排除,或者暂时搁置以后的讨论,有些问题则帮助我提高功能的质量,提高我对验证和日期时间格式化的认识。

4.4.2 循环 1:保存时间表条目

在我脑海中有来自前一个提示的问题后,我开始我的第一个循环,通过在我的 IDE 中创建一个空的 Maven 项目,然后前往 pom.xml 添加所需的依赖项。我需要 JUnit 5 来构建我的单元检查,所以我输入了注释:

<!--    Junit jupiter engine-->

从 Copilot 返回建议的代码块:

<dependencies>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-engine</artifactId>
        <version>5.8.2</version>
        <scope>test</scope>
    </dependency>
</dependencies>

在撰写时,junit-jupiter-engine 的建议版本是旧版本,所以我更新到 5.9.2,如下所示:

<dependencies>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-engine</artifactId>
        <version>5.9.2</version>
        <scope>test</scope>
    </dependency>
</dependencies>

这次必要的更新是数据来源如何影响建议的一个很好的例子。 Copilot 虽然不断地在 GitHub 上推送的代码上进行训练,但存在落后于最新版本库和 API 的风险。因此,在通过 AI 代码助手添加依赖项时,我们需要保持警惕,以确保它们提供的是我们想要的版本。

此外,这里演示了 Copilot IDE 插件将我编写的代码(或在本例中是代码注释),添加到提示中,然后发送给 Copilot LLM 进行处理。LLM 然后向 IDE 插件发送一个建议,提示我输入下一个内容。然后我可以选择通过按 Tab 键接受建议,或继续编写自己的代码。

像其他 LLMs 一样,Copilot 是通过提示触发的。但与基于聊天的 LLM(如 ChatGPT)不同,Copilot 被调整为处理基于代码而不是文本的提示。这个区别很重要,因为它突显了为不同类型的活动调优 LLMs 的不同类型的好处,以及编写适用于不同类型提示的必要性。这就是为什么 TDD 和 AI 助手是一个有趣的组合来共同使用的原因。我们创建的单元检查不仅构成了我们工作的设计框架,还作为提示来告知我们如何实现功能。

例如,对于我们的第一个检查,我提示 Copilot 为我创建第一个单元检查,方法是将以下注释添加到一个名为TimesheetTest的新类中,该类位于src/test/java中:

// Test that when a timesheet is submitted with a project name and hours it returns true

This triggers Copilot to suggest the following unit check:

@Test
public void testSubmitTimesheetWithProjectNameAndHours() {
    Timesheet timesheet = new Timesheet();
    boolean result = timesheet.submitTimesheet("Project 1", 8);
    assertEquals(true, result);
}

查看这个建议的检查,我们看到它具备我们需要的必要细节。它为我打算构建的类起了一个合理的名字Timesheet,并建议了正确的参数项目名称和工时的方法submitTimesheet。这个检查足够提示,当我在src/main/java创建一个新的类Timesheet时,Copilot 建议:

public boolean submitTimesheet(String projectName, int duration) {
    return true;
}

单元检查作为提示,促使 Copilot 创建我的生产代码,虽然产生的东西并不是非常激动人心,但检查已经完成。我们还可以看到 Copilot 通过建立提示来获取我们想要的。首先,我创建了一个形式为代码注释的提示,以帮助 Copilot 建议单元检查的代码,然后该单元检查作为建议生产代码的提示。随着单元检查的增加,Copilot 的提示和指导将增加。现在让我们看看我如何使用这个过程来完善submitTimesheet方法。

4.4.3 循环 2:检索时间表条目

通过第一个循环,我创建了项目的骨架,但目前实际的生产代码几乎没有成果。因此,在第二个循环中,我试图添加更多功能到Timesheet类以帮助完善submitTimesheet方法。同样,我首先在TimesheetTest中添加了一个新的注释提示:

// Test that when timesheets are added they can be retrieved as a list

在创建新行时,Copilot 返回了以下检查作为建议:

@Test
public void testAddTimesheetsToList() {
    Timesheet timesheet = new Timesheet();
    timesheet.submitTimesheet("Project 1", 8);
    timesheet.submitTimesheet("Project 2", 8);
    timesheet.submitTimesheet("Project 3", 8);
    assertEquals(3, timesheet.getTimesheets().size());
}

这次,创建的检查是使用submitTimesheet方法创建一些数据,然后我可以使用新创建的getTimesheets方法进行检查。所以,转到Timesheet类,我使用 Copilot 生成必要的getTimesheets方法,它返回以下内容:

public Collection<Timesheet> getTimesheets() {
    return null;
}

虽然它为我创建了方法签名,但方法的内部并不那么有用。然而,然后我在类的顶部添加了以下内容:

private HashMap<String, Integer> timesheets = new HashMap<>();

然后返回submitTimesheet方法,并从方法内部删除return true。当我将关键字if输入到语句中时,Copilot 返回以下结果:

public boolean submitTimesheet(String projectName, int duration) {
    if (projectName != null && duration > 0) {
        timesheets.put(projectName, duration);
        return true;
    } else {
        return false;
    }
}

这是一个有用的演示,说明了当使用 Copilot 时,项目需要一定的预热时间,LLM 才能返回更准确的建议。返回submitTimesheet方法,清空其内容,然后等待 Copilot 的响应,我得到了来自 Copilot 的基于我提供的新详细信息的更新响应。就像对话 LLM 需要明确和明确的详细信息提示一样,Copilot 也是如此。最初从 Copilot 返回的标准方法可能是因为缺乏要处理的上下文。基本上,发送给 Copilot 的提示缺乏信息。但是随着我创建新的单元检查和额外的代码来展示我们在代码中的意图,建议变得更加有用。

当我完全删除getTimesheets方法时,这也得到了证实。随着添加到类中的新HashMap,我的更新代码促使 Copilot 返回方法的更新版本:

public Collection<Integer> getTimesheets() {
    return timesheets.values();
}

在这个循环之后运行我的两个单元检查,我可以看到两个检查都通过了,现在我有了一个比我开始循环时更丰富一些的工作代码。

4.4.4 循环 3:计算项目的时间

对于最后一个循环,我回到了 ChatGPT 返回的问题,并考虑了这个问题:

How will the system handle potential conflicts if multiple users try to update the same project's timesheet simultaneously?

虽然目前项目中没有多用户功能,但 ChatGPT 的问题让我思考如何处理对同一项目的多次工时提交。因此,我创建了第三个单元检查来处理这种情况,再次创建代码注释以提示 Copilot 进行另一个单元检查:

// Test that the total hours worked can be calculated from a list of timesheets from one project

返回给我的是以下单元检查:

@Test
public void testCalculateTotalHoursWorked() {
    Timesheet timesheet = new Timesheet();
    timesheet.submitTimesheet("Project 1", 8);
    timesheet.submitTimesheet("Project 1", 8);
    timesheet.submitTimesheet("Project 1", 8);
    assertEquals(24, timesheet.getTimesheets().stream().mapToInt(Integer::intValue).sum());
}

这一次,建议的检查接近,但我希望Timesheet类返回一个总数,而不是在单元检查本身中处理总数。因此,我选择更新检查,明确说明有一个方法可以获取总工时:

@Test
public void testCalculateTotalHoursWorked() {
    Timesheet timesheet = new Timesheet();
    timesheet.submitTimesheet("Project 1", 8);
    timesheet.submitTimesheet("Project 1", 8);
    timesheet.submitTimesheet("Project 1", 8);

    int total = timesheet.getTotalTimesheetHours("Project 1");

    assertEquals(24, total);
}

返回到Timesheet并等待 Copilot,LLM 返回以下代码:

public int getTotalTimesheetHours(String projectName) {
    return timesheets.get(projectName);
}

这使得代码可以编译,但我的新单元检查失败了,因为项目计数没有被更新。相反,因为我使用了HashMap,计数被覆盖了。我需要更新submitTimesheet以处理重复的项目,我在submitTimesheet中刚开始的 if 语句之后添加了以下注释:

// Check to see if project already exists

Dropping in the code comment returns me the following suggested code:

if (timesheets.containsKey(projectName)) {
    // If it does, add the new duration to the existing total
    duration += timesheets.get(projectName);
}

submitTimesheet的上下文中,更新它以增加工时表中现有项目的持续时间:

public boolean submitTimesheet(String projectName, int duration) {
    if (projectName != null && duration > 0) {
        // Check to see if project already exists
        if (timesheets.containsKey(projectName)) {
            // If it does, add the new duration to the existing total
            duration += timesheets.get(projectName);
        }
        timesheets.put(projectName, duration);
        return true;
    } else {
        return false;
    }
}

我的三个单元测试的最后一次运行确认它们都通过了,我在Timesheet类中拥有了我想要的所有功能。然而,代码开始变得更加复杂,所以我把注意力转向了使用 ChatGPT 来重构我的代码。

4.4.5 重构代码

与他人合作的一个好处是合作伙伴能够看到我们可能没有看到的潜在问题和风险。然而,在没有合作伙伴的情况下,我选择使用本章早些时候探讨过的第二个提示——具体地分析代码并返回建议的提示。将之前的提示与我的代码和质量特征的准确性和一致性相结合,我将这个提示发送给 ChatGPT:

#A
You are a software testing expert. Analyze the Java code delimited by triple hashes and identify risks that might impact the code. If no code is provided, respond with "No risks could be found"

#B
* Risks must be related to the quality characteristics: Accuracy and Consistency
* Multiple risks can be identified for each quality characteristic
* Output identified risks in the format of Risk - Quality Characteristic - Explanation

#C
Example:
Overflow - Security - Adding in large integers as parameters might cause the method to trigger a buffer overflow.

#D
###
public class Timesheet {

    private HashMap<String, Integer> timesheets = new HashMap<>();

    public boolean submitTimesheet(String projectName, int duration) {
        if (projectName != null && duration > 0) {
            // Check to see if project already exists
            if (timesheets.containsKey(projectName)) {
                // If it does, add the new duration to the existing total
                duration += timesheets.get(projectName);
            }
            timesheets.put(projectName, duration);
            return true;
        } else {
            return false;
        }
    }

    public Collection<Integer> getTimesheets() {
        return timesheets.values();
    }

    public int getTotalTimesheetHours(String projectName) {
        return timesheets.get(projectName);
    }
}
###

发送提示返回了以下我可能想考虑的风险:

Risk 1 - Accuracy - submitTimesheet method can overwrite existing duration:
The submitTimesheet method allows adding a new duration to an existing project's total if the project already exists in the timesheets. However, this approach may not always ensure accuracy. If the user intends to submit a correction or update the duration for a specific project, this method will not provide accurate results. It merely adds the new duration to the existing one without considering the possibility of editing or deleting a previous entry.

Risk 2 - Consistency - Null key in timesheets HashMap:
The timesheets HashMap allows null keys. This might lead to inconsistency issues when querying for timesheet information by project name. If a project with a null name is mistakenly added to the timesheets, it may cause issues in subsequent operations, leading to unexpected behavior.

Risk 3 - Consistency - Lack of validation for project names:
The submitTimesheet method does not validate the project name before adding it to the timesheets HashMap. This lack of validation might result in inconsistent data, such as duplicate or improperly formatted project names. For example, if two different variations of the same project name (e.g., "ProjectA" and "projecta") are added, the system might treat them as separate projects, leading to inconsistencies in reporting or querying.

Risk 4 - Accuracy - getTotalTimesheetHours method may throw NullPointerException:
The getTotalTimesheetHours method directly calls timesheets.get(projectName), which may throw a NullPointerException if the given projectName is not present in the timesheets HashMap. This can lead to inaccurate results if the method is used without checking for the existence of the project name in the timesheets before calling this method.

Risk 5 - Accuracy - Return type of getTotalTimesheetHours method:
The getTotalTimesheetHours method returns an int data type, which might lead to accuracy issues if the actual total timesheet hours for a project exceed the maximum value that an int can hold. This can result in data loss due to overflow if the sum of durations for a project exceeds Integer.MAX_VALUE.

Risk 6 - Consistency - Lack of synchronization in a multi-threaded environment:
If this Timesheet class is used in a multi-threaded environment, there is a risk of inconsistent behavior since the methods of this class are not synchronized. This can lead to data corruption or incorrect calculations if multiple threads concurrently access and modify the timesheets HashMap.

与问题提示不同,这个回复似乎指出了我的代码的具体项目,并说明了事情可能出错的原因。有了这个回复,我有以下几个选择:

  1. 逐个检查每个风险,然后减轻我认为重要的风险并忽略其他风险。

  2. 完全忽略所提出的风险,或者也许发送另一个提示给 ChatGPT,看看是否还有更多的风险。

  3. 将 ChatGPT 的回复本身作为提示,帮助我重构代码。

为了演示的目的,我选择了第三个选项,并向 ChatGPT 发送了一个额外的提示:

Refactor the code to mitigate the risks suggested. Check to see that each risk has been mitigated before outputting the solution.

注意我是如何运用思考时间原则来让 LLM 检查每个风险是否确实被它建议的重构代码所减轻的。这有助于增加返回的代码实际上会有用的可能性。将此提示发送给 ChatGPT 返回了以下代码:

import java.util.Collection;
import java.util.concurrent.ConcurrentHashMap;

public class Timesheet {

#A
    private ConcurrentHashMap<String, Long> timesheets = new ConcurrentHashMap<>();

    public boolean submitTimesheet(String projectName, long duration) {
        if (projectName != null && duration > 0) {
            // Perform validation on project name by converting it to lowercase
#B
            projectName = projectName.toLowerCase();

            // Check to see if project already exists
            timesheets.put(projectName, timesheets.getOrDefault(projectName, 0L) + duration);
            return true;
        } else {
            return false;
        }
    }

#C
    public Collection<Long> getTimesheets() {
        return timesheets.values();
    }

    public long getTotalTimesheetHours(String projectName) {
        return timesheets.getOrDefault(projectName.toLowerCase(), 0L);
    }
}

为了检查这些修改是否有效,我将建议的代码复制到Timesheet中,修改TimesheetTest中的testCalculateTotalHoursWorked以接受一个long而不是int,并发现我的检查仍然通过了,我已经重构了我的代码。

然而,这实际上改进了我的代码吗?为了确认,我再次用我的更新后的代码运行原始代码分析提示,这一次收到了新的风险:

Risk - Accuracy - Case Sensitivity Issue:
Explanation: The method submitTimesheet converts the projectName to lowercase before using it as a key in the timesheets map. This conversion might lead to inaccurate data retrieval if there are projects with the same name but different letter cases (e.g., "ProjectA" and "projecta" will be considered the same project).

Risk - Consistency - Inconsistent Data Entry:
Explanation: The submitTimesheet method does not enforce consistent data entry for projectName values. If different users enter project names in various formats (e.g., "Project A", "project-A", "project_A"), it could lead to multiple entries for what should be the same project in the timesheets map.

Risk - Accuracy - Missing Validation for Duration:
Explanation: Although the method submitTimesheet checks for a positive duration, it does not handle cases where the duration is 0 or negative. This could lead to incorrect data being recorded in the timesheets map.

Risk - Consistency - Lack of Input Sanitization:
Explanation: The submitTimesheet method does not sanitize the projectName input, which could lead to inconsistent data due to invalid characters or unexpected inputs.

Risk - Accuracy - Potential Data Loss:
Explanation: The method submitTimesheet returns a boolean value, but it does not provide any feedback or error handling mechanism if the submission fails. This lack of feedback could lead to data loss if the submission fails silently, and the user is not aware of the issue.

这个回应中有趣的是,我可以确认一些风险已经得到了减轻,比如:

Risk 5 - Accuracy - Return type of getTotalTimesheetHours method:
The getTotalTimesheetHours method returns an int data type, which might lead to accuracy issues if the actual total timesheet hours for a project exceed the maximum value that an int can hold. This can result in data loss due to overflow if the sum of durations for a project exceeds Integer.MAX_VALUE.

但似乎仍然有一些风险还没有被减轻。例如,在我收到的第一个风险清单中,我收到了以下风险:

Risk 3 - Consistency - Lack of validation for project names:
The submitTimesheet method does not validate the project name before adding it to the timesheets HashMap. This lack of validation might result in inconsistent data, such as duplicate or improperly formatted project names. For example, if two different variations of the same project name (e.g., "ProjectA" and "projecta") are added, the system might treat them as separate projects, leading to inconsistencies in reporting or querying.

ChatGPT 通过实现一个lowerCase方法来帮助清理项目名称来处理这个风险。然而,在第二次分析中,我收到了以下内容:

Risk - Consistency - Inconsistent Data Entry:
Explanation: The submitTimesheet method does not enforce consistent data entry for projectName values. If different users enter project names in various formats (e.g., "Project A", "project-A", "project_A"), it could lead to multiple entries for what should be the same project in the timesheets map.

这个风险与原始的、据称已经减轻的风险非常相似。感觉这个关于不一致数据输入的额外风险应该在我的代码重构时得到妥善处理。我可以再次要求 LLM 为我重构代码,但考虑到与 LLM 打转的潜力,对我来说更明智的做法是自己主导并修复问题。这是一个重要的技能要发展,何时依赖于 LLM,何时主导。

选择重要的原因可以通过第二轮分析提出的其他建议风险之一来突出显示。具体地说:

Risk - Accuracy - Missing Validation for Duration:
Explanation: Although the method submitTimesheet checks for a positive duration, it does not handle cases where the duration is 0 or negative. This could lead to incorrect data being recorded in the timesheets map.

这听起来像是一个令人信服的风险,但这只是一种幻觉的示范。就目前的代码而言,如果持续时间为 0 或更少,则该方法只需返回 false 并退出时间表存储:

if (projectName != null && duration > 0)

有时,LLMs 可能会存在偏见,优先考虑提供答案,而不考虑其质量。这意味着我们越多次要求 LLM 分析我们的代码,它就越有可能开始产生幻觉,以给出它正在产生有用结果的印象,而不是返回一个没有有用信息可分享的响应。这就是为什么我们必须仔细掌握何时使用 LLM 何时不使用的原因。

在这一点上,我选择停止使用案例,因为我们所涵盖的内容展示了不同类型的 LLMs 如何在不同方面帮助我。Copilot 提供了快速生成代码的能力,但它需要基于代码的提示来帮助它提出建议。项目越新,我们越有可能在 Copilot 中看到命中和未命中,但我们可以通过单元检查来帮助这个过程。这不仅有助于指导 Copilot 构建我们的代码,而且给我们带来了 TDD 的好处,包括良好设计的可测试代码。

通过 ChatGPT,我们已经证明了它可以是一个有用的分析工具,当正确提示时。构建可以分析思想和代码并提出风险和改进建议的提示可以迅速为我们提供不同的考虑角度,然后我们可以采取行动或拒绝。利用 LLM 作为倡导质量角色的模拟可以帮助我们改进工作。

4.5 改进与 LLMs 的文档和沟通

尽管看起来可能不是那么重要,但通过代码注释和发布说明来传达我们所做工作的方式对产品质量的贡献可能很大。通过分享新的发展和对代码库的更改,我们可以帮助其他开发人员了解我们的工作如何影响他们的工作,指导测试人员在测试我们的工作时应该关注什么,甚至帮助用户了解我们的产品(例如,Slack 早期的发布说明通过其清晰的沟通和幽默帮助市场推广他们的工具)。

尽管存在这些好处,文档和发布说明有时会被放置在开发周期的最后阶段,或者完全被忽略。考虑到编写和维护有用和相关的代码注释和发布说明所需的时间,尤其是当有时间压力不断交付新功能时,这是有道理的。然而,通过使用 LLMs,我们可以减少这种时间开销,同时确保我们创建有用的文档,为未来的读者创造价值。所以让我们看一些可以迅速为我们生成文档的有用提示。

4.5.1 生成代码注释

尽管我们应该始终努力创建以流畅易读的方式排列的代码,无论对于代码库的经验如何,代码注释可以提供额外的细节,以防止代码的误用并加快开发速度。如果我们发布的 API 将被他人使用,这一点尤为重要。(我曾多次因为 API 文档记录不足而浪费时间,试图理解一个库的工作原理。)挑战在于在代码注释中取得正确的平衡:太少了,读你代码的人将不得不自行解决;太多了,我们将为自己增加更多的工作量,以确保代码和注释保持一致。

因此,让我们看一个可以为我们处理注释工作的提示:

#A
You are a Java Developer. Update the code delimited by three hashes and add in code comments to explain how the code works. Check that no parts of the code have been changed before outputting the completed code commented code.

#B
###
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

public class Timesheet {

    private ConcurrentHashMap<String, Long> timesheets = new ConcurrentHashMap<>();

    public boolean submitTimesheet(String projectName, long duration) {
        if (projectName != null && duration > 0) {
                    projectName = projectName.toLowerCase();

            timesheets.put(projectName, timesheets.getOrDefault(projectName, 0L) + duration);
            return true;
        } else {
            return false;
        }
    }

    public Collection<Long> getTimesheets() {
        return timesheets.values();
    }

    public long getTotalTimesheetHours(String projectName) {
        return timesheets.getOrDefault(projectName.toLowerCase(), 0L);
    }

}
###

在这个提示中,我们使用通常的分隔符策略提供我们希望看到注释的代码,但我们也希望确保 LLM 不会改变我们的代码,以使其与它创建的注释保持一致。所以我们在提示中也明确表示,在输出完成的注释代码之前,检查代码的任何部分是否已更改。将此提示与我们之前的示例代码发送到 ChatGPT 返回了以下注释代码:

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

public class Timesheet {

    // ConcurrentHashMap to store project timesheets where the key is the project name (in lowercase) and the value is the total duration.
    private ConcurrentHashMap<String, Long> timesheets = new ConcurrentHashMap<>();

    // Method to submit a timesheet entry for a project.
    public boolean submitTimesheet(String projectName, long duration) {
        if (projectName != null && duration > 0) {
            // Normalize the project name to lowercase to ensure case-insensitive matching.
            projectName = projectName.toLowerCase();

            // Update the timesheet entry for the project by adding the submitted duration.
            timesheets.put(projectName, timesheets.getOrDefault(projectName, 0L) + duration);
            return true; // Submission successful
        } else {
            return false; // Invalid input, submission failed
        }
    }

    // Method to retrieve all timesheet durations.
    public Collection<Long> getTimesheets() {
        return timesheets.values();
    }

    // Method to get the total hours for a specific project.
    public long getTotalTimesheetHours(String projectName) {
        // Retrieve the total duration for the specified project (case-insensitive) or return 0 if not found.
        return timesheets.getOrDefault(projectName.toLowerCase(), 0L);
    }
}

正如我们所见,现在方法和其中的代码段都已经被注释以提供更多细节。最引人注目的是诸如:

// ConcurrentHashMap to store project timesheets where the key is the project name (in lowercase) and the value is the total duration.
private ConcurrentHashMap<String, Long> timesheets = new ConcurrentHashMap<>();

提供的注释不仅提到了ConcurrentHashMap的用途,还强调了所有项目名称将以小写形式存储,这是对submitTimesheet内部代码的一个参考,与此相比,像这样的注释感觉有点多余:

// Method to submit a timesheet entry for a project.
public boolean submitTimesheet(String projectName, long duration)

再次,我们可以自行选择使用什么,不使用什么。虽然我们使用的提示已经为我们提供了注释,但我们观察到有些可能比其他更有用。因此,我们的角色从注释的作者变为编辑,选择并调整看起来最有用的注释。

使用提示维护代码注释

这个过程如此有用的原因在于,当我们不可避免地遇到代码变化和我们的注释需要更新的时候,我们只需简单地运行带有我们更新代码的提示,以重新生成我们的注释,以整合我们的变化。

最后的观察是,尽管注释对于直接阅读我们的代码的人可能很有用,但如果我们想以结构化文档的形式(例如 Javadoc)提供更多的文档怎么办?在那种情况下,我们可以修改提示,并使用结构化数据策略来创建一个如下所示的提示:

#A
You are a Java Developer. Update the code delimited by three hashes and add in code comments in a JavaDoc format to explain how the code works. Check that no parts of the code have been changed before outputting the completed code commented code.

#B
###
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

public class Timesheet {

    private ConcurrentHashMap<String, Long> timesheets = new ConcurrentHashMap<>();

    public boolean submitTimesheet(String projectName, long duration) {
        if (projectName != null && duration > 0) {
                    projectName = projectName.toLowerCase();

            timesheets.put(projectName, timesheets.getOrDefault(projectName, 0L) + duration);
            return true;
        } else {
            return false;
        }
    }

    public Collection<Long> getTimesheets() {
        return timesheets.values();
    }

    public long getTotalTimesheetHours(String projectName) {
        return timesheets.getOrDefault(projectName.toLowerCase(), 0L);
    }

}
###

将此提示与我们的示例代码一起发送给 ChatGPT 产生以下结果:

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Represents a timesheet management system for tracking hours spent on different projects.
 */
public class Timesheet {

    // A concurrent hash map to store project names and their corresponding total hours
    private ConcurrentHashMap<String, Long> timesheets = new ConcurrentHashMap<>();

    /**
     * Submits a timesheet entry for a project.
     *
     * @param projectName The name of the project.
     * @param duration    The duration in hours to be submitted.
     * @return true if the submission was successful, false otherwise.
     */
    public boolean submitTimesheet(String projectName, long duration) {
        if (projectName != null && duration > 0) {
            // Normalize the project name to lowercase for consistency
            projectName = projectName.toLowerCase();

            // Update the total hours for the project or add a new entry if it doesn't exist
            timesheets.put(projectName, timesheets.getOrDefault(projectName, 0L) + duration);
            return true;
        } else {
            return false;
        }
    }

    /**
     * Retrieves a collection of all total timesheet hours for all projects.
     *
     * @return A collection of total hours for all projects.
     */
    public Collection<Long> getTimesheets() {
        return timesheets.values();
    }

    /**
     * Retrieves the total timesheet hours for a specific project.
     *
     * @param projectName The name of the project.
     * @return The total hours spent on the specified project, or 0 if the project is not found.
     */
    public long getTotalTimesheetHours(String projectName) {
        // Normalize the project name to lowercase for consistency and retrieve the total hours
        return timesheets.getOrDefault(projectName.toLowerCase(), 0L);
    }
}

使用这个提示,我们改进了一些注释的质量,并创建了可以用于为外部用户文档化我们的代码的 JavaDoc 友好的注释。我们需要做的就是在适当的地方进行微调和编辑。

4.5.2 生成发布说明

正如我们可以使用 LLM 修改我们的代码以添加注释一样,我们也可以使用 LLM 将我们的代码库转换为可以由团队和更广泛的人员消费的发布说明。为此,我们创建一个具有以下详细信息的新提示:

  #A
You are a Java Developer. Convert the code delimited by three hashes into a bullet-pointed set of release notes that describes how the code works. Check that the release notes can be read by someone who has no experience with the code created before outputting it. 

#B
* Only output the release notes.

#C
###
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

public class Timesheet {

    private ConcurrentHashMap<String, Long> timesheets = new ConcurrentHashMap<>();

    public boolean submitTimesheet(String projectName, long duration) {
        if (projectName != null && duration > 0) {
                    projectName = projectName.toLowerCase();

            timesheets.put(projectName, timesheets.getOrDefault(projectName, 0L) + duration);
            return true;
        } else {
            return false;
        }
    }

    public Collection<Long> getTimesheets() {
        return timesheets.values();
    }

    public long getTotalTimesheetHours(String projectName) {
        return timesheets.getOrDefault(projectName.toLowerCase(), 0L);
    }

}
###

这个提示遵循了与以前的代码注释为重点的提示类似的模式,但这次我们不是要求它将注释注入我们的代码,而是要求将代码转换为自然语言发布说明。例如,将带有示例代码的提示发送给 ChatGPT 返回了以下结果:

Release Notes:

    * The code defines a Java class named Timesheet that is used to manage timesheet data.
    * It uses a ConcurrentHashMap to store timesheet entries, where project names (in lowercase) are associated with their respective durations (in hours).
    * The submitTimesheet method allows users to submit timesheet entries. It requires a project name and a positive duration as parameters. If these conditions are met, it stores the duration associated with the project name in the map. If not, it returns false.
    * The getTimesheets method returns a collection of all recorded timesheet durations.
    * The getTotalTimesheetHours method allows users to retrieve the total duration for a specific project name (case-insensitive). If the project name is not found, it returns 0.

Note: Ensure that project names are provided in lowercase when using the submitTimesheet method to avoid case sensitivity issues when retrieving total timesheet hours.

我们得到的结果是完全不同于我们原始代码的格式,试图详细说明我们的代码库做了什么。回顾输出,它确实感觉非常技术化,这是有道理的,因为我们要求对少量代码进行详细说明。然而,即使是这些发布说明也可以派上用场,因为我们可以修改先前的提示以建议分析发布说明的风险:

#A
You are a software testing expert. Analyze the release notes delimited by triple hashes and identify risks that might impact the code. If no code is provided, respond with "No risks could be found"

#B
* Risks must be related to the quality characteristics: Performance, security and interoperability
* Multiple risks can be identified for each quality characteristic
* Output identified risks in the format of Risk - Quality Characteristic - Explanation

#C
Example:
Overflow - Security - Adding in large integers as parameters might cause the method to trigger a buffer overflow.

#D
###
Release Notes:

    * The code defines a Java class named Timesheet that is used to manage timesheet data.
    * It uses a ConcurrentHashMap to store timesheet entries, where project names (in lowercase) are associated with their respective durations (in hours).
    * The submitTimesheet method allows users to submit timesheet entries. It requires a project name and a positive duration as parameters. If these conditions are met, it stores the duration associated with the project name in the map. If not, it returns false.
    * The getTimesheets method returns a collection of all recorded timesheet durations.
    * The getTotalTimesheetHours method allows users to retrieve the total duration for a specific project name (case-insensitive). If the project name is not found, it returns 0.

Note: Ensure that project names are provided in lowercase when using the submitTimesheet method to avoid case sensitivity issues when retrieving total timesheet hours.
###

使用这个提示在 ChatGPT 中返回了以下建议的风险:

Concurrent Modification - Performance - The use of ConcurrentHashMap may not be suitable for high concurrent access, leading to performance bottlenecks if there are frequent modifications to the timesheet entries by multiple users simultaneously.

Lack of Input Validation - Security - The submitTimesheet method lacks proper input validation. It only checks for positive durations but does not validate the project name, making it vulnerable to injection attacks or other malicious input.

Case Sensitivity - Interoperability - The requirement to provide project names in lowercase may lead to interoperability issues when integrating with systems that expect case-insensitive input or have different naming conventions.

No Authentication/Authorization - Security - The release notes do not mention any authentication or authorization mechanisms, which can pose security risks if unauthorized users can submit or access timesheet data.

Data Exposure - Security - The getTimesheets method provides access to all recorded timesheet durations, which might expose sensitive information if not properly restricted or authenticated.

当然,如果我们可以直接要求 LLM 根据代码直接提出风险,这可能是一个不必要的步骤,但它确实展示了 LLM 将数据从一种格式转换为另一种格式的能力。

4.6 与代码助手保持平衡

对于像 Copilot 和 ChatGPT 这样的工具所提供的价值观取决于个人。对于一些人来说,这意味着开发的自动化和许多角色的流失,对于其他人来说,它只是一个随机选择建议代码的先进工具。本章展示的持续主题是它们的价值和用处在这两个极端之间。

这些模型是在大量来自 Stack Overflow 和 GitHub 存储库的数据上进行训练的,它们对于生产和测试代码提出了非常复杂的建议。但是它们仍然需要我们人类的指导 - 这种指导是由我们与利益相关者沟通的能力、分析需求和设计实施所引导的。我们能够多好地使用 AI 工具来进行开发取决于我们如何完善我们的辅助技能,这可以通过使用图 4.2 所示的区域效应模型进行总结。

图 4.2 一个区域效应模型,更新以展示人类技能和代码助手工具的能力

平衡的方法可以帮助我们更快地交付功能,但仍然确保我们建立质量。因此,我们的目标是在需要依赖我们自己的能力或代码助手工具的情况下保持平衡。有时,代码助手无法提供正确的实现建议,我们需要掌控。这可以给我们更多的控制,但会牺牲速度。在其他时候,我们可以依赖代码助手工具参考大量数据来通过单元测试或对话来提出新的设计理念。但我们希望确保我们的 TDD 循环专注于设计,而不是测试覆盖率。过多的单元测试会使我们失去设计的视野,最终陷入一个钩钩检查的活动中。

4.7 总结

  • 目前市场上大多数的人工智能工具都依赖于从互联网上采集的大量数据进行训练的大型语言模型。

  • LLM 是一种先进的算法,通过对我们的请求应用统计分析来确定应该回应的输出内容。

  • Copilot 是一个使用 OpenAI GPT-4 并训练于存储在 GitHub 上的代码的编码助手工具。

  • Copilot 在 IDE 中工作,通过读取您的代码作为提示来建议添加到测试代码和生产代码中的内容。

  • 像 Copilot 这样的工具可以很好地与 TDD 红/绿/重构循环配合使用,帮助我们快速创建单元测试和生产代码。

  • 为了帮助 Copilot 返回有价值的代码,我们需要通过提示对其进行引导。

  • 成功地使用人工智能代码助手取决于我们对自己的能力以及代码助手工具的功能的了解。

  • 我们的主导程度与工具主导设计之间存在一种推拉关系。

  • 当平衡从人类主导转变为工具主导时,我们必须意识到其中的权衡。

  • 26
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值