Use this skill before answering, planning, or editing any MSTest 3.x-to-4.x upgrade or post-upgrade failure. Triggers include "MSTest v4 breaking changes"; CS0507/CS0103/CS1061/CS1615; ExecuteAsync, CallerInfo, DisplayName, or custom TestMethodAttribute; ClassCleanupBehavior; ContainsKey; ThrowsExactly or ExpectedException; IsInstanceOfType out parameters; TestTimeout.Infinite; ManagedType; net6/net7 compatibility; TestCase.Id history; TestName in ClassInitialize; TreatDiscoveryWarningsAsErro...
Scanned 9/5/2026
Install to Claude Code
npx -y skills add managedcode/dotnet-skills --skill migrate-mstest-v3-to-v4 --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Migrate Mstest V3 To V4?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/managedcode-migrate-mstest-v3-to-v4-dotnet-skills)More formats (shields.io, HTML) on the badges page.
---
name: migrate-mstest-v3-to-v4
description: >
Use this skill before answering, planning, or editing any MSTest 3.x-to-4.x
upgrade or post-upgrade failure. Triggers include "MSTest v4 breaking
changes"; CS0507/CS0103/CS1061/CS1615; ExecuteAsync, CallerInfo, DisplayName,
or custom TestMethodAttribute; ClassCleanupBehavior; ContainsKey;
ThrowsExactly or ExpectedException; IsInstanceOfType out parameters;
TestTimeout.Infinite; ManagedType; net6/net7 compatibility; TestCase.Id
history; TestName in ClassInitialize; TreatDiscoveryWarningsAsErrors;
discovery errors after a clean build; and MSTest.Sdk/MTP or vstest.console
discovery changes. Do not use for v1/v2-to-v3 leftovers, framework
conversion, runner-only migration, or a general .NET upgrade.
license: MIT
---
# MSTest v3 -> v4 Migration
Migrate a test project from MSTest v3 to MSTest v4. The outcome is a project using MSTest v4 that builds cleanly, passes tests, and accounts for every source-incompatible and behavioral change. MSTest v4 is **not binary compatible** with MSTest v3 -- any library compiled against v3 must be recompiled against v4.
## First Action
Inspect the supplied project and source before searching the web or answering
from memory. Classify the request as a focused source fix, runtime behavior
change, CI discovery issue, compatibility question, or full migration, then
follow the matching row below. A clean compile does not exclude this skill:
discovery failures, `TestContext` lifecycle exceptions, and test-history
changes are runtime migration failures.
## When to Use
- Upgrading `MSTest.TestFramework`, `MSTest.TestAdapter`, or `MSTest` metapackage from 3.x to 4.x
- Upgrading `MSTest.Sdk` from 3.x to 4.x
- Fixing build errors after updating to MSTest v4 packages
- Resolving behavioral changes in test execution after upgrading to MSTest v4
- Updating custom `TestMethodAttribute` or `ConditionBaseAttribute` implementations for v4
## When Not to Use
- The project already uses MSTest v4 and builds cleanly -- migration is done
- Upgrading from MSTest v1 or v2 -- use `migrate-mstest-v1v2-to-v3` first, then return here
- The project does not use MSTest
- Migrating between test frameworks (e.g., MSTest to xUnit or NUnit)
## Inputs
| Input | Required | Description |
|-------|----------|-------------|
| Project or solution path | No | The `.csproj`, `.sln`, or `.slnx` entry point containing MSTest test projects. **Discover it yourself** by globbing the working directory; ask only when nothing is found or the choice is genuinely ambiguous |
| Build command | No | How to build (e.g., `dotnet build`, a repo build script). Auto-detect if not provided |
| Test command | No | How to run tests (e.g., `dotnet test`). Auto-detect if not provided |
## Decisions That Change the Result
| Detected request or state | Required action |
|---|---|
| Files are supplied in the current workspace | Search there and open the literal returned paths. The skill directory is not the project directory. If one tool rejects a valid path, retry with another available reader/editor; do not ask the user for a path you can discover. |
| User asks to apply changes to supplied files: "fix my project/files", "please update this source", "make the changes", or "then build and run" | Edit every affected occurrence. Run the narrowest meaningful build/test command against the actual package version; skill activation is not a reason to stop at advice. |
| User asks "what should I expect?", "how do I fix these changes?", for compatibility advice, or for a plan | Answer directly from the actual project state even when source is visible. Keep a single-symptom answer focused; include only adjacent risks that change the decision. |
| Unsupported TFM in a full migration | Update the TFM first, then update MSTest packages, then fix source breaks. Do not bury this order in a release-note inventory. |
| Custom `TestMethodAttribute` subclass | Treat `ExecuteAsync`, CallerInfo propagation, display-name handling, and the subclass's retry/result semantics as one coupled migration. Fix the actual class, not a placeholder example. |
| `MSTest.Sdk` v4 source/API errors (`ManagedType`, `TestTimeout`, `Contains`) | Give the exact source replacements, then add the adjacent runner warning: MTP mode no longer supplies `Microsoft.NET.Test.Sdk`; add it only if VSTest discovery is still required. |
| `MSTest.Sdk` v4 plus `vstest.console` | This is a v4 change: MTP mode no longer brings `Microsoft.NET.Test.Sdk`. Keep MTP and add that package for transitional VSTest discovery, opt into `UseVSTest`, or migrate CI to `dotnet test`; state which runner the choice preserves. |
## Response Guidelines
- **Always identify the current version first**: Before recommending any migration steps, explicitly state the current MSTest version detected in the project (e.g., "Your project uses MSTest v3 (3.8.0)"). This confirms you've read the project files and grounds the migration advice.
- **Resolve, do not assume, the target version**: When the user asks for "latest", query the project's configured package source and select the latest stable MSTest v4 version available at execution time. Never copy the example version from this skill into the result without checking it. Keep all MSTest packages on the same resolved version.
- **Focused fix requests** (user has specific compilation errors after upgrading): Address only the relevant breaking changes from Step 3. Make edits only when the requested deliverable is a source change; "how do I fix these?" remains an answer request. **Always provide concrete fixed code** using the user's actual types and method names. If the fixture still references v3, do not claim that a green v3 build verifies v4 compatibility; either update packages when requested or state the verification boundary. For custom `TestMethodAttribute` subclasses, show the full fixed class including CallerInfo propagation to the base constructor. Mention any related analyzer that could have caught this earlier (e.g., MSTEST0006 for ExpectedException). When the project uses `MSTest.Sdk`, also state that v4 MTP mode no longer supplies `Microsoft.NET.Test.Sdk` and whether that affects the visible runner. Do not walk through the entire migration workflow.
- **"What to expect" questions** (user asks about breaking changes before upgrading): Present ALL major breaking changes from the Step 3 quick-lookup table -- not just the ones visible in the current code. For each, provide a one-line fix summary. Also mention key behavioral changes from Step 4 (especially TestCase.Id history impact and TreatDiscoveryWarningsAsErrors default). If project code is available, highlight which changes apply directly.
- **Full migration requests** (user wants complete migration): Follow the complete workflow below.
- **Behavioral/runtime symptom reports** (user describes test execution differences without build errors): Match described symptoms to the behavioral changes table in Step 4. Provide targeted, symptom-specific advice. Mention other behavioral changes the user should watch for. Do not walk through source breaking changes unless the user also has build errors.
- **CI/test-discovery issues** (tests not discovered, vstest.console stopped working, CI pipeline failures after upgrading): Focus on 4.5 (MSTest.Sdk v4 no longer includes Microsoft.NET.Test.Sdk in its default MTP mode -- it is still needed for vstest.console) and 4.4 (TreatDiscoveryWarningsAsErrors). Explain the root cause clearly and give all three paths: add Microsoft.NET.Test.Sdk while preserving MTP, set `UseVSTest` to switch the project runner, or switch CI to MTP-native `dotnet test`. Do not walk through the full migration workflow.
- **Explanatory questions** (user asks "is this a known change?", "what else should I watch out for?"): Explain the relevant changes and advise. Mention related changes the user might encounter next. Do not prescribe a full migration procedure.
- **Result proof**: End implementation work with the detected v3 version, resolved v4 version, runner choice, files changed, and actual build/test counts. Never report a build, VSTest compatibility, discovery, or passing tests from inference.
## Workflow
> **Commit strategy:** Do not create commits unless the user asks. Keep package,
> source, and behavioral changes logically separable in the diff, but finish and
> verify the requested migration.
### Step 1: Assess the project
1. Identify the current MSTest version by checking package references for `MSTest`, `MSTest.TestFramework`, `MSTest.TestAdapter`, or `MSTest.Sdk` in `.csproj`, `Directory.Build.props`, or `Directory.Packages.props`.
2. Confirm the project is on MSTest v3 (3.x). If on v1 or v2, use `migrate-mstest-v1v2-to-v3` first.
3. Check target framework(s) -- MSTest v4 drops support for .NET Core 3.1 through .NET 7. Supported target frameworks are: **net8.0**, **net9.0**, **net462** (.NET Framework 4.6.2+), **uap10.0.16299** (UWP), **net9.0-windows10.0.17763.0** (modern UWP), and **net8.0-windows10.0.18362.0** (WinUI).
4. Check for custom `TestMethodAttribute` subclasses -- these require changes in v4.
5. Check for usages of `ExpectedExceptionAttribute` -- removed in v4 (deprecated since v3 with analyzer MSTEST0006).
6. Check for usages of `Assert.ThrowsException` (deprecated) -- removed in v4.
7. Run a clean build to establish a baseline of existing errors/warnings.
### Step 2: Update packages to MSTest v4
First resolve the latest stable v4 version from the configured package source. Pin that exact
version consistently in the metapackage, individual packages, `MSTest.Sdk`, and central package
management.
- For the `MSTest` metapackage, update its `PackageReference` to the resolved exact version.
- For individual packages, update `MSTest.TestFramework` and `MSTest.TestAdapter` to that same version.
- For `MSTest.Sdk`, update the SDK version in the project or `global.json` pin to that same version.
Run `dotnet restore`, then `dotnet build`. Collect all errors for Step 3.
### Step 3: Resolve source breaking changes
Work through compilation errors systematically. Use this quick-lookup table to identify all applicable changes, then apply each fix:
| Error / Pattern in code | Breaking change | Fix |
|---|---|---|
| Custom `TestMethodAttribute` overrides `Execute` | Execute removed | Change to `ExecuteAsync` returning `Task<TestResult[]>` (3.1) |
| `[TestMethod("name")]` or custom attribute constructor | CallerInfo params added | Use `DisplayName = "name"` named param; propagate CallerInfo in subclasses (3.2) |
| `ClassCleanupBehavior.EndOfClass` | Enum removed | Remove argument: just `[ClassCleanup]` (3.3) |
| `TestContext.Properties.Contains("key")` | `Properties` is `IDictionary<string, object>` | Change to `ContainsKey("key")` (3.4) |
| `[Timeout(TestTimeout.Infinite)]` | `TestTimeout` enum removed | Replace with `[Timeout(int.MaxValue)]` (3.5) |
| `TestContext.ManagedType` | Property removed | Use `FullyQualifiedTestClassName` (3.6) |
| `Assert.AreEqual(a, b, "msg {0}", arg)` | Message+params overloads removed | Use string interpolation: `$"msg {arg}"` (3.7) |
| `Assert.ThrowsException<T>(...)` | Renamed | Replace with `Assert.ThrowsExactly<T>(...)` or `Assert.Throws<T>(...)` (3.7) |
| `Assert.IsInstanceOfType<T>(obj, out var t)` | Out parameter removed | Use `var t = Assert.IsInstanceOfType<T>(obj)` (3.7) |
| `[ExpectedException(typeof(T))]` | Attribute removed | Move assertion into test body: `Assert.ThrowsExactly<T>(() => ...)` (3.8) |
| Project targets net5.0, net6.0, or net7.0 | TFM dropped | Change to net8.0 or net9.0 (3.9) |
> **Important**: Scan the entire project for ALL patterns above before starting fixes. Multiple breaking changes often coexist in the same project.
#### 3.1 TestMethodAttribute.Execute -> ExecuteAsync
If you have custom `TestMethodAttribute` subclasses that override `Execute`, change to `ExecuteAsync`. This change was made because the v3 synchronous `Execute` API caused deadlocks when test code used `async`/`await` internally -- the synchronous wrapper would block the thread while the async operation needed that same thread to complete.
```csharp
// Before (v3)
public sealed class MyTestMethodAttribute : TestMethodAttribute
{
public override TestResult[] Execute(ITestMethod testMethod)
{
// custom logic
return result;
}
}
// After (v4) -- Option A: wrap synchronous logic with Task.FromResult
public sealed class MyTestMethodAttribute : TestMethodAttribute
{
public override Task<TestResult[]> ExecuteAsync(ITestMethod testMethod)
{
// custom logic (synchronous)
return Task.FromResult(result);
}
}
// After (v4) -- Option B: make properly async
public sealed class MyTestMethodAttribute : TestMethodAttribute
{
public override async Task<TestResult[]> ExecuteAsync(ITestMethod testMethod)
{
// custom async logic
return await base.ExecuteAsync(testMethod);
}
}
```
Use `Task.FromResult` when your override logic is purely synchronous. Use `async`/`await` when you call `base.ExecuteAsync` or other async methods.
#### 3.2 TestMethodAttribute CallerInfo constructor
`TestMethodAttribute` now uses `[CallerFilePath]` and `[CallerLineNumber]` parameters in its constructor.
**If you inherit from TestMethodAttribute**, propagate caller info to the base class:
```csharp
public class MyTestMethodAttribute : TestMethodAttribute
{
public MyTestMethodAttribute(
[CallerFilePath] string callerFilePath = "",
[CallerLineNumber] int callerLineNumber = -1)
: base(callerFilePath, callerLineNumber)
{
}
}
```
If the subclass has its own display-name constructor, do not pass that string to
the v4 base constructor. Propagate only caller information and assign the
`DisplayName` property:
```csharp
public sealed class NamedTestMethodAttribute : TestMethodAttribute
{
public NamedTestMethodAttribute(
string displayName,
[CallerFilePath] string callerFilePath = "",
[CallerLineNumber] int callerLineNumber = -1)
: base(callerFilePath, callerLineNumber)
{
DisplayName = displayName;
}
}
```
**If you use `[TestMethodAttribute("Custom display name")]`**, switch to the named parameter syntax:
```csharp
// Before (v3)
[TestMethodAttribute("Custom display name")]
// After (v4)
[TestMethodAttribute(DisplayName = "Custom display name")]
```
#### 3.3 ClassCleanupBehavior enum removed
The `ClassCleanupBehavior` enum is removed. In v3, this enum controlled whether class cleanup ran at end of class (`EndOfClass`) or end of assembly (`EndOfAssembly`). In v4, class cleanup always runs at end of class. Remove the enum argument:
```csharp
// Before (v3)
[ClassCleanup(ClassCleanupBehavior.EndOfClass)]
public static void ClassCleanup(TestContext testContext) { }
// After (v4)
[ClassCleanup]
public static void ClassCleanup(TestContext testContext) { }
```
If you previously used `ClassCleanupBehavior.EndOfAssembly`, move that cleanup logic to an `[AssemblyCleanup]` method instead.
#### 3.4 TestContext.Properties type change
`TestContext.Properties` changed from `IDictionary` to `IDictionary<string, object>`. Update any `Contains` calls to `ContainsKey`:
```csharp
// Before (v3)
testContext.Properties.Contains("key");
// After (v4)
testContext.Properties.ContainsKey("key");
```
#### 3.5 TestTimeout enum removed
The `TestTimeout` enum (with only `TestTimeout.Infinite`) is removed. Replace with `int.MaxValue`:
```csharp
// Before (v3)
[Timeout(TestTimeout.Infinite)]
// After (v4)
[Timeout(int.MaxValue)]
```
#### 3.6 TestContext.ManagedType removed
The `TestContext.ManagedType` property is removed. Use `TestContext.FullyQualifiedTestClassName` instead.
#### 3.7 Assert API signature changes
- **Message + params removed**: Assert methods that accepted both `message` and `object[]` parameters now accept only `message`. Use string interpolation instead of format strings:
```csharp
// Before (v3)
Assert.AreEqual(expected, actual, "Expected {0} but got {1}", expected, actual);
// After (v4)
Assert.AreEqual(expected, actual, $"Expected {expected} but got {actual}");
```
- **Assert.ThrowsException renamed**: The `Assert.ThrowsException` APIs are renamed. Use `Assert.ThrowsExactly` (strict type match) or `Assert.Throws` (accepts derived exception types):
```csharp
// Before (v3)
Assert.ThrowsException<InvalidOperationException>(() => DoSomething());
// After (v4) -- exact type match (same behavior as old ThrowsException)
Assert.ThrowsExactly<InvalidOperationException>(() => DoSomething());
// After (v4) -- also catches derived exception types
Assert.Throws<InvalidOperationException>(() => DoSomething());
```
- **Assert.IsInstanceOfType out parameter changed**: `Assert.IsInstanceOfType<T>(x, out var t)` changes to `var t = Assert.IsInstanceOfType<T>(x)`:
```csharp
// Before (v3)
Assert.IsInstanceOfType<MyType>(obj, out var typed);
// After (v4)
var typed = Assert.IsInstanceOfType<MyType>(obj);
```
Apply this assignment rewrite to every occurrence, preserving the concrete asserted type and all
later uses of the typed variable. When source is available, show or edit the actual method rather
than substituting a generic `MyType` example, then verify that the project compiles.
- **Assert.AreEqual for IEquatable\<T\> removed**: If you get generic type inference errors, explicitly specify the type argument as `object`.
#### 3.8 ExpectedExceptionAttribute removed
The `[ExpectedException]` attribute is removed in v4. In MSTest 3.2, the `MSTEST0006` analyzer was introduced to flag `[ExpectedException]` usage and suggest migrating to `Assert.ThrowsExactly` while still on v3 (a non-breaking change). In v4, the attribute is gone entirely. Migrate to `Assert.ThrowsExactly`:
```csharp
// Before (v3)
[ExpectedException(typeof(InvalidOperationException))]
[TestMethod]
public void TestMethod()
{
MyCall();
}
// After (v4)
[TestMethod]
public void TestMethod()
{
Assert.ThrowsExactly<InvalidOperationException>(() => MyCall());
}
```
**When the test has setup code before the throwing call**, wrap only the throwing call in the lambda -- keep Arrange/Act separation clear:
```csharp
// Before (v3)
[ExpectedException(typeof(ArgumentNullException))]
[TestMethod]
public void Validate_NullInput_Throws()
{
var service = new ValidationService();
service.Validate(null); // throws here
}
// After (v4)
[TestMethod]
public void Validate_NullInput_Throws()
{
var service = new ValidationService();
Assert.ThrowsExactly<ArgumentNullException>(() => service.Validate(null));
}
```
**For async test methods**, use `Assert.ThrowsExactlyAsync`:
```csharp
// Before (v3)
[ExpectedException(typeof(HttpRequestException))]
[TestMethod]
public async Task FetchData_BadUrl_Throws()
{
await client.GetAsync("https://localhost:0");
}
// After (v4)
[TestMethod]
public async Task FetchData_BadUrl_Throws()
{
await Assert.ThrowsExactlyAsync<HttpRequestException>(
() => client.GetAsync("https://localhost:0"));
}
```
**If `[ExpectedException]` used the `AllowDerivedTypes` property**, use `Assert.ThrowsAsync<T>` (base type matching) instead of `Assert.ThrowsExactlyAsync<T>` (exact type matching).
For a focused migration, convert every attributed method in the supplied source, wrap only the
statement expected to throw, preserve arrange/setup statements outside the lambda, and run the
affected tests. A prose-only API substitution is incomplete when editable project files are present.
#### 3.9 Dropped target frameworks
MSTest v4 supports **.NET 8 and later** and **.NET Framework 4.6.2 and later**. Platform-specific supported targets also include **uap10.0.16299** (UWP), with modern UWP and WinUI using their corresponding supported Windows-specific .NET TFMs. .NET Core 3.1 through .NET 7 are dropped.
If the test project targets an unsupported framework, update `TargetFramework`:
```xml
<!-- Before -->
<TargetFramework>net6.0</TargetFramework>
<!-- After -->
<TargetFramework>net8.0</TargetFramework>
```
#### 3.10 Unfolding strategy moved to TestMethodAttribute
The `UnfoldingStrategy` property (introduced in MSTest 3.7) has moved from individual data source attributes (`DataRowAttribute`, `DynamicDataAttribute`) to `TestMethodAttribute`.
#### 3.11 ConditionBaseAttribute.ShouldRun renamed
The `ConditionBaseAttribute.ShouldRun` property is renamed to `IsConditionMet`.
#### 3.12 Internal/removed types
Several types previously public are now internal or removed:
- `MSTestDiscoverer`, `MSTestExecutor`, `AssemblyResolver`, `LogMessageListener`
- `TestExecutionManager`, `TestMethodInfo`, `TestResultExtensions`
- `UnitTestOutcomeExtensions`, `GenericParameterHelper`
- `ITestMethod` in PlatformServices assembly (the one in TestFramework is unchanged)
If your code references any of these, find alternative approaches or remove the dependency.
### Step 4: Address behavioral changes
These changes won't cause build errors but may affect test runtime behavior.
| Symptom | Cause | Fix |
|---|---|---|
| Tests show as new in Azure DevOps / test history lost | `TestCase.Id` generation changed (4.3) | No code fix; history will re-baseline |
| `TestContext.TestName` throws in `[ClassInitialize]` | v4 enforces lifecycle scope (4.2) | Move access to `[TestInitialize]` or test methods |
| Tests not discovered / discovery failures | `TreatDiscoveryWarningsAsErrors` now true (4.4) | Fix warnings, or set to false in .runsettings |
| Tests hang that didn't before | AppDomain disabled by default (4.1) | Set `DisableAppDomain` to false in .runsettings `RunConfiguration` |
| vstest.console can't find tests with MSTest.Sdk after the v4 upgrade | MSTest.Sdk defaults to MTP; v4 stopped adding `Microsoft.NET.Test.Sdk` in MTP mode (4.5) | Add an explicit package while preserving MTP, set `UseVSTest`, or switch CI to `dotnet test` |
| New warnings from analyzers | Analyzer severities upgraded (4.6) | Fix warnings or suppress in .editorconfig |
#### 4.1 DisableAppDomain defaults to true
AppDomains are disabled by default. On .NET Framework, when running inside testhost (the default for `dotnet test` and VS), MSTest re-enables AppDomains automatically. If you need to explicitly control AppDomain isolation, set it via `.runsettings`:
```xml
<RunSettings>
<RunConfiguration>
<DisableAppDomain>false</DisableAppDomain>
</RunConfiguration>
</RunSettings>
```
#### 4.2 TestContext throws when used incorrectly
MSTest v4 now throws when accessing test-specific properties in the wrong lifecycle stage:
- `TestContext.FullyQualifiedTestClassName` -- cannot be accessed in `[AssemblyInitialize]`
- `TestContext.TestName` -- cannot be accessed in `[AssemblyInitialize]` or `[ClassInitialize]`
**Fix**: Move any code that accesses `TestContext.TestName` from `[ClassInitialize]` to `[TestInitialize]` or individual test methods, where per-test context is available. Do not replace `TestName` with `FullyQualifiedTestClassName` as a workaround -- they have different semantics.
#### 4.3 TestCase.Id generation changed
The generation algorithm for `TestCase.Id` has changed to fix long-standing bugs. This may affect Azure DevOps test result tracking (e.g., test failure tracking over time). There is no code fix needed, but be aware of test result history discontinuity.
#### 4.4 TreatDiscoveryWarningsAsErrors defaults to true
v4 uses stricter defaults. Discovery warnings are now treated as errors, which means tests that previously ran despite discovery issues may now fail entirely. If you see unexpected test failures after upgrading (not build errors, but tests not being discovered), check for discovery warnings. To restore v3 behavior while you investigate:
```xml
<RunSettings>
<MSTest>
<TreatDiscoveryWarningsAsErrors>false</TreatDiscoveryWarningsAsErrors>
</MSTest>
</RunSettings>
```
> **Recommended**: Fix the underlying discovery warnings rather than suppressing this setting.
#### 4.5 MSTest.Sdk and vstest.console compatibility
MSTest.Sdk defaults to Microsoft.Testing.Platform (MTP) mode. MSTest.Sdk v3
still added `Microsoft.NET.Test.Sdk` in that mode; v4 removes the unnecessary
reference. A CI pipeline that separately invokes `vstest.console` can therefore
drop to zero discovered tests immediately after the v4 upgrade.
**Option A -- Preserve MTP and transitional VSTest discovery**: Add the exact
compatible `Microsoft.NET.Test.Sdk` package explicitly. This is the least
disruptive fix when MTP remains the primary runner but an existing
`vstest.console` job cannot be removed yet:
Use a direct `PackageReference` with the exact compatible version resolved from
the configured feed. Under Central Package Management, add or update the
`Microsoft.NET.Test.Sdk` `PackageVersion` in `Directory.Packages.props` and keep
the project reference versionless. Do not copy a fixed example version.
Verify with the actual `vstest.console` command; a passing `dotnet test` MTP run
does not prove VSTest discovery.
**Option B -- Switch the project to VSTest mode**: Set the `UseVSTest` property.
MSTest.Sdk then adds `Microsoft.NET.Test.Sdk`:
```xml
<PropertyGroup>
<UseVSTest>true</UseVSTest>
</PropertyGroup>
```
Keep the resolved exact `MSTest.Sdk` v4 pin from Step 2; this option changes the runner, not the
selected MSTest version or target framework.
**Option C -- Switch CI to `dotnet test`**: Replace `vstest.console` invocations in your CI pipeline with `dotnet test`. This works natively with MTP and is the recommended long-term approach for MSTest.Sdk projects.
Do not say this behavior predates v4: removal of the transitive
`Microsoft.NET.Test.Sdk` reference in MTP mode is one of the v4 behavioral
breaking changes.
#### 4.6 Analyzer severity changes
Multiple analyzers have been upgraded from Info to Warning by default:
- MSTEST0001, MSTEST0007, MSTEST0017, MSTEST0023, MSTEST0024, MSTEST0025
- MSTEST0030, MSTEST0031, MSTEST0032, MSTEST0035, MSTEST0037, MSTEST0045
Review and fix any new warnings, or suppress them in `.editorconfig` if intentional.
### Step 5: Verify
1. Run `dotnet build` -- confirm zero errors and review any new warnings
2. Run `dotnet test` -- confirm all tests pass
3. Compare test results (pass/fail counts) to the pre-migration baseline
4. If using Azure DevOps test tracking, be aware that `TestCase.Id` changes may affect history continuity
5. Check that no tests were silently dropped due to stricter discovery
## Validation
- [ ] All MSTest packages updated to 4.x
- [ ] Project builds with zero errors
- [ ] All tests pass with `dotnet test`
- [ ] Custom `TestMethodAttribute` subclasses updated for `ExecuteAsync` and CallerInfo
- [ ] `ExpectedExceptionAttribute` replaced with `Assert.ThrowsExactly`
- [ ] `Assert.ThrowsException` replaced with `Assert.ThrowsExactly` (or `Assert.Throws`)
- [ ] `ClassCleanupBehavior` enum usages removed
- [ ] `TestContext.Properties.Contains` updated to `ContainsKey`
- [ ] All target frameworks are net8.0+, net9.0, net462+, uap10.0.16299, or WinUI
- [ ] Behavioral changes reviewed and addressed
- [ ] No tests were lost during migration (compare test counts)
## Related Skills
- `writing-mstest-tests` -- for modern MSTest v4 assertion APIs and test authoring best practices
- `run-tests` -- for running tests after migration
## Common Pitfalls
| Pitfall | Solution |
|---------|----------|
| Custom `TestMethodAttribute` still overrides `Execute` | Change to `ExecuteAsync` returning `Task<TestResult[]>` |
| `TestMethodAttribute("display name")` no longer compiles | Use `TestMethodAttribute(DisplayName = "display name")` |
| `ClassCleanupBehavior` enum not found | Remove the enum argument; `[ClassCleanup]` now always runs at end of class. For end-of-assembly cleanup, use `[AssemblyCleanup]` |
| `TestContext.Properties.Contains` missing | Use `ContainsKey` -- `Properties` is now `IDictionary<string, object>` |
| `ExpectedException` attribute not found | Replace with `Assert.ThrowsExactly<T>(() => ...)` inside the test body |
| `Assert.ThrowsException` not found | Replace with `Assert.ThrowsExactly` (or `Assert.Throws` for derived types) |
| `Assert.AreEqual` with format string args fails | Use string interpolation: `$"message {value}"` |
| Tests hang that didn't before | AppDomain is disabled by default; on .NET Fx in testhost it is re-enabled automatically |
| Azure DevOps test history breaks | Expected -- `TestCase.Id` generation changed; no code fix, results will re-baseline |
| Discovery warnings now fail the run | `TreatDiscoveryWarningsAsErrors` is true by default; fix the discovery warnings |
| Net6.0/net7.0 targets don't compile | Update to net8.0 -- MSTest v4 supports net8.0, net9.0, net462, uap10.0.16299, modern UWP, and WinUI |
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!