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
解析: InternalsVisibleTo 把 internal 可见性扩展到指定的友元程序集,是测试访问内部实现的常规机制(无需反射)。它不要求强命名(强命名时需附带公钥),是编译期可见性决策而非运行时开关。“测试只能测 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() 的火花任务被丢弃:断言立即执行,异步工作还在排队;即使它失败,测试也早已通过。修复是让测试返回 Task 并 await,或至少 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++ 这类条件会真实修改程序状态,导致“调试本身改变行为”。条件应保持为无副作用的纯表达式。
关键点: 断点条件在被调试进程内求值,副作用危险。
0970 在 Process(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.ReportDiagnostic,SupportedDiagnostics可以为空 - 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.Length抛NullReferenceException
查看答案与解析
正确答案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 恢复原设置。