|
26 | 26 | - **`required` on private/internal types** is cleaner than `null!` field initializers. |
27 | 27 | - **TryCreate/TryGet patterns** — out params need `?`, callers use `!` after the success guard. Out parameters that are guaranteed non-null when the method returns true should be annotated with `[NotNullWhen(true)]`. Don't annotate `[NotNullWhen]` unless it's actually true for all code paths. |
28 | 28 | - **Work in batches** — group related files, fix source, fix cascading, build, repeat. If this means we need multiple pull requests for enabling nullable, that's fine. Don't try to do it all in one go. |
| 29 | + |
| 30 | +## Migrating PowerShell E2E Tests to Apex Tests |
| 31 | + |
| 32 | +### Overview |
| 33 | + |
| 34 | +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. |
| 35 | + |
| 36 | +### Template mapping |
| 37 | + |
| 38 | +| PowerShell function | Apex `ProjectTemplate` | Package management | Verified | |
| 39 | +|---|---|---|---| |
| 40 | +| `New-ConsoleApplication` | `ProjectTemplate.ConsoleApplication` | packages.config | ✅ | |
| 41 | +| `New-ClassLibrary` | `ProjectTemplate.ClassLibrary` | packages.config | ✅ | |
| 42 | +| `New-WebSite` | `ProjectTemplate.WebSiteEmpty` | packages.config | ✅ | |
| 43 | +| `New-WebApplication` | `ProjectTemplate.WebApplicationEmpty` | packages.config | ❌ | |
| 44 | +| `New-WPFApplication` | `ProjectTemplate.WPFApplication` | packages.config | ❌ | |
| 45 | +| `New-MvcApplication` | `ProjectTemplate.WebApplicationEmptyMvc` | packages.config | ❌ | |
| 46 | +| `New-FSharpLibrary` | `ProjectTemplate.FSharpLibrary` | PackageReference | ❌ | |
| 47 | +| `New-NetCoreConsoleApp` | `ProjectTemplate.NetCoreConsoleApp` | PackageReference | ✅ | |
| 48 | +| `New-NetStandardClassLib` | `ProjectTemplate.NetStandardClassLib` | PackageReference | ✅ | |
| 49 | + |
| 50 | +| PowerShell function | Apex equivalent | |
| 51 | +|---|---| |
| 52 | +| `New-SolutionFolder 'Name'` | `testContext.SolutionService.AddSolutionFolder("Name")` | |
| 53 | + |
| 54 | +### Command execution |
| 55 | + |
| 56 | +| Scenario | Apex API | |
| 57 | +|---|---| |
| 58 | +| Standard install with `-Version` | `nugetConsole.InstallPackageFromPMC(packageName, packageVersion)` | |
| 59 | +| Install with extra flags (`-Source`, `-WhatIf`, `-IgnoreDependencies`) | `nugetConsole.Execute($"Install-Package {packageName} -ProjectName {project.Name} -Source {source}")` | |
| 60 | +| Standard uninstall | `nugetConsole.UninstallPackageFromPMC(packageName)` | |
| 61 | +| Standard update | `nugetConsole.UpdatePackageFromPMC(packageName, packageVersion)` | |
| 62 | +| Any raw PMC command | `nugetConsole.Execute(command)` | |
| 63 | + |
| 64 | +**Rule:** If the PS test does not use `-Version`, use `Execute()` with the raw command string. `InstallPackageFromPMC()` always adds `-Version`. |
| 65 | + |
| 66 | +**Important:** `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. |
| 67 | + |
| 68 | +### Assertion mapping |
| 69 | + |
| 70 | +| PowerShell assertion | Apex equivalent | |
| 71 | +|---|---| |
| 72 | +| `Assert-Package $p PackageName Version` (packages.config project) | `CommonUtility.AssertPackageInPackagesConfig(VisualStudio, testContext.Project, packageName, version, Logger)` | |
| 73 | +| `Assert-Package $p PackageName` (no version, packages.config) | `CommonUtility.AssertPackageInPackagesConfig(VisualStudio, testContext.Project, packageName, Logger)` | |
| 74 | +| `Assert-Package $p PackageName Version` (PackageReference project) | `CommonUtility.AssertPackageInAssetsFile(VisualStudio, testContext.Project, packageName, version, Logger)` | |
| 75 | +| `Assert-Throws { ... } $expectedMessage` | `nugetConsole.IsMessageFoundInPMC(expectedMessage)` — PMC errors appear as text, not C# exceptions | |
| 76 | +| `Assert-Null (Get-ProjectPackage ...)` / package not installed | `CommonUtility.AssertPackageNotInPackagesConfig(VisualStudio, testContext.Project, packageName, Logger)` | |
| 77 | + |
| 78 | +### Package sources |
| 79 | + |
| 80 | +| PowerShell source | Apex equivalent | |
| 81 | +|---|---| |
| 82 | +| `$context.RepositoryRoot` or `$context.RepositoryPath` | `testContext.PackageSource` — create packages with `CommonUtility.CreatePackageInSourceAsync()` | |
| 83 | +| No `-Source` (uses nuget.org) | Create a local package with `CommonUtility.CreatePackageInSourceAsync(testContext.PackageSource, ...)` — never depend on nuget.org | |
| 84 | +| Hardcoded invalid sources (`http://example.com`, `ftp://...`) | Use the same hardcoded strings directly | |
| 85 | + |
| 86 | +### NuGet.Config manipulation |
| 87 | + |
| 88 | +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`. |
| 89 | + |
| 90 | +**Via Settings API** (preferred): |
| 91 | +```csharp |
| 92 | +using var simpleTestPathContext = new SimpleTestPathContext(); |
| 93 | +simpleTestPathContext.Settings.AddSource("PrivateRepo", privatePath); |
| 94 | +// ... then pass it in: |
| 95 | +using var testContext = new ApexTestContext(VisualStudio, projectTemplate, Logger, |
| 96 | + simpleTestPathContext: simpleTestPathContext); |
| 97 | +``` |
| 98 | + |
| 99 | +**Via raw config file** (for settings not covered by the API like `dependencyVersion` or `bindingRedirects`): |
| 100 | +```csharp |
| 101 | +using var simpleTestPathContext = new SimpleTestPathContext(); |
| 102 | +File.WriteAllText(simpleTestPathContext.NuGetConfig, |
| 103 | + $@"<?xml version=""1.0"" encoding=""utf-8""?> |
| 104 | +<configuration> |
| 105 | + <config> |
| 106 | + <add key=""dependencyVersion"" value=""HighestPatch"" /> |
| 107 | + </config> |
| 108 | + <packageSources> |
| 109 | + <clear /> |
| 110 | + <add key=""source"" value=""{simpleTestPathContext.PackageSource}"" /> |
| 111 | + </packageSources> |
| 112 | +</configuration>"); |
| 113 | + |
| 114 | +using var testContext = new ApexTestContext(VisualStudio, projectTemplate, Logger, |
| 115 | + simpleTestPathContext: simpleTestPathContext); |
| 116 | +``` |
| 117 | + |
| 118 | +### Test structure patterns |
| 119 | + |
| 120 | +**Error-path tests** (no package creation needed) — synchronous: |
| 121 | +```csharp |
| 122 | +[TestMethod] |
| 123 | +[Timeout(DefaultTimeout)] |
| 124 | +public void DescriptiveTestName_Fails() |
| 125 | +{ |
| 126 | + using var testContext = new ApexTestContext(VisualStudio, ProjectTemplate.ConsoleApplication, Logger); |
| 127 | + |
| 128 | + var packageName = "Rules"; |
| 129 | + var source = @"c:\temp\data"; |
| 130 | + var expectedMessage = $"Unable to find package '{packageName}' at source '{source}'. Source not found."; |
| 131 | + |
| 132 | + var nugetConsole = GetConsole(testContext.Project); |
| 133 | + nugetConsole.Execute($"Install-Package {packageName} -ProjectName {testContext.Project.Name} -Source {source}"); |
| 134 | + |
| 135 | + Assert.IsTrue( |
| 136 | + nugetConsole.IsMessageFoundInPMC(expectedMessage), |
| 137 | + $"Expected error message was not found in PMC output. Actual output: {nugetConsole.GetText()}"); |
| 138 | +} |
| 139 | +``` |
| 140 | + |
| 141 | +**Success-path tests** (need package creation) — async: |
| 142 | +```csharp |
| 143 | +[TestMethod] |
| 144 | +[Timeout(DefaultTimeout)] |
| 145 | +public async Task DescriptiveTestNameAsync(/* or [DataTestMethod] with ProjectTemplate */) |
| 146 | +{ |
| 147 | + using var testContext = new ApexTestContext(VisualStudio, ProjectTemplate.ConsoleApplication, Logger); |
| 148 | + |
| 149 | + var packageName = "TestPackage"; |
| 150 | + var packageVersion = "1.0.0"; |
| 151 | + await CommonUtility.CreatePackageInSourceAsync(testContext.PackageSource, packageName, packageVersion); |
| 152 | + |
| 153 | + var nugetConsole = GetConsole(testContext.Project); |
| 154 | + nugetConsole.InstallPackageFromPMC(packageName, packageVersion); |
| 155 | + |
| 156 | + CommonUtility.AssertPackageInPackagesConfig(VisualStudio, testContext.Project, packageName, packageVersion, Logger); |
| 157 | +} |
| 158 | +``` |
| 159 | + |
| 160 | +### Style rules |
| 161 | + |
| 162 | +- Use `using var` (inline declaration), not `using (var ...) { }`. |
| 163 | +- Place migrated tests before the static helper methods (`GetNetCoreTemplates`, etc.) in the file. |
| 164 | +- Method names: `{Action}FromPMC{Scenario}[_Fails|Async]`. Suffix with `_Fails` for error tests, `Async` for async tests. |
| 165 | +- Always include `[Timeout(DefaultTimeout)]`. |
| 166 | +- Include `nugetConsole.GetText()` in assertion failure messages for diagnostics. |
| 167 | + |
| 168 | +### Tests that should NOT be migrated |
| 169 | + |
| 170 | +Skip PS tests that: |
| 171 | +- Use `Assert-BindingRedirect` — binding redirect tests are already `[SkipTest]` in PS and not worth migrating. |
| 172 | +- Use `Get-ProjectItem`, `Get-ProjectItemPath`, or other VS DTE project-item inspection not available in Apex. |
| 173 | + |
| 174 | +### After migration |
| 175 | + |
| 176 | +1. Remove the migrated function from the PS test file. |
| 177 | +2. If a PS test is already covered by an existing Apex test (duplicate), just delete the PS test — no new Apex test needed. |
| 178 | +3. Verify with `get_errors` that the Apex file compiles cleanly. |
0 commit comments