单元测试:program.cs Main 中的 try 语句

Unit testing: try statement in program.cs Main

我的项目如下所示。我想创建一个单元测试来验证当 appsetting.Environment.json 不存在或 DataSource:JsonPath 文件不存在时会发生什么。

我正在使用 xUnit,但刚刚开始,

在这种情况下,它应该抛出 FileNotFoundException 并退出。

internal class Program
{
    private static int Main(string[] args)
    {
        public static IConfiguration Configuration { get; } = new ConfigurationBuilder()
            .SetBasePath(Directory.GetCurrentDirectory())
            .AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ENVIRONMENT") ?? "Production"}.json", optional: true)
            .Build();

        try
        {
            Log.Information($"{DateTime.Now.ToLongTimeString()} Starting...");

            var jsonFileName = Configuration["DataSource:JsonPath"];
            if (!File.Exists(jsonFileName)) throw new FileNotFoundException(jsonFileName);

            //code if file exist...
        }
        catch (Exception ex)
        {
            Log.Fatal(ex, "Terminated unexpectedly");
            return 1;
        }
        finally
        {
            Log.CloseAndFlush();
        }
    }
}

在不更改任何代码的情况下,有两种方法可以做到这一点:

  • 实际上运行程序使用Process class and examine the exit code of your program (exposed in the Process.ExitCode属性)。在这种情况下,class 是 internalMainprivate 的事实无关紧要。

  • 使用反射获取 Main 方法并调用它,检查 return 值。

如果其中 none 是可口的,您可以将 Main 更改为 internal 并使用 [InternalsVisibleTo] 属性使您的单元测试无需调用该方法倒影。

对于所有这 3 种情况,如果退出代码为 0,则测试通过;如果退出代码为 2,则测试失败(假设没有其他可能的退出代码)。

在最好的情况下,您会将 Log 依赖项注入到 Main 或您尝试测试的任何 class 中。然后您将测试您的 Log 方法在您开发的任何情况下都被调用。您还可以注入配置和文件访问依赖项,而不是紧密耦合,这样您就不必在物理媒体上实际进行 read/write 访问来测试您的代码。目前你不能做真正的单元代码测试因为你有紧密耦合的外部依赖,所以你的代码在技术上正在做集成测试。

我建议阅读 Autofac DI container in console app. (I don't have any affiliate with Autofac,我只是更喜欢它而不是其他 DI 框架。有很多所以如果你决定使用它们做一些研究)。如果我要实现它,它可能看起来像这样:

internal class Program
{
    private static int Main(string[] args)
    {
      var container = CreateContainer();
      var worker = container.resolve<IWorker>();
      worker.Execute();
    }

}

internal class Worker : IWorker
{
    private IConfiguration _configuration;
    private IFileSystem _filesystem;
    private ILog _log;
    private IDateTimeFactory _datetimeFactory;

    public Worker(
      IConfiguration configuration,
      IFileSystem filesystem,
      ILog log,
      IDateTimeFactory _datetimeFactory)
    {
      _configuration = configuration;
      _filesystem = filesystem;
      _log = log;
      _datetimeFactory = datetimeFactory;
    }

    public void Execute()
    {
        try
        {
            _log.Information($"{_datetimeFactory.Now.ToLongTimeString()} Starting...");

            var jsonFileName = _configuration["DataSource:JsonPath"];
            if (!_filesystem.Exists(jsonFileName)) 
              throw new FileNotFoundException(jsonFileName);

            //code if file exist...
        }
        catch (Exception ex)
        {
            _log.Fatal(ex, "Terminated unexpectedly");
            return 1;
        }
        finally
        {
            _log.CloseAndFlush();
        }
    }
}

以下是一般示例,我不能保证它 100% 有效。 现在,当我想测试 worker 时,我几乎可以测试任何东西: (我假设您想使用 nSubstitute and nUnit 进行测试)

[TestFixture]
public WorkerTests
{
  [Test]
  public void Execute_ConfigurationFileNotFound_LogsException()
  {
     var config = Substitute.For<IConfiguration>();
     var filesystem = Substitute.For<IFileSystem>();
     var log = Substitute.For<ILog>();
     var datetime = Substitute.For<IConfiIDateTimeFactory>();

     var worker = new Worker(
       config,
       filesystem,
       log,
       datetime)

     // setup the mock'd file system to always return false for Exists()
     // That is what we're testing
     filesystem.Exists(string.Empty).ReturnsForAnyArgs(false);

     worker.Execute();

     Assert.That(log.Received().Fatal(), Is.True);
  }
}