返回题库高级 .NET 刷题C# 选择题 · 第 49 / 50 篇

C# 试题 49:测试、调试与分析器

0961 关于以下测试的判断,哪项正确?

难度: 实战

[Fact]
public void CanOrder_WithFakeRepo()
{
    var repo = new FakeOrderRepo();          // 内存替身
    var service = new OrderService(repo);    // 被测单元
    Assert.True(service.CanOrder(1));
}
  • A. 属于集成测试,因为引入了 OrderService
  • B. 是合格单元测试:被测单元通过替身隔离,无 I/O、网络与共享状态,快速且确定
  • C. 是反模式,单元测试不能注入依赖
  • D. 编译失败,测试必须访问真实数据源
查看答案与解析

正确答案B

解析: 单元测试的边界是“被测单元 + 替身”:这里用内存 FakeOrderRepo 替掉仓储,不碰数据库与网络,行为确定、执行快,符合单元测试的隔离要求。单元测试不是“不能有依赖”,而是“依赖必须被替身隔离”。

关键点: 替身隔离 I/O 是单元测试成立的边界。

0962 以下测试的定位是什么?

难度: 实战

[Fact]
public void PlaceOrder_WithRealDatabase()
{
    var db = new SqlConnection("Server=prod;Database=orders");
    var service = new OrderService(db);
    var result = service.PlaceOrder(1);
    Assert.NotNull(result);
}
  • A. 这是典型单元测试
  • B. 编译失败,测试方法不能访问数据库
  • C. 这是快照测试
  • D. 这是集成测试特征:依赖真实数据库,不满足单元测试的隔离与无 I/O 边界
查看答案与解析

正确答案D

解析: 直接连接真实数据库意味着测试结果取决于外部状态(数据内容、网络、连接串),既不隔离也不确定,属于集成测试。单元测试应通过抽象(接口/仓储替身)切断这类依赖;把“能连数据库”当单元测试是团队最常见的归类错误。

关键点: 真实数据库依赖 = 集成测试,不是单元测试。

0963 以下测试存在什么问题?

难度: 进阶

public static class Counter
{
    public static int Value;
}

[Fact]
public void Test1()
{
    Counter.Value = 1;
}

[Fact]
public void Test2()
{
    Assert.Equal(1, Counter.Value);   // 依赖 Test1 先执行
}
  • A. 共享可变静态状态使测试顺序敏感:单独运行 Test2 会失败,属于测试坏味道
  • B. 没有问题,[Fact] 保证执行顺序
  • C. 编译失败,静态字段不能跨测试访问
  • D. 两个测试必然都通过
查看答案与解析

正确答案A

解析: 测试框架默认不保证方法执行顺序,且可能并行运行。Test2 依赖 Test1 写下的静态状态,单独运行或在并行环境中必然失败——这是“顺序耦合测试”。修复方式是每测前重置状态(set up/fixture),或把状态放进测试实例。

关键点: 测试之间共享可变静态状态 = 顺序敏感,应避免。

0964 关于以下程序集特性的使用,哪项正确?

难度: 进阶

// 生产程序集 AssemblyInfo.cs
[assembly: InternalsVisibleTo("MyApp.Tests")]
  • A. 语法错误,该特性不存在
  • B. 只有强命名程序集才能使用
  • C. 这是常见做法:允许测试程序集访问生产代码的 internal 成员,便于按单元边界做白盒测试
  • D. 会降低程序集安全性,且只在运行时生效
查看答案与解析

正确答案C

解析: InternalsVisibleTointernal 可见性扩展到指定的友元程序集,是测试访问内部实现的常规机制(无需反射)。它不要求强命名(强命名时需附带公钥),是编译期可见性决策而非运行时开关。“测试只能测 public”是常见误解。

关键点: InternalsVisibleTo 让测试程序集成为内部成员的友元。

0965 以下测试方法的问题是什么?

难度: 进阶

[Fact]
public async void BadAsyncTest()
{
    await Task.Delay(10);
    throw new InvalidOperationException("boom");
}
  • A. 编译失败,测试方法不能声明为 async void
  • B. 测试正常失败,异常被框架捕获
  • C. 测试必然通过
  • D. async void 测试方法在 await 之后无法被框架等待,异常成为未观察异常,测试宿主可能崩溃或异常丢失
查看答案与解析

正确答案D

解析: async void 返回后测试框架无法拿到任务去 await:方法在第一个 await 处立即返回,后续异常没有可观察的任务,直接成为未观察异常(进入线程池,可能让测试宿主崩溃或悄悄丢失)。测试方法必须返回 Task(或框架支持的异步类型),异常才能被捕获并转为失败。

关键点: async void 测试丢异常,测试方法必须返回 Task。

0966 以下测试方法的问题是什么?

难度: 实战

[Fact]
public void FireAndForget()
{
    _ = DoWorkAsync();      // 未 await
    Assert.True(true);
}
  • A. 可靠,断言在异步工作完成后执行
  • B. 测试可能在异步工作完成前结束,异步中的失败不会被测试框架看到
  • C. 编译失败
  • D. 必然超时
查看答案与解析

正确答案B

解析: 测试方法本身不是异步的,DoWorkAsync() 的火花任务被丢弃:断言立即执行,异步工作还在排队;即使它失败,测试也早已通过。修复是让测试返回 Taskawait,或至少 await 该任务并断言完成。

关键点: 未 await 的异步测试会“假绿”,失败被吞掉。

0967 以下测试的运行结果是什么?

难度: 实战

[Fact]
public async Task CorrectAsyncTest()
{
    await Task.Delay(10);
    Assert.Equal(2, 1 + 1);
}
  • A. 测试通过:异步测试返回 Task,框架会等待其完整执行
  • B. 编译失败,测试方法不能返回 Task
  • C. 测试失败
  • D. 运行抛 InvalidOperationException
查看答案与解析

正确答案A

解析: 正确的异步测试形态是返回 Task(或 ValueTask)的 async 方法:框架 await 测试任务,Assert 失败会包装成任务异常并转为测试失败。与 0965/0966 对比,差别只在“返回值能否被框架等待”。

关键点: 异步测试的标准形态是 async Task

0968 关于异步测试的写法,哪项正确?

难度: 进阶

  • A. async void 测试与返回 Task 的测试完全等价
  • B. 异步测试必须使用 Thread.Sleep 等待
  • C. 测试方法应返回 Task 让框架 await 完整执行;async void 的异常无法被捕获,未 await 的任务会丢失失败信息
  • D. 异步测试中不能使用断言
查看答案与解析

正确答案C

解析: 异步测试的核心是“让框架拿到可等待的句柄”:返回 Task 才能把异常与失败回传;async void 无句柄,未 await 的任务无观察者。Thread.Sleep 会阻塞线程池线程且不参与异步协作,不是替代方案。

关键点: 可等待的返回类型是异步测试正确性的前提。

0969 关于条件断点的条件求值,哪项正确?

难度: 进阶

  • A. 条件表达式在调试器进程内求值,不影响程序
  • B. 条件表达式在被调试进程内求值,带副作用的表达式(如 count++)会真实改变程序状态
  • C. 条件断点在 Release 构建下不生效
  • D. 条件为 false 时也会暂停
查看答案与解析

正确答案B

解析: 条件断点的条件由被调试进程在每次命中时求值——它运行在目标程序的上下文中,能访问其变量,也会执行副作用。写 count++ 这类条件会真实修改程序状态,导致“调试本身改变行为”。条件应保持为无副作用的纯表达式。

关键点: 断点条件在被调试进程内求值,副作用危险。

0970Process(i); 行设置条件断点 (count++) == 5,调试行为是什么?

难度: 实战

int count = 0;

for (int i = 0; i < 10; i++)
{
    Process(i);   // 断点条件:(count++) == 5
}
  • A. 只影响调试器,count 不会被修改
  • B. 表达式非法,无法设置该条件
  • C. 断点永远不会命中
  • D. 每次命中都执行 count++,第 6 次命中暂停,同时 count 变为 6,可能改变后续程序逻辑
查看答案与解析

正确答案D

解析: 条件 (count++) == 5 每次命中都自增 count:第 1~5 次条件为 false(不暂停但已自增),第 6 次命中时先 count++ 再比较,6 == 5 为 false……实际永远不会因该条件暂停,而 count 被改成了 6。这类“副作用条件”既难命中又污染状态,是调试事故高发点。

关键点: 断点条件中的自增会真实执行,条件本身也不按直觉工作。

0971 关于条件断点、命中次数与“当命中时”操作,哪项正确?

难度: 进阶

  • A. 条件断点每次命中都要求值条件,存在性能开销;命中次数按命中计数触发;“当命中时”操作(记录日志等)不暂停执行
  • B. 条件断点不执行任何代码,零开销
  • C. 命中次数断点不能用于循环
  • D. 条件断点只能在方法入口处设置
查看答案与解析

正确答案A

解析: 三种机制各司其职:条件(每次命中求值表达式)、命中次数(按命中计数决定是否暂停)、操作(命中时执行日志等动作但不中断)。条件求值发生在被调试进程中,热点循环里的大量命中会显著拖慢调试,应优先用命中次数或缩小范围。

关键点: 条件有求值成本,命中次数按计数触发,操作不暂停。

0972 关于 Release 构建下的调试,哪项正确?

难度: 进阶

// Release 构建下的方法
public static int Compute(int x)
{
    return x * 2;   // 行断点可能因内联/优化而无法命中或位置漂移
}
  • A. Release 与 Debug 下断点行为完全一致
  • B. 行断点在 Release 下一定命中
  • C. Release 的优化(内联、寄存器化、语句重排)可能导致行断点/条件断点行为与 Debug 不同,调试应优先使用 Debug 构建
  • D. 条件断点在 Release 下会自动转成普通断点
查看答案与解析

正确答案C

解析: Release 开启优化:方法可能被内联、局部变量进寄存器、语句重排,行号映射与变量可见性都会变化,断点可能不命中或落在“不对的行”,条件断点引用的变量可能已不存在。排障时用 Debug 构建获得可靠的单步与断点语义,Release 仅用于验证性能与线上行为。

关键点: 优化构建会破坏断点可靠性,调试用 Debug 构建。

0973 关于以下分析器的运行模型,哪项正确?

难度: 实战

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class MyAnalyzer : DiagnosticAnalyzer
{
    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; }
    public override void Initialize(AnalysisContext context) { }
}
  • A. 分析器在运行时由 JIT 调用
  • B. 分析器在编译期随编译器管线运行(IDE 与构建均可),可访问语法树与语义模型,但不能直接改写源代码
  • C. 分析器只能报告语法错误
  • D. 分析器必须配合 UnmanagedCallersOnly 才能运行
查看答案与解析

正确答案B

解析: Roslyn 分析器挂载在编译管线中:IDE 打开文件时与 dotnet build 时都会运行,输入是语法树与语义模型,输出是 Diagnostic。它们不修改代码——改写源码是 CodeFixProvider 的职责。把“分析器能自动修代码”当默认认知是常见的架构误解。

关键点: 分析器 = 编译期只读检查,修复交给 CodeFixProvider。

0974 关于分析器“如何产生诊断”,哪项正确?

难度: 进阶

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class MyAnalyzer : DiagnosticAnalyzer
{
    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; }

    public override void Initialize(AnalysisContext context)
    {
        context.RegisterSyntaxNodeAction(Analyze, SyntaxKind.InvocationExpression);
    }

    private static void Analyze(SyntaxNodeAnalysisContext ctx) { }
}
  • A. 分析器自动分析全部代码,无需注册任何回调
  • B. 在 Initialize 中返回诊断数组即可
  • C. 回调只需调用 ctx.ReportDiagnosticSupportedDiagnostics 可以为空
  • D. 必须注册回调(如 RegisterSyntaxNodeAction)并在回调中调用 ctx.ReportDiagnostic,且 SupportedDiagnostics 包含对应描述符,否则诊断不会出现
查看答案与解析

正确答案D

解析: 分析器的三要素缺一不可:Initialize 注册动作(告诉编译器关注哪些节点/符号)、回调里 ctx.ReportDiagnostic(...) 报告、SupportedDiagnostics 声明描述符(供宿主过滤与展示)。只写空壳 Initialize 或只声明描述符都不会产生诊断。

关键点: 注册回调 + 报告诊断 + 声明描述符,三者共同生效。

0975 关于分析器回调的并发模型,哪项正确?

难度: 进阶

public override void Initialize(AnalysisContext context)
{
    context.RegisterSyntaxNodeAction(Analyze, SyntaxKind.InvocationExpression);
}
  • A. 编译器可能并行调用 Analyze(按语法树/符号分区),共享可变状态是竞态错误
  • B. 回调总是单线程串行执行
  • C. 可以用静态可变字典缓存结果而无需同步
  • D. 回调内不能访问语义模型
查看答案与解析

正确答案A

解析: Roslyn 会对多个语法树/符号并行执行分析回调以加速编译,因此回调必须无共享可变状态或自行同步;静态缓存、计数器等都是竞态温床。语义模型在回调内可用,但要通过 SyntaxNodeAnalysisContext 获取,避免重复构建整树模型。

关键点: 分析器回调可能并行执行,必须线程安全。

0976 关于诊断与代码修复的分工,哪项正确?

难度: 进阶

[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(Fix))]
public sealed class Fix : CodeFixProvider
{
    public override ImmutableArray<string> FixableDiagnosticIds { get; }
    public override Task RegisterCodeFixesAsync(CodeFixContext context) => Task.CompletedTask;
}
  • A. DiagnosticAnalyzer 直接改写源文件完成修复
  • B. CodeFixProvider 负责报告诊断
  • C. 分析器只报告诊断;CodeFixProvider 针对 FixableDiagnosticIds 提供 CodeAction 修复方案,两者在编译期管线中协作
  • D. 代码修复只能在运行时执行
查看答案与解析

正确答案C

解析: 职责分离:分析器产出诊断(定位+描述符),代码修复提供者订阅诊断 ID 并给出 CodeAction(含文档修改),IDE 展示为“快速修复”。CodeAction 的修改在 IDE 中应用,构建时分析器只报告不落盘。理解这条边界才能写出可维护的规则与修复。

关键点: 分析器报问题,CodeFixProvider 给方案,互不越权。

0977 以下代码产生什么编译结果?

难度: 实战

#nullable enable

string s = null;
Console.WriteLine(s.Length);
  • A. 编译通过且无任何警告
  • B. 产生编译警告(默认情况下):赋值处 CS8600,解引用处 CS8602
  • C. 编译错误,可空警告默认就是错误
  • D. 运行抛 NullReferenceException,但编译无任何提示
查看答案与解析

正确答案B

解析: 启用 NRT 后,把 null 赋给 string 报 CS8600 警告,对可能为 null 的值解引用报 CS8602 警告。可空警告默认只是警告不是错误,除非项目用 <TreatWarningsAsErrors><WarningsAsErrors> 提升。它提示风险但不阻止编译,这是常见的治理杠杆。

关键点: 可空警告默认是警告,可配置提升为错误。

0978 以下代码的行为是什么?

难度: 进阶

#nullable enable

static string? Maybe() => null;

string s = Maybe()!;
Console.WriteLine(s.Length);
  • A. 编译警告,! 不能抑制可空警告
  • B. ! 在运行时把 null 转换成空字符串
  • C. 编译失败
  • D. 编译无警告(! 空包容运算符抑制检查),但运行时 s 仍是 null,s.LengthNullReferenceException
查看答案与解析

正确答案D

解析: !(null-forgiving)只影响编译器的空状态分析,把“可能 null”声明为“视为非 null”以消除警告;它不生成任何运行时检查,也不改变值。Maybe() 仍返回 null,s.Length 照常抛 NullReferenceException。把它当“运行时判空”是致命误解。

关键点: ! 只抑制警告,不改变运行时行为。

0979 以下代码中 s.Length 是否产生可空警告?

难度: 进阶

static bool TryGet(out string? value)
{
    value = "ok";
    return true;
}

if (TryGet(out var s))
{
    Console.WriteLine(s.Length);   // 此处是否产生可空警告?
}
  • A. 产生警告:out string? 不会因返回 true 自动收窄,需用 [NotNullWhen(true)] 标注返回值与 out 参数的关系
  • B. 无警告,编译器自动收窄
  • C. 编译失败,out 参数不能声明为可空
  • D. 运行抛异常
查看答案与解析

正确答案A

解析: 编译器不知道 TryGet 返回 true 时 value 一定非空,out var s 的类型仍是 string?s.Length 报可空解引用警告。要消除警告并保留契约,需在方法上标注 [return: NotNullWhen(true)](作用于 bool 返回值),把“true ⇒ out 非空”告诉编译器。这是 TryGet 模式的标准标注。

关键点: out 参数收窄需要 [NotNullWhen] 显式声明契约。

0980 以下代码三处赋值各产生什么结果?

难度: 实战

#nullable enable
string a = null;          // ①

#nullable disable
string b = null;          // ②

#nullable restore
string c = null;          // ③
  • A. 三处都有可空警告
  • B. 只有 ② 有可空警告
  • C. ① 与 ③ 有可空警告,② 被 #nullable disable 抑制
  • D. 三处都没有可空警告
查看答案与解析

正确答案C

解析: #nullable enable/disable/restore 按区域切换 NRT 状态:① 在 enable 区域有警告,② 在 disable 区域被抑制,③ restore 回到文件/项目设置(此处为 enable)所以又有警告。逐区域禁用便于迁移存量代码,但会留下“局部失明”的盲区,属于临时手段。

关键点: #nullable 指令按区域控制警告,restore 恢复原设置。

官方资料

当前分类

C# 选择题

查看全部分类 →
  1. 46C# 试题 46:C# 14 新特性20 题
  2. 47C# 试题 47:unsafe 与原生互操作20 题
  3. 48C# 试题 48:装箱、闭包与性能20 题
  4. 49C# 试题 49:测试、调试与分析器20 题
  5. 50C# 试题 50:综合代码阅读与易错点20 题
ESC

输入关键词开始搜索