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 Truedef 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 unittest
from unittest.mock import MagicMock, patch
from pathlib import Path
from 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):# Arrangemonth = "2023-12"test_data = "Test sales data"self.database.get_data.return_value = test_data# Actself.report_generator.generate_monthly_report(month)# Assertself.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):# Arrangedate_str = "2023-12-25"test_data = "Test daily data"self.database.get_data.return_value = test_data# Actself.report_generator.generate_daily_report(date_str)# Assertself.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 filesif self.test_dir.exists():for file in self.test_dir.glob("*"):file.unlink()self.test_dir.rmdir()def test_write_report(self):# Arrangefilename = "test_report.txt"content = "Test report content"# Actself.report_writer.write_report(filename, content)# Assertreport_path = self.test_dir / filenameself.assertTrue(report_path.exists())self.assertEqual(report_path.read_text(), content)if __name__ == '__main__':unittest.main()
主程序文件(refactored_test.py)代碼如下。
from abc import ABC, abstractmethod
from typing import Protocol
from dataclasses import dataclass
import os
from datetime import date
from 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:...@dataclass
class ReportConfig:output_directory: Pathlog_file: Pathclass Printer:def __init__(self, name: str = "Default Printer", log_file: Path = Path("print_history.log")):self.printer_name = nameself.log_file = log_filedef print_document(self, content: str) -> None:print(f"Printing with {self.printer_name}: {content}")# Safely handle file operationsself.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_stringdef save_data(self, data: dict) -> bool:print(f"Saving to database: {data}")return Truedef get_data(self, query: str) -> str:return f"Data for query: {query}"class ReportWriter:def __init__(self, config: ReportConfig):self.config = configdef write_report(self, filename: str, content: str) -> None:filepath = self.config.output_directory / filenamefilepath.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 = printerself.database = databaseself.report_writer = report_writerdef generate_monthly_report(self, month: str) -> None:print("Starting monthly report generation...")# Get datasales_data = self.database.get_data(f"SELECT * FROM sales WHERE month = {month}")# Generate report contentreport_content = self._format_monthly_report(month, sales_data)# Write reportself.report_writer.write_report(f"report_{month}.txt", report_content)# Print reportself.printer.print_document(f"Monthly Report - {month}")# Log completionself._log_report_completion("monthly", month=month)def generate_daily_report(self, date_str: str) -> None:print("Starting daily report generation...")# Get datadaily_data = self.database.get_data(f"SELECT * FROM daily_sales WHERE date = {date_str}")# Generate report contentreport_content = self._format_daily_report(date_str, daily_data)# Write reportself.report_writer.write_report(f"daily_report_{date_str}.txt", report_content)# Print reportself.printer.print_document(f"Daily Report - {date_str}")# Log completionself._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
重構后的代碼,主要的變更和好處如下:
-
定義了接口協議類-PrinterInterface 定義了打印機的接口,而 Printer 是它的一個具體的實現。給予這種設計,可以有更多的實現,比如 pdf 打印機,激光打印機等等。-DatabaseInterface 定義數據庫的接口,而 Database 是它的一個具體的實現,基于這種設計,可以有更多的實現,比如內存型數據庫、文件型數據庫、關系型數據庫等等。-可以很容易地升級/替換 Printer 和 Database 的實現代碼,而不影響 ReportGenerator 本身的功能。
-
增加了系統的契約-ReportGenerator 不依賴于具體的實現,而是依賴于契約(接口)-基于接口的設計,可以非常容易地置換為 Mock 的實現,來進行充分的測試。-有了契約,就有了可測試性。
一圖勝千言,為了更好地理解重構帶來的變化,可以再次讓 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 開發之旅
構建無限, 探索啟程!