- name
- apex-migration
- description
- >-
# 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
1. **Read the PS test** — understand what it does: which project type, which PMC commands, what assertions.
2. **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.
3. **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).
4. **Translate** — use the mappings in this skill to convert each PS construct to its C# equivalent.
5. **Verify** — run `get_errors` or build the project to confirm it compiles cleanly.
6. **Remove the PS function** — delete the migrated function from the PS test file.
7. **If already covered** — if an existing Apex or unit test already covers the same scenario, just delete the PS test. No new test needed.
8. **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`,
> or `New-NetCoreConsoleMultipleTargetFrameworksApp`. These don't have a 1:1 `ProjectTemplate` enum
> value — check the Apex `ProjectTemplate` enum 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:
```csharp
await CommonUtility.CreatePackageInSourceAsync(testContext.PackageSource, packageName, packageVersion);
```
For packages with dependencies:
```csharp
await CommonUtility.CreateDependenciesPackageInSourceAsync(
testContext.PackageSource, packageName, packageVersion, dependencyName, dependencyVersion);
```
For .NET Framework-specific packages:
```csharp
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):
```csharp
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`):
```csharp
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
```csharp
[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
```csharp
[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]`:
```csharp
[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:
```csharp
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`](../../../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), not `using (var ...) { }`.
- Place migrated tests before the static helper methods (`GetNetCoreTemplates`, etc.) in the file.
- Method names: `{Action}FromPMC{Scenario}[_Fails|Async]`. Suffix with `_Fails` for error tests,
`Async` for async tests.
- Always include `[Timeout(DefaultTimeout)]`.
- Always include `nugetConsole.GetText()` in assertion failure messages for diagnostics.
- The test class inherits `SharedVisualStudioHostTestClass` which provides `VisualStudio` and `Logger`.
- 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-ProjectItem` to check tree structure,
parent/child relationships). However, if the PS test only uses `Get-ProjectItem` /
`Get-ProjectItemPath` to verify a **file exists on disk**, migrate it using filesystem assertions
instead: `File.Exists(path)`, XML reads on the project file, or
`CommonUtility.WaitForFileExists()`.
## After migration checklist
1. ✅ Remove the migrated function from the PS test file.
2. ✅ If a PS test is already covered by an existing Apex test (duplicate), just delete the PS
test — no new Apex test needed.
3. ✅ Build the Apex project or run `get_errors` to verify it compiles cleanly.
4. ✅ 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 `-Source` implicitly use nuget.org. Always
replace this with local package creation via `CreatePackageInSourceAsync` — tests must not depend
on external feeds.
- **`NuGetApexTestService` limitations**: It does NOT expose `ISolutionManager` or VS DTE project
View on GitHub