掘金 人工智能 05月19日 09:33
Amazon Q 从入门到精通 – 测试与重构
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

本文探讨了如何利用 Amazon Q Developer 提升软件开发中的单元测试效率和代码质量。通过分析“意大利面条”式代码案例,揭示了其设计缺陷及测试难点。文章展示了Amazon Q如何通过代码分析、重构建议和自动化测试用例生成,帮助开发者改善代码结构,提高可测试性和维护性,最终实现更可靠的软件开发流程。

💡 **代码可测试性挑战:** 文章首先通过一个难以测试的“意大利面条”式代码示例,展示了紧耦合、违反单一职责原则、直接依赖外部实体和缺少接口抽象等问题,这些都导致了代码难以进行单元测试。

🤖 **Amazon Q 的代码分析:** 随后,文章演示了 Amazon Q Developer 如何通过代码分析功能,识别代码中的设计和测试问题,并提供详细的问题列表,帮助开发者理解代码的缺陷所在。

🛠️ **Amazon Q 的代码重构:** 文章展示了 Amazon Q 如何根据开发者的需求,提供代码重构建议,改善代码结构,例如通过依赖注入、接口抽象等方式,提高代码的可测试性和模块化程度。

✅ **自动化测试用例生成:** 最后,文章展示了 Amazon Q 如何为重构后的代码生成单元测试用例,包括测试类的设置、测试方法的编写等,从而帮助开发者快速构建完善的测试体系。

Amazon Q Developer 是亚马逊推出的一个专为专业开发人员设计的人工智能助手,旨在提升代码开发和管理效率。其主要功能包括代码生成、调试、故障排除和安全漏洞扫描,提供一站式代码服务。

众所周知,在软件开发领域,测试代码是软件成功的重要基石。它确保应用程序是可靠的,符合质量标准,并且按预期工作。自动化软件测试有助于及早发现问题和缺陷,减少对最终用户体验和业务的影响。此外,测试本身就是一个最可靠的文档,把每个细分功能进行了明确。同时,它也是一个细化到最小功能单元的安全网,可以防止代码随时间变化而发生回归(Regression)问题。

因此,在现代软件工程实践中,经常会看到书写 100 行功能代码的同时,开发人员会同时书写 1.5 倍甚至更多的测试代码来保证功能的正确性。另外,在知名的 GitHub 开源工程中,当贡献者开启 Pull Request 时,系统就会自动运行开发者自己编写的单元测试程序。单元测试程序的好坏和执行结果,都是评审人重要的审查标准。

在这篇博客文章中,我们将展示如何通过集成像 Amazon Q Developer 这样的智能 GenAI 工具来为单元测试,自动化测试场景快速、准确地生成测试用例,并以一些实际的代码用例,来描述测试的最佳实践原则,以及 Amazon Q 如何能够在其中扮演重要的角色。

不可测试的代码

当我们追求整洁、优雅的代码的同时,像硬币总会有另一面一样,世界上总会存在着混乱,风格怪异,难以测试的“意大利面条”式的代码。

什么是“意大利面条”式的代码呢?如下所示:

class Printer:    def __init__(self):        self.printer_name = "Default Printer"    def print_document(self, content):        print(f"Printing with {self.printer_name}: {content}")        # 模拟打印操作        with open("print_history.log", "a") as f:            f.write(f"Printed: {content}\n")class Database:    def __init__(self):        self.connection = "Database Connection String"    def save_data(self, data):        print(f"Saving to database: {data}")        # 模拟数据库操作        return True    def get_data(self, query):        # 模拟从数据库获取数据        return f"Data for query: {query}"class ReportGenerator:    def __init__(self):        # 直接在构造函数中实例化依赖,这是不好的实践        self.printer = Printer()        self.database = Database()    def generate_monthly_report(self, month):        # 违反单一职责原则:既处理数据,又负责打印        print("Starting report generation...")        # 直接访问数据库        sales_data = self.database.get_data(f"SELECT * FROM sales WHERE month = {month}")        # 直接处理文件        with open(f"report_{month}.txt", "w") as f:            f.write(f"Sales Report for Month: {month}\n")            f.write(str(sales_data))        # 直接打印        self.printer.print_document(f"Monthly Report - {month}")        # 再次访问数据库保存记录        self.database.save_data({            "report_type": "monthly",            "month": month,            "status": "completed"        })    def generate_daily_report(self, date):        # 类似的混乱逻辑        daily_data = self.database.get_data(f"SELECT * FROM daily_sales WHERE date = {date}")        # 直接文件操作        with open(f"daily_report_{date}.txt", "w") as f:            f.write(f"Daily Report for: {date}\n")            f.write(str(daily_data))        # 直接打印        self.printer.print_document(f"Daily Report - {date}")        # 保存状态到数据库        self.database.save_data({            "report_type": "daily",            "date": date,            "status": "completed"        })# 使用示例if __name__ == "__main__":    report_gen = ReportGenerator()    report_gen.generate_monthly_report("2023-12")    report_gen.generate_daily_report("2023-12-26")

这段代码看上去很简单,主对象 report_gen,依赖于 printer,和 database 对象来进行打印和报表保存。甚至为了更快地得到代码所要展现的信息,可以让 Amazon Q 帮你绘制一个文字风格的时序图。如下图所示:

真的很棒!基本都不用看代码,就能知道它在做什么了,这是一个对开发者很实用的功能。

把代码执行一下,它的输入如下图所示。

接下来,让 Amazon Q 来解释一下这段代码,看看它能否找到一些问题?在 Amazon Q Chat 窗口里,输入最关注的问题,如“Can you help me find issues with the code in test.py, from design and testability perspective? don’t give suggestion, just list all of issues.”。Amazon Q 的回复如下图所示。

Amazon Q 很轻松地找到了相关的核心问题,问题不少,但本文只挑选设计和测试方面的问题如下:

-ReportGenerator 直接实例化了 printer和database。

-直接实例化导致 ReportGenerator 无法被隔离。

-因此无法注入 mock 的 printer 和 database 来对 ReportGenerator 进行测试。

-ReportGenerator 身兼数职,不但要做数据库操作和文件操作,连报表生成也都一起包揽。

-Printer 类里包含了打印和日志两项职能。

-Database 类包含了读写两类操作。

-Printer 类直接文件操作。

-ReportGenerator 类直接进行文件操作。

-Database 类的直接操作。

-Printer 类没有对应的接口抽象。

-Database 类没有对应的接口抽象。

-组件之间交互时,没有契约。

显然,这种无法做单元测试的代码,不但很难保证质量,维护起来也很麻烦,复用性也很差。

使用 Amazon Q Developer 重构代码

稍微改动一下 Amazon Q 的提示词,允许它给我们提提建议。在 Amazon Q Chat 窗口里,再次输入问题“/dev Can you help me refactor these issues? Please focus on design and testing related issues only”。Amazon Q 的回复如下图所示。

在接受了所有代码变更之后,Amazon Q Developer 为我们创建了一个主程序文件,和一个测试代码文件。

其中,test_report_generator.py 是一个测试代码文件。它的代码如下所示。

import unittestfrom unittest.mock import MagicMock, patchfrom pathlib import Pathfrom refactored_test import (    ReportGenerator,    Printer,    Database,    ReportWriter,    ReportConfig)class TestReportGenerator(unittest.TestCase):    def setUp(self):        self.printer = MagicMock()        self.database = MagicMock()        self.report_writer = MagicMock()        self.report_generator = ReportGenerator(            printer=self.printer,            database=self.database,            report_writer=self.report_writer        )    def test_generate_monthly_report(self):        # Arrange        month = "2023-12"        test_data = "Test sales data"        self.database.get_data.return_value = test_data        # Act        self.report_generator.generate_monthly_report(month)        # Assert        self.database.get_data.assert_called_once()        self.report_writer.write_report.assert_called_once()        self.printer.print_document.assert_called_once_with(f"Monthly Report - {month}")        self.database.save_data.assert_called_once()    def test_generate_daily_report(self):        # Arrange        date_str = "2023-12-25"        test_data = "Test daily data"        self.database.get_data.return_value = test_data        # Act        self.report_generator.generate_daily_report(date_str)        # Assert        self.database.get_data.assert_called_once()        self.report_writer.write_report.assert_called_once()        self.printer.print_document.assert_called_once_with(f"Daily Report - {date_str}")        self.database.save_data.assert_called_once()class TestReportWriter(unittest.TestCase):    def setUp(self):        self.test_dir = Path("test_reports")        self.config = ReportConfig(            output_directory=self.test_dir,            log_file=Path("test_logs/print_history.log")        )        self.report_writer = ReportWriter(self.config)    def tearDown(self):        # Clean up test files        if self.test_dir.exists():            for file in self.test_dir.glob("*"):                file.unlink()            self.test_dir.rmdir()    def test_write_report(self):        # Arrange        filename = "test_report.txt"        content = "Test report content"        # Act        self.report_writer.write_report(filename, content)        # Assert        report_path = self.test_dir / filename        self.assertTrue(report_path.exists())        self.assertEqual(report_path.read_text(), content)if __name__ == '__main__':    unittest.main()

主程序文件(refactored_test.py)代码如下。

from abc import ABC, abstractmethodfrom typing import Protocolfrom dataclasses import dataclassimport osfrom datetime import datefrom pathlib import Pathclass PrinterInterface(Protocol):    def print_document(self, content: str) -> None:        ...class DatabaseInterface(Protocol):    def save_data(self, data: dict) -> bool:        ...    def get_data(self, query: str) -> str:        ...@dataclassclass ReportConfig:    output_directory: Path    log_file: Pathclass Printer:    def __init__(self, name: str = "Default Printer", log_file: Path = Path("print_history.log")):        self.printer_name = name        self.log_file = log_file    def print_document(self, content: str) -> None:        print(f"Printing with {self.printer_name}: {content}")        # Safely handle file operations        self.log_file.parent.mkdir(parents=True, exist_ok=True)        with open(self.log_file, "a") as f:            f.write(f"Printed: {content}\n")class Database:    def __init__(self, connection_string: str = "Database Connection String"):        self.connection = connection_string    def save_data(self, data: dict) -> bool:        print(f"Saving to database: {data}")        return True    def get_data(self, query: str) -> str:        return f"Data for query: {query}"class ReportWriter:    def __init__(self, config: ReportConfig):        self.config = config    def write_report(self, filename: str, content: str) -> None:        filepath = self.config.output_directory / filename        filepath.parent.mkdir(parents=True, exist_ok=True)        with open(filepath, "w") as f:            f.write(content)class ReportGenerator:    def __init__(        self,        printer: PrinterInterface,        database: DatabaseInterface,        report_writer: ReportWriter    ):        self.printer = printer        self.database = database        self.report_writer = report_writer    def generate_monthly_report(self, month: str) -> None:        print("Starting monthly report generation...")                # Get data        sales_data = self.database.get_data(f"SELECT * FROM sales WHERE month = {month}")                # Generate report content        report_content = self._format_monthly_report(month, sales_data)                # Write report        self.report_writer.write_report(f"report_{month}.txt", report_content)                # Print report        self.printer.print_document(f"Monthly Report - {month}")                # Log completion        self._log_report_completion("monthly", month=month)    def generate_daily_report(self, date_str: str) -> None:        print("Starting daily report generation...")                # Get data        daily_data = self.database.get_data(f"SELECT * FROM daily_sales WHERE date = {date_str}")                # Generate report content        report_content = self._format_daily_report(date_str, daily_data)                # Write report        self.report_writer.write_report(f"daily_report_{date_str}.txt", report_content)                # Print report        self.printer.print_document(f"Daily Report - {date_str}")                # Log completion        self._log_report_completion("daily", date=date_str)    def _format_monthly_report(self, month: str, data: str) -> str:        return f"Sales Report for Month: {month}\n{data}"    def _format_daily_report(self, date_str: str, data: str) -> str:        return f"Daily Report for: {date_str}\n{data}"    def _log_report_completion(self, report_type: str, **kwargs) -> None:        completion_data = {            "report_type": report_type,            "status": "completed",            **kwargs        }        self.database.save_data(completion_data)# Example usage:def create_report_system(    output_dir: str = "reports",    log_file: str = "logs/print_history.log") -> tuple[ReportGenerator, PrinterInterface, DatabaseInterface, ReportWriter]:    config = ReportConfig(        output_directory=Path(output_dir),        log_file=Path(log_file)    )        printer = Printer(log_file=config.log_file)    database = Database()    report_writer = ReportWriter(config)    report_generator = ReportGenerator(printer, database, report_writer)        return report_generator, printer, database, report_writer

重构后的代码,主要的变更和好处如下:

一图胜千言,为了更好地理解重构带来的变化,可以再次让 Amazon Q Developer 来图文结合地进行描述和总结,输入提示词,“Can you show the importance of introducing abstract interface than before in ASCII-style diagram?”,Amazon Q Developer 将用文字版图形来描述重构里引入抽象接口起到的关键作用。

通过简单/直接的自然语言交互,在分钟级别的时间范围内,Amazon Q Developer 便完成了对不良设计的重构,把遵循良好设计的代码呈现在开发者的面前。

快捷的单元测试生成方式

如果开发者当下的任务是节约编写单元测试的精力和时间,除了使用/dev 来进行代码重构外,Amazon Q Developer 提供了专门的/test 命令

打开要编写单元测试的文件,在 Amazon Q Developer 的 Chat 窗口里输入 /test,即可开始编写单元测试代码,如下图所示。

单元测试代码创建中,会显示进度。如下图所示。

最终,和使用/dev 一样,Amazon Q Developer 不会直接变更代码,而是给出一个临时的变更结果给开发者,开发者可以以 diff 的形式进行查看,并决定是接受,还是拒绝。

就是如此简单,开发者就可以完成之前繁琐的创建单元测试的工作。

不仅如此,当业务代码不断随着市场需求发生频繁变化的时候,开发者将可以随时以智能化、自动化的方式,让 Amazon Q Developer 协助生成最新的单元测试代码,让单元测试能够提供精确代码质量保证的同时,不再产生高昂的维护代价!

最后

本文以一个“意大利面条式”的,充满了不良设计的代码为样例,展示了 Amazon Q Developer 如何能够以简单/精炼的自然语言交互的方式,短时间内帮助开发者完成代码重构和自动化测试用例的编写,在确保代码质量的同时,大大降低了测试代码的维护成本。

*前述特定亚马逊云科技生成式人工智能相关的服务仅在亚马逊云科技海外区域可用,亚马逊云科技中国仅为帮助您了解行业前沿技术和发展海外业务选择推介该服务。

本篇作者

本期最新实验为《Agentic AI 帮你做应用 —— 从0到1打造自己的智能番茄钟

✨ 自然语言玩转命令行,10分钟帮你构建应用,1小时搞定新功能拓展、测试优化、文档注释和部署

💪 免费体验企业级 AI 开发工具,质量+安全全掌控

⏩️[点击进入实验] 即刻开启 AI 开发之旅

构建无限, 探索启程!

Fish AI Reader

Fish AI Reader

AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

FishAI

FishAI

鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

联系邮箱 441953276@qq.com

相关标签

Amazon Q Developer 单元测试 代码重构 GenAI
相关文章