Migrating PowerShell E2E Tests to Apex Tests
PowerShell E2E tests live in test/EndToEnd/tests/. Apex tests live in
test/NuGet.Tests.Apex/NuGet.Tests.Apex/NuGetEndToEndTests/. The goal is to migrate PS tests
to C# Apex tests that run the exact same scenario, then remove the PS test function.
Workflow
- Read the PS test — understand what it does: which project type, which PMC commands, what assertions.
- Decide: unit test or Apex test — if it only tests cmdlet behavior (filtering, listing, prerelease logic) without needing VS project system integration, write a unit test. Otherwise use Apex.
- Pick the right file — for Apex, match the scenario to an existing test class (see File Placement below). For unit tests, use the cmdlet-specific file in
Cmdlets/(see Unit test file placement below). - Translate — use the mappings in this skill to convert each PS construct to its C# equivalent.
- Verify — run
get_errorsor build the project to confirm it compiles cleanly. - Remove the PS function — delete the migrated function from the PS test file.
- If already covered — if an existing Apex or unit test already covers the same scenario, just delete the PS test. No new test needed.
- Update this skill — if you discovered new mappings, gotchas, or corrections, add them to the appropriate section of this file.
File placement
Choose the target file by interaction surface, not project type:
| Interaction surface | Apex file |
|---|---|
Get-Package command scenarios (-ListAvailable, -Updates, -Filter, path source variants, prerelease behaviors) |
GetPackageTestCase.cs |
| Other PMC commands (Install-Package, Update-Package, Uninstall-Package, Get-Project) | NuGetConsoleTestCase.cs |
| NuGet UI / Package Manager dialog | NuGetUITestCase.cs |
| IVsPackageInstaller / IVsServices API | IVsServicesTestCase.cs |
| Sync/binding redirect scenarios | SyncPackageTestCase.cs |
| Audit / vulnerability scenarios | NuGetAuditTests.cs |
| .NET Core project-creation / restore / source-mapping | NetCoreProjectTestCase.cs |
PMC tests for PackageReference projects still follow command-based placement. For Get-Package
scenarios use GetPackageTestCase.cs; for other PMC commands use NuGetConsoleTestCase.cs.
Template mapping
| PowerShell function | Apex ProjectTemplate |
Package management | Verified |
|---|---|---|---|
New-ConsoleApplication |
ProjectTemplate.ConsoleApplication |
packages.config | ✅ |
New-ClassLibrary |
ProjectTemplate.ClassLibrary |
packages.config | ✅ |
New-WebSite |
ProjectTemplate.WebSiteEmpty |
packages.config | ✅ |
New-WebApplication |
ProjectTemplate.WebApplicationEmpty |
packages.config | ❌ |
New-WPFApplication |
ProjectTemplate.WPFApplication |
packages.config | ❌ |
New-MvcApplication |
ProjectTemplate.WebApplicationEmptyMvc |
packages.config | ❌ |
New-FSharpLibrary |
ProjectTemplate.FSharpLibrary |
PackageReference | ❌ |
New-NetCoreConsoleApp |
ProjectTemplate.NetCoreConsoleApp |
PackageReference | ✅ |
New-NetStandardClassLib |
ProjectTemplate.NetStandardClassLib |
PackageReference | ✅ |
| PowerShell function | Apex equivalent |
|---|---|
New-SolutionFolder 'Name' |
testContext.SolutionService.AddSolutionFolder("Name") |
The package management style determines which assertion methods to use — packages.config projects
use AssertPackageInPackagesConfig, while PackageReference projects use AssertPackageInAssetsFile.
Note: This table covers the most common PS project factories. Some PS tests use specialized factories like
New-ClassLibraryNET46,New-BuildIntegratedProj,New-UwpPackageRefClassLibrary, orNew-NetCoreConsoleMultipleTargetFrameworksApp. These don't have a 1:1ProjectTemplateenum value — check the ApexProjectTemplateenum and existing tests for the closest match, or create a standard template and modify the csproj afterward (e.g., for multi-targeting).
Command execution
| Scenario | Apex API |
|---|---|
Standard install with -Version |
nugetConsole.InstallPackageFromPMC(packageName, packageVersion) |
Install with extra flags (-Source, -WhatIf, -IgnoreDependencies) |
nugetConsole.Execute($"Install-Package {packageName} -ProjectName {project.Name} -Source {source}") |
| Standard uninstall | nugetConsole.UninstallPackageFromPMC(packageName) |
Standard update with -Version |
nugetConsole.UpdatePackageFromPMC(packageName, packageVersion) |
Update with -Safe, -Reinstall, etc. |
nugetConsole.Execute($"Update-Package {packageName} -Safe") |
| Any raw PMC command | nugetConsole.Execute(command) |
Key rule: Both InstallPackageFromPMC() and UpdatePackageFromPMC() always inject -Version.
If the original PS test does not use -Version, use Execute() with the raw command string
instead — using the helper changes the semantics.
PowerShell session state is accessible. nugetConsole.Execute() runs in a live PMC PowerShell
session. It can execute any PowerShell command, not just NuGet commands. This means PS session
state — global variables ($global:InstallVar), registered functions
(Test-Path function:\Get-World), environment checks — can all be queried and asserted via
Execute() + IsMessageFoundInPMC(). Do not skip tests just because they assert PS session state.
Project-level build operations:
| Scenario | Apex API |
|---|---|
| Clean (deletes obj/bin) | testContext.Project.Clean() |
| Rebuild | testContext.Project.Rebuild() |
| Cache file path | CommonUtility.GetCacheFilePath(testContext.Project) |
| Wait for file to appear | CommonUtility.WaitForFileExists(path) |
| Wait for file to disappear | CommonUtility.WaitForFileNotExists(path) |
For single-project solutions, project-level Clean/Rebuild is equivalent to solution-level.
Assertion mapping
| PowerShell assertion | Apex equivalent |
|---|---|
Assert-Package $p PackageName Version (packages.config) |
CommonUtility.AssertPackageInPackagesConfig(VisualStudio, testContext.Project, packageName, version, Logger) |
Assert-Package $p PackageName (no version, packages.config) |
CommonUtility.AssertPackageInPackagesConfig(VisualStudio, testContext.Project, packageName, Logger) |
Assert-Package $p PackageName Version (PackageReference) |
CommonUtility.AssertPackageInAssetsFile(VisualStudio, testContext.Project, packageName, version, Logger) |
Assert-Throws { ... } $expectedMessage |
nugetConsole.IsMessageFoundInPMC(expectedMessage) — PMC errors appear as text, not C# exceptions |
Assert-Null (Get-ProjectPackage ...) / not installed |
CommonUtility.AssertPackageNotInPackagesConfig(VisualStudio, testContext.Project, packageName, Logger) |
Assert-NoPackage $p PackageName Version (PackageReference) |
CommonUtility.AssertPackageNotInAssetsFile(VisualStudio, testContext.Project, packageName, version, Logger) |
Assert-PackageReference $p PackageName Version |
CommonUtility.AssertPackageReferenceExists(VisualStudio, testContext.Project, packageName, version, Logger) |
Assert-NoPackageReference $p PackageName |
CommonUtility.AssertPackageReferenceDoesNotExist(VisualStudio, testContext.Project, packageName, Logger) |
Package sources
| PowerShell source | Apex equivalent |
|---|---|
$context.RepositoryRoot / $context.RepositoryPath |
testContext.PackageSource — create packages with CommonUtility.CreatePackageInSourceAsync() |
No -Source (uses nuget.org) |
Create a local package with CommonUtility.CreatePackageInSourceAsync(testContext.PackageSource, ...) — never depend on nuget.org |
Hardcoded invalid sources (http://example.com, ftp://...) |
Use the same hardcoded strings directly |
Creating test packages
For simple packages:
await CommonUtility.CreatePackageInSourceAsync(testContext.PackageSource, packageName, packageVersion);
For packages with dependencies:
await CommonUtility.CreateDependenciesPackageInSourceAsync(
testContext.PackageSource, packageName, packageVersion, dependencyName, dependencyVersion);
For .NET Framework-specific packages:
await CommonUtility.CreateNetFrameworkPackageInSourceAsync(
testContext.PackageSource, packageName, packageVersion);
NuGet.Config manipulation
PS tests that use Get-VSComponentModel + ISettings to modify NuGet config at runtime can be
migrated by pre-configuring SimpleTestPathContext before passing it to ApexTestContext.
Via Settings API (preferred):
using var simpleTestPathContext = new SimpleTestPathContext();
simpleTestPathContext.Settings.AddSource("PrivateRepo", privatePath);
using var testContext = new ApexTestContext(VisualStudio, projectTemplate, Logger,
simpleTestPathContext: simpleTestPathContext);
Via raw config file (for settings not covered by the API like dependencyVersion or bindingRedirects):
using var simpleTestPathContext = new SimpleTestPathContext();
File.WriteAllText(simpleTestPathContext.NuGetConfig,
$@"<?xml version=""1.0"" encoding=""utf-8""?>
<configuration>
<config>
<add key=""dependencyVersion"" value=""HighestPatch"" />
</config>
<packageSources>
<clear />
<add key=""source"" value=""{simpleTestPathContext.PackageSource}"" />
</packageSources>
</configuration>");
using var testContext = new ApexTestContext(VisualStudio, projectTemplate, Logger,
simpleTestPathContext: simpleTestPathContext);
Test structure patterns
Error-path tests (no package creation needed) — synchronous
[TestMethod]
[Timeout(DefaultTimeout)]
public void DescriptiveTestName_Fails()
{
using var testContext = new ApexTestContext(VisualStudio, ProjectTemplate.ConsoleApplication, Logger);
var packageName = "Rules";
var source = @"c:\temp\data";
var expectedMessage = $"Unable to find package '{packageName}' at source '{source}'. Source not found.";
var nugetConsole = GetConsole(testContext.Project);
nugetConsole.Execute($"Install-Package {packageName} -ProjectName {testContext.Project.Name} -Source {source}");
Assert.IsTrue(
nugetConsole.IsMessageFoundInPMC(expectedMessage),
$"Expected error message was not found in PMC output. Actual output: {nugetConsole.GetText()}");
}
Success-path tests (need package creation) — async
[TestMethod]
[Timeout(DefaultTimeout)]
public async Task DescriptiveTestNameAsync()
{
using var testContext = new ApexTestContext(VisualStudio, ProjectTemplate.ConsoleApplication, Logger);
var packageName = "TestPackage";
var packageVersion = "1.0.0";
await CommonUtility.CreatePackageInSourceAsync(testContext.PackageSource, packageName, packageVersion);
var nugetConsole = GetConsole(testContext.Project);
nugetConsole.InstallPackageFromPMC(packageName, packageVersion);
CommonUtility.AssertPackageInPackagesConfig(VisualStudio, testContext.Project, packageName, packageVersion, Logger);
}
Data-driven tests (multiple project templates)
When the same scenario applies to multiple project types, use [DataTestMethod] or [DynamicData]:
[DataTestMethod]
[DynamicData(nameof(GetPackageReferenceTemplates), DynamicDataSourceType.Method)]
[Timeout(DefaultTimeout)]
public async Task InstallPackageAsync(ProjectTemplate projectTemplate)
{
using var simpleTestPathContext = new SimpleTestPathContext();
EnsurePackageReferenceFormat(simpleTestPathContext, projectTemplate);
using var testContext = new ApexTestContext(VisualStudio, projectTemplate, Logger,
simpleTestPathContext: simpleTestPathContext);
// ... test body
}
// Yields both legacy and SDK-style PackageReference templates
private static IEnumerable<object[]> GetPackageReferenceTemplates()
{
yield return new object[] { ProjectTemplate.ConsoleApplication };
yield return new object[] { ProjectTemplate.NetCoreConsoleApp };
}
// Only legacy (ConsoleApplication) needs this; SDK projects ignore it
private static void EnsurePackageReferenceFormat(SimpleTestPathContext context, ProjectTemplate template)
{
if (template == ProjectTemplate.ConsoleApplication)
context.Settings.SetPackageFormatToPackageReference();
}
Use [DynamicData] over [DataRow] when the template set is reused across multiple tests in the
same class. This ensures one test method covers both legacy and SDK-style PackageReference projects.
Multi-targeted project tests
To create a multi-targeted project, modify the csproj after project creation:
using var testContext = new ApexTestContext(VisualStudio, ProjectTemplate.NetCoreConsoleApp, Logger);
// Modify csproj to multi-target via XDocument:
// change <TargetFramework> to <TargetFrameworks>net8.0;netstandard2.0</TargetFrameworks>
Style rules
Follow the repo-wide coding guidelines first — they apply to all code, not just migrations:
docs/coding-guidelines.md (e.g. no #region blocks, no
reflection, var usage, nullable enabled). Don't restate or duplicate those rules here; this section
only lists conventions specific to test migration that supplement the common guidelines:
- Use
using var(inline using declaration), notusing (var ...) { }. - Place migrated tests before the static helper methods (
GetNetCoreTemplates, etc.) in the file. - Method names:
{Action}FromPMC{Scenario}[_Fails|Async]. Suffix with_Failsfor error tests,Asyncfor async tests. - Always include
[Timeout(DefaultTimeout)]. - Always include
nugetConsole.GetText()in assertion failure messages for diagnostics. - The test class inherits
SharedVisualStudioHostTestClasswhich providesVisualStudioandLogger. - Get PMC console via
GetConsole(testContext.Project)helper method in the test class. - Don't use the method-delegates-to-async-helper pattern unless the helper is actually called from multiple classes. Inline the test logic directly in the test method.
- Do NOT add "Migrated from" comments in the test code. The PR description tracks the mapping — the code should stand on its own without referencing the old PS test.
Tests that should NOT be migrated
Skip PS tests that:
- Use
Assert-BindingRedirect— binding redirect tests are already[SkipTest]in PS and not worth migrating. - Depend on DTE project hierarchy semantics (e.g.,
Get-ProjectItemto check tree structure, parent/child relationships). However, if the PS test only usesGet-ProjectItem/Get-ProjectItemPathto verify a file exists on disk, migrate it using filesystem assertions instead:File.Exists(path), XML reads on the project file, orCommonUtility.WaitForFileExists().
After migration checklist
- ✅ Remove the migrated function from the PS test file.
- ✅ If a PS test is already covered by an existing Apex test (duplicate), just delete the PS test — no new Apex test needed.
- ✅ Build the Apex project or run
get_errorsto verify it compiles cleanly. - ✅ Verify assertion methods match the project's package management style (packages.config vs PackageReference).
Common gotchas
- Console width: PMC output assertions are text-sensitive. The Apex infrastructure forces console width to 1024 to avoid wrapping issues.
- Restore timing: After install/update operations, the Apex infrastructure handles waiting for restore completion. You generally don't need explicit waits.
- nuget.org dependency: PS tests that don't specify
-Sourceimplicitly use nuget.org. Always replace this with local package creation viaCreatePackageInSourceAsync— tests must not depend on external feeds. NuGetApexTestServicelimitations: It does NOT exposeISolutionManageror VS DTE project item inspection. OnlyIVsPackageInstaller,IVsSolutionRestoreStatusProvider,IVsPackageUninstaller,IVsPathContextProvider2, andIVsUIShellare available.- IVs error-path tests:
NuGetApexTestService.InstallPackage()swallowsInvalidOperationExceptionand logs it — it does NOT rethrow. For error-path IVs tests, assert that the package was NOT installed (AssertPackageNotInPackagesConfig) rather than trying to catch exceptions. - Feature renaming: Some PS tests use older feature names (e.g., "PackageNameSpace"). When migrating, use the current feature name (e.g., "PackageSourceMapping") in test method names and comments.
- IVs tests use
EnvDTE.Project: IVs API methods likeInstallPackage()takeproject.UniqueName(fromEnvDTE.Project), not aProjectTestExtension. Get it viaVisualStudio.Dte.Solution.Projects.Item(1). _pathContextvstestContext:IVsServicesTestCaseuses a class-levelSimpleTestPathContext _pathContext(initialized in constructor), not per-testApexTestContext. PMC tests inNuGetConsoleTestCaseuse per-testApexTestContext.- Never overwrite
SimpleTestPathContext's NuGet.config: UsingCreateConfigurationFileto write a full config replaces defaults likeglobalPackagesFolder,fallbackPackageFolders, andhttpCacheFolder— causing packages to pollute the user's real global packages folder. Instead usesimpleTestPathContext.Settings.AddSource()andAddPackageSourceMapping()to layer config on top of the defaults. - Legacy PackageReference projects don't auto-restore — never call
WaitForAutoRestore()forConsoleApplicationwithSetPackageFormatToPackageReference(). Only SDK-style projects (e.g.,NetCoreConsoleApp) auto-restore on project open. - Always check for existing Apex coverage before migrating — search the Apex test files for
equivalent scenarios. Common negative tests like
UpdatePackageFromPMCNotInstalled_FailsandUninstallPackageFromPMCNotInstalled_Failsalready exist. If covered, just delete the PS test. - Daily vs regular Apex cadence matters — a test in
NuGet.Tests.Apex.Dailydoes NOT count as coverage for removing an E2E test that runs on every PR. Only regular Apex tests (NuGet.Tests.Apex) provide equivalent gating. - Ambiguous startup project names require the unique name — Apex
SolutionService.SetStartupProject(ProjectTestExtension)selects by the short project name, so it cannot distinguish projects with the same name. For these scenarios, setDTE.Solution.SolutionBuild.StartupProjectsfrom a host-side Apex service using the project'sUniqueName. - Create duplicate project names through host-side DTE APIs — Apex
SolutionService.AddProjectcan fail when a project with the same short name already exists in a solution folder. UseSolutionFolder.AddFromTemplateandSolution2.AddFromTemplatewith distinct destination directories from a host-side Apex service.
Learnings (continuously updated)
This section captures lessons learned from actual migration runs that don't fit neatly into the sections above. Update this section after every migration run with new discoveries or corrections.
2026-05-20: PackageReferenceTestCase patterns
- PR #7246 incorrectly removed
Test-NetCoreConsoleAppClean— it was claimed to be covered by Apex Daily'sVerifyCacheFileInsideObjFolder, but that test is in Daily (different cadence) and bundles 3 behaviors. We properly covered it by addingNetCoreConsoleApptoGetPackageReferenceTemplates()inPackageReferenceTestCase.CleanDeletesCacheFile.
2026-06-26: Find-Package / Get-Package paging migration
Get-Package -ListAvailabledefaultsFirstto 50 (GetPackageCommand.DefaultFirstValue). The old E2E testTest-GetPackageRetunsMoreThanServerPagingLimitasserted>100against nuget.org and only worked under an older default. When migrating "more than server paging limit" to a unit test, pass an explicit large-First(e.g.105) over a local source with 105 packages and assert> 100— this faithfully validates page aggregation inPackageFeedEnumerator. Without-First, the local result caps at 50.- Find/Get cmdlet filtering is substring match on a local feed — creating ids that share a keyword prefix (e.g.
FirstKeywordPackage1..6) and searching the keyword returns all of them, so-First/-Skippaging is testable with a unit test. vs-tests.ymlpart:list is NOT maintained per-migration — e.g.SyncPackageTest.ps1remains listed after deletion in #7270. Follow precedent: when fully migrating a PS test file, delete the file and remove it fromNuGet.slnsolution items, but leave thevs-tests.ymlpart list untouched.-Sourceby name and-Updatessource filtering are UNIT-testable, not Apex-only.GetMatchingSourceresolves-SourceagainstISourceRepositoryProvider.PackageSourceProvider.LoadPackageSources(), and-UpdatesqueriesPrimarySourceRepositories(= just the matched-Sourcewhen one is given). The unit harness'sTestSourceRepositoryUtility.CreateSourceRepositoryProvider(IEnumerable<PackageSource>)builds a real provider — passnew PackageSource(path, "FriendlyName")to test name resolution, and register a second empty source to verify-Updates -Source EmptySourcereturns 0. Don't reach for Apex just because a scenario mentions-Source.-Source 'All'is a host-level concept, NOT a cmdlet-Sourcevalue. The aggregate "All" is handled byPowerShellHost.SetPrivateDataOnHost(the PMC source dropdown), which maps it to an empty active source. Passing-Source 'All'literally to a cmdlet hitsGetMatchingSource('All')→ null →CheckSourceValidity→ throws "Unknown source 'All'". The oldGetPackageAcceptsAllAsSourceNamePS function had noTest-prefix (never ran) and relied on this non-existent behavior — do not migrate it.
2026-08-28: DependencyVersion data-driven migration
- Related E2E scenarios that differ only by a PMC enum argument and expected package version can
share a
[DataTestMethod]with one[DataRow]per migrated function. Each row remains an independently reported Apex test while avoiding repeated Visual Studio test code. - To test dependency selection across several available versions, add the minimum dependency range
to the root package's
Dependencies, create the root without recursively creating dependencies, and create each concrete dependency version separately. - Use an explicit open-ended dependency range such as
[1.0.0,)when the original package means>= 1.0.0. Create that root withCreatePackagesWithoutDependenciesAsync, then create every concrete dependency version separately; otherwise the range placeholder is recursively created as a package and contaminates the test source. NuGetApexTestService.IsPackageInstalledonly returns direct dependencies. UseIsPackageInstalledIncludingTransitivewhen verifying a transitive package version.- Explicitly call
simpleTestPathContext.Settings.SetPackageFormatToPackagesConfig()for migrated packages.config scenarios. Otherwise the Visual Studio profile's persisted package-management default can create a legacy project as PackageReference and change PMC behavior. - Config-driven dependency behavior can share the same data-driven test as explicit
-DependencyVersionarguments. UseSimpleTestSettingsContext.SetDependencyVersionbefore creatingApexTestContext, and leave the command-line argument empty for that row.
2026-09-10: Install/Update/Uninstall bulk migration
- Assert the solution
packagesfolder viasimpleTestPathContext.PackagesV2.ApexTestContextdoes not expose it, so create your ownSimpleTestPathContextand pass it in.Assert-SolutionPackage X 1.0.0maps toCommonUtility.WaitForDirectoryExists(Path.Combine(pathContext.PackagesV2, "X.1.0.0")), andAssert-Null (Get-SolutionPackage ...)maps toWaitForDirectoryNotExists. Use the wait helpers rather thanDirectory.Existsbecause uninstall deletes the folder asynchronously. Update-Package -ToHighestPatchis an[Alias]of-Safe(UpdatePackageCommand), so PS tests for the two flags are the same scenario. Migrate them as one[DataTestMethod]with a[DataRow]per flag instead of two duplicate test methods.-ToHighestMinoris a real, separate switch.Install-Package <path>\Id.Version.nupkgworks without-Source.InstallPackageCommand.ParseUserInputForIddetects a.nupkgsuffix, derivesSourcefrom the containing directory, and parses id/version from the file name. Quote the path in theExecute()string so directories with spaces still work.-Sourceaccepts the configured source name. UseSimpleTestSettingsContext.DefaultPackageSourceName("source") rather than repeating the literal when migrating PS tests that pass a friendly source name.- Build diamond/transitive graphs with
CommonUtility.CreatePackage+Dependencies.Add, then create only the root(s) withSimpleTestPackageUtility.CreatePackagesAsync— it recursively creates every referenced dependency package, so each concrete version ends up in the source. - Watch for duplicate function names in the PS files.
UpdatePackageTest.ps1definesTest-UpdatePackageDoesNotConsiderPrereleasePackagesForSafeUpdateIfFlagIsNotSpecifiedtwice; the later definition wins at runtime. Don't include such a function in a migration set, and don't "clean up" the duplicate as a side effect of an unrelated migration. - PS functions without a
Test-prefix never run (e.g.PackageInstallAcceptsAllAsSourceName,UninstallSolutionOnlyPackageWhenAmbiguous). They are dead code, not coverage — exclude them from migration counts and leave them alone. - Build the Apex project with VS MSBuild, not
dotnet.msbuildisn't onPATH; locate it viavswhere.exe -latest -prerelease -products * -requires Microsoft.Component.MSBuild -find MSBuild\**\Bin\MSBuild.exeand pass/restore. Executing Apex tests requires a live VS host, so build-only validation is the realistic bar for a migration change.
Migrating PowerShell E2E Tests to Unit Tests
Some E2E tests exercise cmdlet behavior (parameter handling, filtering, prerelease logic) rather than Visual Studio integration. These are better covered by fast unit tests that invoke the cmdlet directly in a PowerShell runspace — no VS instance needed.
When to use unit tests vs Apex tests
| Scenario | Target |
|---|---|
| Tests that verify cmdlet parameter behavior (filtering, prerelease, -AllVersions, -Updates) | Unit test |
| Tests that verify VS integration (project creation, restore, UI, solution operations) | Apex test |
| Tests that exercise Install/Update/Uninstall side effects on actual project files | Apex test |
| Tests that only query package sources or list installed packages | Unit test |
The key distinction: if the test needs a real Visual Studio instance, solution, or project system, use Apex. If it only needs the cmdlet logic + a mock package source, use a unit test.
Unit test project
Unit tests live in:
test/NuGet.Clients.Tests/NuGetConsole.Host.PowerShell.Test/Cmdlets/
The project (NuGetConsole.Host.PowerShell.Test.csproj) references:
NuGet.PackageManagement.PowerShellCmdlets(the source project)VisualStudio.Test.Utility(shared test helpers)Microsoft.VisualStudio.Sdk.TestFramework.Xunit(VS mocking infrastructure)
Tests run sequentially via xunit.runner.json:
{
"maxParallelThreads": 1,
"parallelizeTestCollections": false
}
Test infrastructure
The reference implementation is in:
test/NuGet.Clients.Tests/NuGetConsole.Host.PowerShell.Test/Cmdlets/GetPackageCommandTests.cs
Read this file for the full working patterns. Key architectural details below.
CmdletRunspaceFixture (inner class in test file)
Creates a real PowerShell runspace with the cmdlet registered via SessionStateCmdletEntry. Key points:
- Constructor takes
activeSource(always passpathContext.PackageSource— never nuget.org) - Registers the cmdlet type (e.g.,
typeof(GetPackageCommand)) intoInitialSessionState Invoke(cmdletName, parameters)runs the cmdlet and returnsIList<PSObject>- Is
IDisposable— use withusing var
When adding tests for a different cmdlet (e.g., Install-Package), create a new fixture class that registers that cmdlet's type instead.
TestPSHost (inner class in test file)
A minimal PSHost that provides PrivateData with two properties NuGet cmdlets require:
"activePackageSource"— the source URL/path the cmdlet defaults to"CancellationTokenKey"— aCancellationToken(useCancellationToken.Nonein tests)
Without these, cmdlets throw during initialization.
VS Service Mocking (constructor)
The test class implements IAsyncServiceProvider and sets up mocks in the constructor:
- Takes
GlobalServiceProvider(from xUnit collectionMockedVS.Collection) - Mocks
IVsSolutionManager— must setIsSolutionOpen = true,IsSolutionAvailableAsync = true, and provide a default project viaGetDefaultNuGetProjectAsync() - Mocks
IComponentModel— registers all services the cmdlet'sPreprocess()method resolves:ISettings,IVsSolutionManager,ISourceControlManagerProvider,ICommonOperations,IPackageRestoreManager,IDeleteOnRestartManager,IRestoreProgressReporter, andISourceRepositoryProvider - Calls
ServiceLocator.InitializePackageServiceProvider(this)to wire it all up
If a new cmdlet requires additional services, add them to the IComponentModel mock setup.
Unit test file placement
| Cmdlet | Test file |
|---|---|
Get-Package |
Cmdlets/GetPackageCommandTests.cs |
Install-Package |
Cmdlets/InstallPackageCommandTests.cs (create if needed) |
Update-Package |
Cmdlets/UpdatePackageCommandTests.cs (create if needed) |
Uninstall-Package |
Cmdlets/UninstallPackageCommandTests.cs (create if needed) |
Find-Package |
Cmdlets/FindPackageCommandTests.cs (create if needed) |
When creating a new test file, follow the same structure as GetPackageCommandTests.cs — copy the class skeleton (constructor, IAsyncServiceProvider, inner fixture/host classes) and adapt the cmdlet type.
Key patterns
Creating test packages
Use SimpleTestPathContext + SimpleTestPackageUtility (same utilities as other NuGet tests):
using var pathContext = new SimpleTestPathContext();
await SimpleTestPackageUtility.CreatePackagesAsync(
pathContext.PackageSource,
new SimpleTestPackageContext("PackageA", "1.0.0"),
new SimpleTestPackageContext("PackageA", "2.0.0-beta"));
Setting up the source repository provider
Register a real ISourceRepositoryProvider pointing at the local test source via TestSourceRepositoryUtility.CreateSourceRepositoryProvider(new PackageSource(localPath)). See SetupSourceRepositoryProvider() in the reference file.
Setting up installed packages (for -Updates tests)
Create a mock NuGetProject with PackageReference entries and wire it into _solutionManager.GetNuGetProjectsAsync() / GetDefaultNuGetProjectAsync(). See CreateMockProject() and SetupProjectWithInstalledPackage() helpers in the reference file.
Invoking cmdlets
using var fixture = new CmdletRunspaceFixture(activeSource: pathContext.PackageSource);
var results = fixture.Invoke(
"Get-Package",
new Dictionary<string, object>
{
{ "ListAvailable", true },
{ "Source", pathContext.PackageSource },
{ "Filter", "TestPackage" },
});
Switch parameters (-ListAvailable, -AllVersions, -IncludePrerelease) are passed as true in the dictionary.
Asserting results
Results come back as PSObject wrappers. Cast BaseObject to the expected type:
| Scenario | BaseObject type |
|---|---|
-ListAvailable |
PowerShellRemotePackage |
| Installed (no switch) | PowerShellInstalledPackage |
-Updates |
PowerShellUpdatePackage |
Use FluentAssertions: results.Should().ContainSingle(), then cast and assert .Id, .Version, .Versions.
Unit test style rules
- Use xUnit (
[Fact]) with FluentAssertions. - Test class uses
[Collection(MockedVS.Collection)]and takesGlobalServiceProviderin constructor. - Method names:
{CmdletName}{Scenario}_{Expectation}Async(e.g.,GetPackageListAvailable_WithFilter_ReturnsMatchingPackageAsync). - Use
using varfor disposables. - Arrange/Act/Assert pattern with comments.
- All tests are
async Task(package creation is async). - Use
varfor locals except value tuples.
Unit test migration workflow
- Identify testable behavior — read the PS test and determine if it's testing cmdlet logic or VS integration.
- Check existing unit test coverage — look in
Cmdlets/for the relevant command test file. - Write the unit test — use the patterns above.
- Build —
dotnet build test/NuGet.Clients.Tests/NuGetConsole.Host.PowerShell.Test/NuGetConsole.Host.PowerShell.Test.csproj - Run —
dotnet test test/NuGet.Clients.Tests/NuGetConsole.Host.PowerShell.Test/NuGetConsole.Host.PowerShell.Test.csproj - Remove the PS test function — delete the migrated function from the PS E2E file.
- Keep one Apex sanity test — if migrating all PS tests for a given cmdlet to unit tests, keep (or create) one Apex test that exercises the basic end-to-end flow through VS. Unit tests mock the VS layer, so a single Apex test ensures the real integration still works. This sanity test should cover multiple scenarios in a single test case to maximize breadth without adding multiple slow VS-launching tests — e.g.,
GetPackageTestCase.cshas a lifecycle test that installs, lists, updates, and uninstalls all in one method.
Tests that should be unit tests (not Apex)
PS E2E tests that do any of the following are good candidates for unit tests:
- Only call
Get-Packagewith-ListAvailable,-Filter,-AllVersions,-Updates,-Prerelease - Assert returned package IDs/versions without modifying project state
- Test behavior with different source configurations
- Verify filtering/sorting logic
- Test prerelease inclusion/exclusion behavior