返回资讯中心
外部精选
软件工程
#开发者实时流

文章提出Transcript Testing方法,帮助人类核查AI生成的代码

来源标题:Transcript Testing: Helping Humans Verify AI Code

针对AI编程工具生成的测试代码量大且难以理解的问题,文章提出Transcript Testing方法,主张测试应显式地面向人类阅读,以帮助开发者验证和理解AI构建的系统。作者指出,过去半年AI生成的PR中测试覆盖已不再是问题,反而出现数百行难以读懂的测试代码,常被人工审查时略过。

Hacker News 最新qianli_cs2026/11/13 · 今天30 分钟阅读

以下正文由 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 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。

打开原文
去社区讨论 →