文章提出Transcript Testing方法,帮助人类核查AI生成的代码
来源标题:Transcript Testing: Helping Humans Verify AI Code
针对AI编程工具生成的测试代码量大且难以理解的问题,文章提出Transcript Testing方法,主张测试应显式地面向人类阅读,以帮助开发者验证和理解AI构建的系统。作者指出,过去半年AI生成的PR中测试覆盖已不再是问题,反而出现数百行难以读懂的测试代码,常被人工审查时略过。
以下正文由 AI 翻译自 Hacker News 最新(原文版权归原站所有),可能存在偏差,重要内容请以原文为准。
在AI时代,软件测试应当明确地以方便人类阅读的方式来编写,帮助我们验证并理解我们正在构建的系统。随着AI工具让我们越来越少地用编程语言写代码,这一点变得更加重要。本文提出“Transcript Testing”来应对这一问题。
关于人类在实现和维护软件系统中未来的角色,目前有很多猜测,但至少在短期内,共识是人类需要验证AI的输出。传统上,软件工程中有很大一部分工作是创建和维护软件测试,既为了防止回归,也为了精确记录系统的预期行为。审查人类编写的PR时,其中一项工作就是确保测试覆盖充分(通常并不充分)。而在过去6个月里,我没有见过任何一个测试不充分的PR(这要归功于AI编码工具)。现在我的问题反过来了:PR里包含数百行难以理解的测试。事实上,这些测试可读性和可理解性太差,我怀疑在人工审查时它们经常被草草略过,甚至直接被跳过(我承认自己偶尔也这样)。如果我们不理解(或者提不起精神去读)agent写的测试,我们又怎么能验证自己负责的系统呢🤔?
下面是一个典型的AI生成测试,来自Apache DataFusion一个近期PR的初始提交,该PR优化了Parquet reader读取Bloom filter的方式(不知道这是什么意思也没关系)。1 这个PR包含四个测试函数(总共约400行),覆盖了row group内容、writer设置和谓词的各种组合。
#[tokio::test]
async fn incremental_bloom_filter_pruning_short_circuits_remaining_columns() {
let store = Arc::new(InMemory::new()) as Arc<dyn ObjectStore>;
// Two row groups, each with columns a and b:
// RG-0: a = [99, 98, 97], b = [900, 980, 970] — no predicate values present
// RG-1: a = [10, 20, 30], b = [100, 200, 300] — contains predicate match
//
// Statistics pruning is disabled throughout; Bloom filters are the only pruning
// mechanism. Decoder-level filter push-down is enabled so matching rows are
// physically filtered out of RG-1 where possible.
let batch_rg0 = record_batch!(
("a", Int32, vec![99_i32, 98, 97]),
("b", Int32, vec![900_i32, 980, 970])
)
.unwrap();
let batch_rg1 = record_batch!(
("a", Int32, vec![10_i32, 20, 30]),
("b", Int32, vec![100_i32, 200, 300])
)
.unwrap();
let schema = batch_rg0.schema();
let props = WriterProperties::builder()
// Force each batch into its own row group.
.set_max_row_group_row_count(Some(3))
.set_bloom_filter_enabled(true)
.set_statistics_enabled(EnabledStatistics::None)
.build();
let data_size = write_parquet_batches(
Arc::clone(&store),
"multi_col_bloom.parquet",
vec![batch_rg0, batch_rg1],
Some(props),
)
.await;
let file =
PartitionedFile::new("multi_col_bloom.parquet".to_string(), data_size as u64);
// ── Query 1: a = 999 AND b = 100 ─────────────────────────────────────────────
// 'a' Bloom filter is checked first for each row group. 999 is absent from
// both, so both groups are pruned after reading only the 'a' filter — the 'b'
// filter is never fetched (short-circuit).
let pred1 =
logical2physical(&col("a").eq(lit(999)).and(col("b").eq(lit(100))), &schema);
let metrics1 = ExecutionPlanMetricsSet::new();
let morselizer1 = ParquetMorselizerBuilder::new()
.with_store(Arc::clone(&store))
.with_schema(Arc::clone(&schema))
.with_predicate(Arc::clone(&pred1))
.with_pushdown_filters(true)
.with_row_group_stats_pruning(false)
.with_enable_bloom_filter(true)
.with_metrics(metrics1.clone())
.build();
let stream1 = open_file(&morselizer1, file.clone()).await.unwrap();
let bytes_scanned1 = counter_metric_value(&metrics1, "bytes_scanned");
assert_eq!(collect_int32_values(stream1).await, Vec::<i32>::new());
let (pruned1, matched1) = bloom_filter_pruning_metrics(&metrics1);
// Both row groups pruned; no row group survived Bloom evaluation.
assert_eq!(pruned1, 2);
assert_eq!(matched1, 0);
// ── Query 2: a = 10 AND b = 999 ──────────────────────────────────────────────
// RG-0: 'a' filter doesn't contain 10 → pruned immediately (no 'b' read).
// RG-1: 'a' filter contains 10 → must read 'b'; 'b' filter doesn't contain 999
// → pruned on the second column.
let pred2 =
logical2physical(&col("a").eq(lit(10)).and(col("b").eq(lit(999))), &schema);
let metrics2 = ExecutionPlanMetricsSet::new();
let morselizer2 = ParquetMorselizerBuilder::new()
.with_store(Arc::clone(&store))
.with_schema(Arc::clone(&schema))
.with_predicate(Arc::clone(&pred2))
.with_pushdown_filters(true)
.with_row_group_stats_pruning(false)
.with_enable_bloom_filter(true)
.with_metrics(metrics2.clone())
.build();
let stream2 = open_file(&morselizer2, file.clone()).await.unwrap();
let bytes_scanned2 = counter_metric_value(&metrics2, "bytes_scanned");
assert_eq!(collect_int32_values(stream2).await, Vec::<i32>::new());
let (pruned2, matched2) = bloom_filter_pruning_metrics(&metrics2);
assert_eq!(pruned2, 2);
assert_eq!(matched2, 0);
// Query 2 must read both 'a' and 'b' filters for RG-1, so it scans strictly
// more Bloom filter bytes than Query 1 (which short-circuits on 'a' alone).
assert!(bytes_scanned1 > 0, "expected non-zero bloom filter I/O");
assert!(
bytes_scanned2 > bytes_scanned1,
"query 2 should scan more bloom filter bytes due to loading 'b' filter for RG-1"
);
// ── Query 3: a = 10 (one row group pruned, one matched) ───────────────────────
// RG-0: 'a' filter doesn't contain 10 → pruned.
// RG-1: 'a' filter contains 10 → matched (not pruned); the actual matching
// rows [10, 20, 30] are returned through decoder-level filtering.
let pred3 = logical2physical(&col("a").eq(lit(10)), &schema);
let metrics3 = ExecutionPlanMetricsSet::new();
let morselizer3 = ParquetMorselizerBuilder::new()
.with_store(Arc::clone(&store))
(原文较长,此处译出前 6000 字,完整内容请点「打开原文」。)
正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。