Skip to content

Commit fb8c1be

Browse files
committed
Add v8 upgrade guide
1 parent 44132b4 commit fb8c1be

File tree

2 files changed

+149
-0
lines changed

2 files changed

+149
-0
lines changed

docs/_data/navigation.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,3 +107,5 @@ sidebar:
107107
url: /upgradingtov6
108108
- title: Upgrading to 7.0
109109
url: /upgradingtov7
110+
- title: Upgrading to 8.0
111+
url: /upgradingtov8

docs/_pages/upgradingtov8.md

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
---
2+
title: Upgrading to version 8.0
3+
permalink: /upgradingtov8
4+
layout: single
5+
toc: true
6+
sidebar:
7+
nav: "sidebar"
8+
---
9+
10+
## From `Execute.Assertion` to `AssertionChain`
11+
12+
We've made quite some changes to the API that you use to build your own assertions. For example, the `BooleanAssertions` class was instantiated in `AssertionExtensions` like this:
13+
14+
```csharp
15+
public static BooleanAssertions Should(this bool actualValue)
16+
{
17+
return new BooleanAssertions(actualValue);
18+
}
19+
```
20+
21+
On turn, the `BooleanAssertions` would expose a `BeTrue` method
22+
23+
```csharp
24+
public AndConstraint<TAssertions> BeTrue(string because = "", params object[] becauseArgs)
25+
{
26+
Execute.Assertion
27+
.ForCondition(Subject == true)
28+
.BecauseOf(because, becauseArgs)
29+
.FailWith("Expected {context:boolean} to be {0}{reason}, but found {1}.", true, Subject);
30+
31+
return new AndConstraint<TAssertions>((TAssertions)this);
32+
}
33+
```
34+
35+
To be able to support chaining multiple assertions where the chained assertion can extend the caller identification, we introduced an `AssertionChain` class which instance can flow from one assertion to another. Because of that, the above code changed to:
36+
37+
```csharp
38+
public static BooleanAssertions Should(this bool actualValue)
39+
{
40+
return new BooleanAssertions(actualValue, AssertionChain.GetOrCreate());
41+
}
42+
```
43+
44+
Notice how we pass the call to `AssertionChain.GetOrCreate` to the assertions class? By default `GetOrCreate` will create a new instance of `AssertionChain`. But if the previous assertion method uses `AssertionChain.ReuseOnce`, `GetOrCreate` will return that reused instance only once.
45+
46+
The new `BeTrue` now looks like:
47+
48+
```csharp
49+
public AndConstraint<TAssertions> BeTrue(string because = "", params object[] becauseArgs)
50+
{
51+
assertionChain
52+
.ForCondition(Subject == true)
53+
.BecauseOf(because, becauseArgs)
54+
.FailWith("Expected {context:boolean} to be {0}{reason}, but found {1}.", true, Subject);
55+
56+
return new AndConstraint<TAssertions>((TAssertions)this);
57+
}
58+
```
59+
60+
So all of the methods to build an assertion that used to live on the `AssertionScope` (which is what `Execute.Assertion` returned), have now moved to `AssertionChain`. This is great because it allows the second assertion to get access to the state of the first assertion. For instance, if the first assertion failed, any successive attempts to call `FailWith` will not do anything.
61+
62+
## No more `ClearExpectation`
63+
64+
If you wanted to reuse the first part of the failure message across multiple failures, you could use the following construct (example taken from `TimeOnlyAssertions.BeCloseTo`):
65+
66+
```csharp
67+
Execute.Assertion
68+
.BecauseOf(because, becauseArgs)
69+
.WithExpectation("Expected {context:the time} to be within {0} from {1}{reason}, ", precision, nearbyTime)
70+
.ForCondition(Subject is not null)
71+
.FailWith("but found <null>.")
72+
.Then
73+
.ForCondition(Subject?.IsCloseTo(nearbyTime, precision) == true)
74+
.FailWith("but {0} was off by {1}.", Subject, difference)
75+
.Then
76+
.ClearExpectation();
77+
```
78+
79+
When using an `using new AssertionScope()` construct to wrap multiple assertions, all assertions executed within that scope will reuse the same instance of `AssertionScope` (which is what `Execute.Assertion` returned). The problem was that you had to explicitly call `ClearExpectation` to prevent the failure message passed to `WithExpectation` to leak into the next assertion within that scope. People often forgot that.
80+
81+
We solved this in v7, by making `WithExpectation` use a nested construct. This is what it now looks like:
82+
83+
```csharp
84+
assertionChain
85+
.BecauseOf(because, becauseArgs)
86+
.WithExpectation("Expected {context:the time} to be within {0} from {1}{reason}, ", precision, nearbyTime, chain => chain
87+
.ForCondition(Subject is not null)
88+
.FailWith("but found <null>.")
89+
.Then
90+
.ForCondition(Subject?.IsCloseTo(nearbyTime, precision) == true)
91+
.FailWith("but {0} was off by {1}.", Subject, difference)
92+
);
93+
```
94+
95+
All the code nested within the `WithExpectation` will share the first part of the failure message, and there's no need to explicitly clear it anymore.
96+
97+
## Amending caller identifiers with `WithPostfix`
98+
99+
Imagine the following chained assertion
100+
101+
```csharp
102+
var element = XElement.Parse(
103+
"""
104+
<parent>
105+
<child />
106+
<child />
107+
</parent>
108+
""");
109+
110+
111+
element.Should().HaveElement("child", AtLeast.Twice()).Which.Should().HaveCount(1);
112+
```
113+
114+
Prior to version 7, if the `HaveElement` assertion succeeded, but the `NotBeNull` failed, you would get the following exception:
115+
116+
Expected element to contain 1 item(s), but found 3: {<child />, <child />, <child />}.
117+
118+
Now, in v7, it'll will return the following:
119+
120+
Expected element/child to contain 1 item(s), but found 3: {<child />, <child />, <child />}.
121+
122+
This is possible because `HaveElement` will pass the `AssertionChain` through `ReuseOnce` to the succeeding `HaveCount()` _and_ amend the automatically detected caller identifier `element` (the part on which the first `Should` is invoked) with `"/child"` using `WithCallerPostfix`. Since this is a common thing in v7, the `AndWhichConstraint` has a constructor that does most of that automatically.
123+
124+
This is what `HaveElement` looks like (with some details left out):
125+
126+
```csharp
127+
public AndWhichConstraint<XElementAssertions, XElement> HaveElement(XName expected,
128+
string because = "", params object[] becauseArgs)
129+
{
130+
xElement = Subject!.Element(expected);
131+
132+
assertionChain
133+
.ForCondition(xElement is not null)
134+
.BecauseOf(because, becauseArgs)
135+
.FailWith(
136+
"Expected {context:subject} to have child element {0}{reason}, but no such child element was found.",
137+
expected.ToString().EscapePlaceholders());
138+
139+
return new AndWhichConstraint<XElementAssertions, XElement>(this, xElement, assertionChain, "/" + expected);
140+
}
141+
```
142+
143+
Notice the last argument to the `AndWhichConstraint` constructor.
144+
145+
## Other breaking changes
146+
147+
Check out the [release notes](releases.md) for other changes that might affect the upgrade to v7.

0 commit comments

Comments
 (0)