0Pricing
C# Academy · 课时

调试失败的测试

让失败可重现,移除时间和随机性,将问题缩减为最小案例,并改进断言消息以便快速诊断。

调试失败的测试 是 CoddyKit 上的免费 C# Academy 课时。 这是第 3 节课,共 3 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 C# Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 C# Academy 课程共包含 3 节课。

解决问题的计划

解决失败测试的计划:

  • 可靠地重现
  • 消除不稳定性(时间/随机性 I/O)
  • 缩小到最小的失败案例
  • 添加清晰的断言消息

设计上不稳定

使用 UtcNow 和默认的 Random 会使每次运行的行为发生变化 → 测试不稳定。

using System;

// Demo: occasionally fails because it depends on current second and default Random()
public static class Promo
{
  // Gives bonus on even seconds; random adds small jitter
  public static int BonusNow()
  {
    int baseBonus = (DateTime.UtcNow.Second % 2 == 0) ? 10 : 9;
    Random r = new Random(); // time-based seed → unpredictable
    return baseBonus + (r.Next(0, 2)); // 0 or 1 more
  }
}

public class Program
{
  public static void Main(string[] args)
  {
    // Simulated test: sometimes expects exactly 10, but result varies
    int got = Promo.BonusNow();
    if (got != 10) Console.WriteLine("FAIL (flaky): expected 10 but got " + got);
    else Console.WriteLine("PASS (maybe): got " + got);
  }
}

控制依赖项

注入时间和 Random。使用固定种子和固定时间戳后,失败就变得可重现。

using System;

public interface IClock { DateTime NowUtc(); }
public sealed class FakeClock : IClock
{
  private readonly DateTime _fixedUtc;
  public FakeClock(DateTime fixedUtc) { _fixedUtc = fixedUtc; }
  public DateTime NowUtc() { return _fixedUtc; }
}

public sealed class PromoDeterministic
{
  private readonly IClock _clock;
  private readonly Random _rng;
  public PromoDeterministic(IClock clock, Random rng) { _clock = clock; _rng = rng; }

  public int BonusNow()
  {
    int baseBonus = (_clock.NowUtc().Second % 2 == 0) ? 10 : 9;
    return baseBonus + _rng.Next(0, 2);
  }
}

public static class Assert
{
  public static void AreEqual(int expected, int actual, string msg)
  {
    if (expected != actual) throw new Exception("Expected " + expected + " but got " + actual + " :: " + msg);
  }
}

public class Program
{
  public static void Main(string[] args)
  {
    try
    {
      IClock clock = new FakeClock(new DateTime(2016, 1, 1, 0, 0, 20, DateTimeKind.Utc)); // even second
      Random rng = new Random(123); // fixed seed
      PromoDeterministic p = new PromoDeterministic(clock, rng);

      int got = p.BonusNow(); // predictable now
      Assert.AreEqual(10, got, "even second, first draw should be 0");
      Console.WriteLine("PASS: deterministic promo");
    }
    catch (Exception ex)
    {
      Console.WriteLine("FAIL: " + ex.Message);
    }
  }
}

最小重现案例

不断缩小输入,直到一个很小的示例失败(这里是空数组)。较小的案例更容易分析。

using System;

// Buggy function: average of ints, but divides by count without guarding empty input
public static class Stats
{
  public static double Average(int[] xs)
  {
    int sum = 0;
    for (int i = 0; i < xs.Length; i++) sum += xs[i];
    return sum / xs.Length; // BUG: divide by zero if empty; also int division truncates
  }
}

public static class Assert
{
  public static void Throws<T>(Action a, string msg) where T : Exception
  {
    try { a(); }
    catch (Exception ex)
    {
      if (ex is T) return;
      throw new Exception("Expected " + typeof(T).Name + " but got " + ex.GetType().Name + " :: " + msg);
    }
    throw new Exception("Expected " + typeof(T).Name + " but no exception thrown :: " + msg);
  }
}

public class Program
{
  public static void Main(string[] args)
  {
    // Minimal failing case: empty array
    Assert.Throws<DivideByZeroException>(delegate { Stats.Average(new int[0]); }, "empty input");
    Console.WriteLine("Reproduced: empty input triggers the bug.");
  }
}

修复与消息质量

防止无效输入,并使用描述性的断言消息(包含输入、预期值和误差容限)。

using System;

public static class Stats
{
  public static double Average(int[] xs)
  {
    if (xs == null) throw new ArgumentNullException("xs");
    if (xs.Length == 0) throw new ArgumentException("xs must not be empty", "xs");

    double sum = 0.0; // use double to avoid integer truncation
    for (int i = 0; i < xs.Length; i++) sum += xs[i];
    return sum / xs.Length;
  }
}

public static class Assert
{
  public static void AreAlmostEqual(double expected, double actual, double eps, string msg)
  {
    if (Math.Abs(expected - actual) > eps)
      throw new Exception("Expected " + expected + " but got " + actual + " (eps " + eps + ") :: " + msg);
  }
}

public class Program
{
  public static void Main(string[] args)
  {
    try
    {
      double a = Stats.Average(new int[] { 2, 4, 6 });
      Assert.AreAlmostEqual(4.0, a, 1e-9, "average of 2,4,6");

      Console.WriteLine("PASS: fixed average with clear message");
    }
    catch (Exception ex)
    {
      Console.WriteLine("FAIL: " + ex.Message);
    }
  }
}

调试检查清单

调试检查清单:

  • 稳定依赖项(时间、随机性、I/O)
  • 在本地反复重现
  • 缩小到最小的失败输入
  • 添加精确的断言消息
  • 优先使用纯函数和守卫检查

不稳定测试的第一步

快速检查:一个间歇性失败的测试使用 DateTime.UtcNow 和 new Random()。第一步最好做什么?

回顾

回顾:通过控制时间和随机性来可靠地重现问题,缩小输入,并编写有帮助的断言消息,以快速定位错误。

常见问题解答

「调试失败的测试」课时是免费的吗?

是的 — 「调试失败的测试」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 C# Academy 课程的其余内容,请升级到 CoddyKit PRO。 C# Academy 课程共包含 3 节课。

「调试失败的测试」这节课中我会学到什么?

让失败可重现,移除时间和随机性,将问题缩减为最小案例,并改进断言消息以便快速诊断。 你通过在浏览器中直接运行的动手代码来练习 C# Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 C# Academy 需要有经验吗?

无需任何先前经验。CoddyKit 上的 C# Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 3 节课,共 3 节。

「调试失败的测试」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 C# Academy 课中编写并运行代码吗?

能。每节 C# Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. xUnit/MSTest 设置与断言
  2. 通过接口创建伪对象与测试数据构建器
  3. 调试失败的测试
← 返回 C# Academy