This post walks through the internals of dotnet test
command. We will look at
bits and pieces of code on the way.
A command line toolset
The dotnet command line tools (dotnet-cli) are a collection of command line
utilities, each named after the invocation verb. E.g. new
verb is handled by a
tool named dotnet-new
. The test run is handled by an utility called
dotnet-test.
With the move to csproj based projects, an operation that requires knowledge of the project properties depends on msbuild to find them. The test entrypoint does invoke msbuild to ensure project related information like the output assembly etc..
A word on the scenarios…
In the test operations, there are two key user motivations:
- During development, user may want to run the tests from the project. This requires an understanding of target frameworks, bitness, dependencies etc..
- In a CDP scenario, user may want to directly run the output binary since the source code may not be available on the deployment agent box. Here user provides the test binary directly as input to the runner. All dependencies are available in the published output directory.
The development inner loop scenario is triggered from an IDE/Editor and command
line. dotnet test
supports the inner loop, where as dotnet vstest
is the
entrypoint for advanced scenarios (like IDE/Editor integration).
dotnet-test flow
The dotnet-test
takes a project file as input (testapp.csproj
) to run the
tests. It does some prework to compute the outputs of a project, and then
invokes the test runner. Let’s look at the flow.
1. dotnet test
evaluates input options and invokes msbuild /t:VSTest
.
Source: dotnet-cli/src/dotnet/commands/dotnet-test
Let’s find the msbuild invocation from dotnet test
.
:::bash
$ dotnet test --list-tests --no-build -v:diag | head -n 1
# Invokes msbuild with args translated to Properties for VSTest target
$ ~/dotnet/sdk/1.0.0-preview5-004262/MSBuild.dll.exe /nologo
/Logger:Microsoft.DotNet.Tools.MSBuild.MSBuildLogger,~/dotnet/sdk/1.0.0-preview5-004262/dotnet.dll
/m /p:VSTestListTests=true /p:VSTestNoBuild=true /t:VSTest /v:m
/verbosity:diag
~/tmp/samples/testapp/testapp.csproj
The interesting properties in this case are:
--list-tests
translated to/p:VSTestListTests=true
--no-build
translated to/p:VSTestNoBuild=true
2. The VSTest target
Source: vstest/src/Microsoft.TestPlatform.Build/Microsoft.TestPlatform.targets
VSTest
target tries to build the test project if required, calls
VSTest
task with evaluated parameters. It adds a few project context
specific parameters like the path to test assembly.
Below is the code excerpt.
:::xml
<!--
============================================================
Test target
Main entry point for running tests through vstest.console.exe
============================================================
-->
<Target Name="VSTest" >
<CallTarget Condition="'$(VSTestNoBuild)' != 'true'" Targets="BuildProject" />
<CallTarget Targets="ShowCallOfVSTestTaskWithParameter" />
<Microsoft.TestPlatform.Build.Tasks.VSTestTask
TestFileFullPath="$(TargetPath)"
VSTestSetting="$(VSTestSetting)"
VSTestTestAdapterPath="$(VSTestTestAdapterPath)"
VSTestFramework="$(TargetFrameworkMoniker)"
VSTestPlatform="$(PlatformTarget)"
VSTestTestCaseFilter="$(VSTestTestCaseFilter)"
VSTestLogger="$(VSTestLogger)"
VSTestListTests="$(VSTestListTests)"
VSTestDiag="$(VSTestDiag)"
/>
</Target>
The VSTest
task is defined in Microsoft.TestPlatform.Build
nuget package and
is packages as ~/dotnet/sdk/1.0.0-preview5-004262/Microsoft.TestPlatform.Build.dll
in dotnet-cli.
3. The VSTest task
Source: vstest/src/Microsoft.TestPlatform.Build/Tasks/VSTestTask.cs
VSTest
task parses the parameters, runs vstest.console
runner with test
assembly. It listens to standard output/error of the console runner and relays
it back to msbuild.
Let’s use diagnostic trace to find what’s going on!
:::bash
$ export VSTEST_BUILD_TRACE=1 # enable tracing for VSTest task
$ dotnet test --list-tests --no-build
Test run for /home/codito/tmp/samples/mstesttest/bin/Debug/netcoreapp1.0/mstesttest.dll(.NETCoreApp,Version=v1.0)
VSTest: Starting vstest.console...
VSTest: Arguments: dotnet exec\
"~/dotnet/sdk/1.0.0-preview5-004262/vstest.console.dll"\
--framework:".NETCoreApp,Version=v1.0"\
--listTests\
"~/tmp/samples/testapp/bin/Debug/netcoreapp1.0/testapp.dll"
Microsoft (R) Test Execution Command Line Tool Version 15.0.0.0
Copyright (c) Microsoft Corporation. All rights reserved.
The following Tests are available:
PassingTest
FailingTest
VSTest: Exit code: 0
The output explains it all :)
We’ve one last piece of the build puzzle left: multitargeted test projects.
In case of multitargeted test project, msbuild evaluates the TargetFrameworks
array and calls VSTest
target once for each TargetFramework
. VSTest
target
invokes the console runner (vstest.console
) for each.
This file is packaged as ~/dotnet/sdk/1.0.0-preview5-004262/15.0/Microsoft.Common.CrossTargeting.targets/ImportAfter/Microsoft.TestPlatform.CrossTargeting.targets
4. Inside Microsoft.NET.Test.Sdk
There are three components that come together to run a test with test platform:
- A test framework. E.g. mstest, xunit, nunit etc.
- An adapter which can identify and execute tests written with the framework
- A test host that finds all adapters, and orchestrates the run
In the earlier versions of test platform, test framework and adapter could be
provided by nuget packages. The test host was an integral part of the test
platform. Currently the test host comes with Microsoft.NET.Test.Sdk
nuget
package. A test project carries everything required to run a test with itself.
Source: vstest/src/Microsoft.NET.Test.Sdk.targets
In addition to bringing in test host reference in dependency closure, this package also provides build time support for test project to hide some complexities. E.g. a target that auto generates the program entrypoint
::: xml
<Target Name="GenerateProgramFile"
BeforeTargets="CoreCompile"
DependsOnTargets="PrepareForBuild;CoreGenerateProgramFile"
Condition="'$(GenerateProgramFile)' == 'true'" />
The GenerateProgramFile
target runs before the CoreCompile
target. The
generated program file lives in the intermediate output directory and is passed
to the compiler. That saves every test project from having a program entrypoint.
That’s all for now :) This platform is now open source. If you have any feedback, please create an issue at vstest repository!