Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migration Guide for NUnit 4.0 #827

Merged
merged 19 commits into from
Nov 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 11 additions & 6 deletions docs/articles/nunit/Towards-NUnit4.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,17 @@ NUnit 4 has been long-awaited, and we are now starting to see its outline taking
scheme. This entails aiming to release version 4 as soon as possible and subsequently accelerating the pace of new major
releases compared to previous versions.

We'd like to bring your attention to several interesting aspects of NUnit 4. First and foremost, there is a crucial
[NUnit 4 planning issue](https://github.com/nunit/nunit/issues/3325) that we want to highlight. Additionally, we have
an upcoming [release notes
page](https://github.com/nunit/docs/blob/62c43cbbd32b8424c974d5ec50d5463a5c4cd621/docs/articles/nunit/release-notes/framework.md),
currently in the form of a PR (Pull Request). If you're interested in changes related to supported frameworks and assert
messages, we've compiled a [list of issues](https://github.com/nunit/nunit/issues/4431) for your reference.
We'd like to bring your attention to several interesting aspects of NUnit 4.

* First and foremost, there is a crucial [NUnit 4 planning issue](https://github.com/nunit/nunit/issues/3325) that we
want to highlight.
* Additionally, we have an upcoming [release notes
page](https://github.com/nunit/docs/blob/62c43cbbd32b8424c974d5ec50d5463a5c4cd621/docs/articles/nunit/release-notes/framework.md),
currently in the form of a PR (Pull Request).
* If you're interested in changes related to supported frameworks and assert messages, we've compiled a [list of
issues](https://github.com/nunit/nunit/issues/4431) for your reference.
* We have also created a [Migration Guide](xref:migrationguidance) starting with 4.x that we hope will assist in
navigating any breaking changes.

Moreover, we have created a milestone for version 4, [which you can find a list of open issues for
here](https://github.com/nunit/nunit/issues?q=is%3Aopen+is%3Aissue+milestone%3A4.0). This milestone could be useful in
Expand Down
182 changes: 182 additions & 0 deletions docs/articles/nunit/release-notes/Nunit4.0-MigrationGuide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
---
uid: migrationguidance
---

# Migration Guidance

## 3.x -> 4.x

NUnit 4.0 has a few [breaking changes](../release-notes/breaking-changes.md#nunit-40) making it neither binary nor
source code compatible with NUnit 3.14.0

* Change to [Classic Asserts](../writing-tests/assertions/assertion-models/classic.md)
* Removal of `Assert.That` overloads with _format_ specification and `params`.

### Classic Assert migration

There are different ways to migrate these to NUnit 4.0

* Convert Classic Assert to the [Constraint model](../writing-tests/assertions/assertion-models/constraint.md)
* Update source code to new namespace and class name
* Using `global using` aliases

In the sections below we use the following simple test as an example:

```csharp
public class Tests
{
[Test]
public void TestSomeCalculation()
{
int actual = SomeCalculation();
Assert.AreEqual(42, actual, "Expected {0} to be 42", actual);
StringAssert.StartsWith("42", actualText, "Expected '{0}' to start with '42'", actualText);
}

private static int SomeCalculation() => 42;
}
```

#### Convert Classic Assert to Constraint Model

Although the code can be converted manually, that is a lot of work.

Luckily, the [NUnit.Analyzer](https://www.nuget.org/packages/NUnit.Analyzers) has had rules and associated code fixes
for a while now. Version _3.10.0_ knows about the 2nd non-backward compatible change and will convert the _format_
specification and `params` into a `FormattableString`.

> [!NOTE]
> **Caveat**: The analyzers only run when the code compiles, so execute and act on the analyzer _before_
> upgrading `nunit` to version `4.0.0`!

In our example code, the analyzer will flag the `Assert.AreEqual` as shown below:

![NUnit.Analyzer Classic Assert Warning](../../../images/NUnit.Analyzer-ClassicAssert-Warning.png)

Running the associated `Transform to constraint model` code fix in Visual Studio will convert the code into:

```csharp
public class Tests
{
[Test]
public void TestSomeCalculation()
{
int actual = SomeCalculation();
Assert.That(actual, Is.EqualTo(42), $"Expected {actual} to be 42");
}

private static int SomeCalculation() => 42;
}
```

The analyzer code fix supports _Batch Fixing_:

![NUnit.Analyzer Classic Assert CodeFix](../../../images/NUnit.Analyzer-ClassicAssert-CodeFix.png)

This allows changing all corresponding `Assert` usages for a document, project or a complete solution.

There are many classic asserts and most of these come with a separate code analyzer rule and code fix. Although it
allows full configuration to what classic asserts to keep or convert, it means that a developer has to repeat this
process multiple times, once for each assert method.

NUnit.Analyzer also has code fixers for `CollectionAssert` and `StringAssert`.

```csharp
string actualText = actual.ToString();
StringAssert.StartsWith("42", actualText, "Expected '{0}' to start with '42'", actualText);
```

Will be converted into:

```csharp
string actualText = actual.ToString();
Assert.That(actualText, Does.StartWith("42"), $"Expected '{actualText}' to start with '42'");
```

There are no code fixers for `FileAssert` and `DirectoryAssert`. They could be added, but we don't expect these to be
used too much.

#### Convert Classic Assert into NUnit 4.x equivalent

If you want to keep the Classic Asserts and not convert them to the constraint model -- but do want to use the new NUnit
4.x naming -- you'll need to update the code manually.

The NUnit.Analyzer can't help here as the code either doesn't compile before the change or after, depending on what
version of `nunit` you are compiling with.

If you _only_ use classic asserts, you can get away with a couple of global substitutes:

1. Convert `using NUnit.Framework;` into _both_ `using NUnit.Framework;using NUnit.Framework.Legacy;`

Depending on your editor you can automatically insert a newline between the two `using` statements.
1. Convert `Assert` into `ClassicAssert`.

This global substitute will also convert those asserts that have not changed. You can narrow the scope of this
substitute to do only the asserts that need converting, but there are quite a lot.

1. Convert `Assert.AreEqual` into `ClassicAssert.AreEqual`.
1. Convert `Assert.True` into `ClassicAssert.True`.
1. Similar for `IsTrue`, `False`, `IsFalse`, `Greater`, `Less`, ...

Depending on what is less work, alternatively you can reverse the substitution of those that shouldn't be have been
changed:
1. Convert `ClassicAssert.That` into `Assert.That`.
1. Convert `ClassicAssert.Fail` into `Assert.Fail`.
1. Convert `ClassicAssert.Throws` into `Assert.Throws`.
1. Etc.

Or if you use Visual Studio, it will raise an
[IDE0002](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0002) with a code fix
that can convert all of those that are not considered _classic_ back to assert in one swoop:

![ClassicAssert to Assert](../../../images/IDE0002-ClassicAssert-into-Assert.png)

#### Use `global using` aliases

If you use SDK 6.0 or newer that supports C#10, you can upgrade without modifying any actual tests by adding the
following aliases to `GlobalUsings.cs`

```csharp
global using Assert = NUnit.Framework.Legacy.ClassicAssert;
global using CollectionAssert = NUnit.Framework.Legacy.CollectionAssert;
global using StringAssert = NUnit.Framework.Legacy.StringAssert;
global using DirectoryAssert = NUnit.Framework.Legacy.DirectoryAssert;
global using FileAssert = NUnit.Framework.Legacy.FileAssert;
```

Note that this doesn't mean you have to target .NET 6.0. This also works if targeting .NET Framework as it is purely
done on the source code level.

### Assert.That with _format_ specification and `params` overload conversion

These overloads were removed to allow for better messages in case of failure. See [The "Towards NUnit 4"
article](../Towards-NUnit4.md#improved-assert-result-messages) for more information.

NUnit 4.x has been optimized such that these formattable strings only get formatted in case the test is failing.

```csharp
int actual = SomeCalculation();
Assert.That(actual, Is.EqualTo(42), "Expected {0} to be 42", actual);
```

Needs to be converted into:

```csharp
int actual = SomeCalculation();
Assert.That(actual, Is.EqualTo(42), $"Expected {actual} to be 42");
```

To make this transition easier, the Nunit.Analyzer has been updated with a new rule and corresponding code-fix:

![Replace Format Specification with Formattable String](../../../images/NUnit.Analyzer-ReplaceFormatSpecification.png)

### Using NUnit Extension libraries

If your code doesn't call `nunit` asserts directly but uses a local `NUnitExtension` library or a 3rd party one then
that dependency needs to be upgraded _before_ you can upgrade your own code.

If the library is not NUnit 4.0 compliant, you will get error messages like:

```txt
System.MissingMethodException : Method not found: 'Void NUnit.Framework.Assert.That(!!0, NUnit.Framework.Constraints.IResolveConstraint, System.String, System.Object[])'.
```
12 changes: 12 additions & 0 deletions docs/articles/nunit/release-notes/breaking-changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@ uid: breakingchanges

# Breaking Changes

## NUnit 4.0

* The [Classic Asserts](../writing-tests/assertions/assertion-models/classic.md) have been moved to a separate library
and their namespace and their class name were renamed to: `NUnit.Framework.Legacy.ClassicAssert`.
* The standalone assert classes have also been moved to the `NUnit.Framework.Legacy` namespace. These classes are:
* [CollectionAssert](../writing-tests/assertions/classic-assertions/Collection-Assert.md)
* [StringAssert](../writing-tests/assertions/classic-assertions/String-Assert.md)
* [DirectoryAssert](../writing-tests/assertions/classic-assertions/Directory-Assert.md)
* [FileAssert](../writing-tests/assertions/classic-assertions/File-Assert.md)
* Assert.That overloads with _format_ specification and `params` have been removed in favor of an overload using
`FormattableString`.

## NUnit 3.10

* `NUnit.Framework.Constraints.NUnitEqualityComparer.Default` was deprecated in favor of `new NUnitEqualityComparer()`.
Expand Down
2 changes: 2 additions & 0 deletions docs/articles/nunit/release-notes/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
href: framework.md
- name: Console and Engine
topicUid: consoleenginereleasenotes
- name: Migration Guidance
topicUid: migrationguidance
- name: Breaking Changes
topicUid: breakingchanges
- name: Pre-3.5 Release notes
Expand Down
Binary file added docs/images/IDE0002-ClassicAssert-into-Assert.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.