Search code examples
c#.netasp.net-core.net-coredotnet-cli

How to test a self-contained exe from the debug folder of a test project?


Error when trying to launch the exe from the debug folder of my xunit project.

A fatal error was encountered. The library 'hostpolicy.dll' required to execute the application was not found in 'C:\Program Files\dotnet\'.
Failed to run as a self-contained app.
  - The application was run as a self-contained app because 'C:\Users\me\Desktop\dotnet-testing-cli-issue\MyApp.Console.Tests\bin\Debug\net7.0\MyApp.Console.runtimeconfig.json' did not specify a framework.
  - If this should be a framework-dependent app, specify the appropriate framework in 'C:\Users\me\Desktop\dotnet-testing-cli-issue\MyApp.Console.Tests\bin\Debug\net7.0\MyApp.Console.runtimeconfig.json'.

MyApp.Console.csproj:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net7.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <RuntimeIdentifier>win-x64</RuntimeIdentifier>
  </PropertyGroup>

</Project>

Program.cs

// See https://aka.ms/new-console-template for more information
Console.WriteLine("Hello, World!");

MyApp.Console.Tests.csproj

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net7.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>

    <IsPackable>false</IsPackable>
    <IsTestProject>true</IsTestProject>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="CliWrap" Version="3.6.0" />
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
    <PackageReference Include="xunit" Version="2.4.2" />
    <PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
      <PrivateAssets>all</PrivateAssets>
    </PackageReference>
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\MyApp.Console\MyApp.Console.csproj" />
  </ItemGroup>

</Project>

Tests.cs

public class UnitTest1
{
    [Fact]
    public async Task Test1Async()
    {
        var result = await Cli.Wrap("MyApp.Console.exe")
            .WithArguments(new[] { "--foo", "bar" })
            .ExecuteAsync();
    }
}

Full Reproduction: https://github.com/VictorioBerra/netcore-console-test-exe

This could be an X-Y problem too. I want to test my console app, including one that uses a generic host, appsettings, the works.

How can I launch it and test it?


Solution

  • If you'll build the solution and open "MyApp.Console.Tests\bin\Debug\net7.0" and "MyApp.Console.Tests\bin\Debug\net7.0" folders and compare the contents you will see that the latter has far more files than the former - those files are parts of the runtime shipped with the self-contained app and required to run it. You can set up some elaborate build action to perform correct app publish to some path and use it in the tests project, but I would argue that is too convoluted and brittle approach.

    You can to switch to the "old" Program.Main style (If project is created with dotnet new console CLI command - provide --use-program-main so no need to manually change it), run the Program.Main(args) and then capture the "current" console output via Console.Out:

    public sealed class ConsoleCapture : IDisposable
    {
        private readonly StringWriter _stringWriter = new();
    
        private readonly TextWriter _oldOut;
        private readonly TextWriter _oldErr;
    
        public ConsoleCapture()
        {
            _oldOut = Console.Out;
            _oldErr = Console.Error;
    
            Console.SetOut(_stringWriter);
            Console.SetError(_stringWriter);
        }
    
        public string GetAll()
        {
            return _stringWriter.ToString();
        }
    
        public void Dispose()
        {
            Console.SetOut(_oldOut);
            Console.SetError(_oldErr);
    
            _stringWriter.Dispose();
        }
    }
    

    And usage:

    using var console = new ConsoleCapture();
    var res = Program.Main(args);
    
    var consoleOutput = console.GetAll();
    
    Assert.AreEqual(expectedExitCode, res);
    Assert.Contains("Hello World", res);
    

    Opened PR with example.

    Notes:

    • For ASP.NET Core apps it is even easier with the integration test approach - see this answer for some details (and everything from above is not needed)
    • If you still want to keep the top-level statements Program file then you can do some reflection to invoke the generated Program.<Main>$, though this will be a bit more brittle because the generation pattern can change (though there is an option to mitigate that by using Assembly.EntryPoint - see this answer).
    • As for the config - note that build-in configuration in .NET supports multiple providers and uses hierarchical approach. You can make a copy (or add it as a link from main project) of appsettings file in the test project (though usually you will have a standalone one) and/or provide config as CLI parameters (some examples here)