|
| 1 | +--- |
| 2 | +sidebar_position: 4 |
| 3 | +--- |
| 4 | + |
| 5 | +# CreateAssertion Attribute |
| 6 | + |
| 7 | +The `[CreateAssertion<T>]` attribute provides a powerful way to automatically generate assertion extension methods from existing methods that return boolean values. This approach eliminates boilerplate code and ensures consistency across your assertion library. |
| 8 | + |
| 9 | +## Overview |
| 10 | + |
| 11 | +The `[CreateAssertion<T>]` attribute is a source generator feature that: |
| 12 | +- Automatically creates assertion extension methods from existing boolean-returning methods |
| 13 | +- Supports both instance and static methods |
| 14 | +- Can generate both positive and negative assertions |
| 15 | +- Maintains full IntelliSense support and compile-time safety |
| 16 | + |
| 17 | +## Basic Usage |
| 18 | + |
| 19 | +### Instance Methods |
| 20 | + |
| 21 | +For methods that exist on the type being asserted: |
| 22 | + |
| 23 | +```csharp |
| 24 | +using TUnit.Assertions.Attributes; |
| 25 | + |
| 26 | +[CreateAssertion<string>(nameof(string.StartsWith))] |
| 27 | +[CreateAssertion<string>(nameof(string.EndsWith))] |
| 28 | +[CreateAssertion<string>(nameof(string.Contains))] |
| 29 | +public static partial class StringAssertionExtensions; |
| 30 | +``` |
| 31 | + |
| 32 | +This generates assertion methods that can be used as: |
| 33 | + |
| 34 | +```csharp |
| 35 | +await Assert.That("Hello World").StartsWith("Hello"); |
| 36 | +await Assert.That("Hello World").EndsWith("World"); |
| 37 | +await Assert.That("Hello World").Contains("lo Wo"); |
| 38 | +``` |
| 39 | + |
| 40 | +### Static Methods |
| 41 | + |
| 42 | +For static methods that take the asserted type as a parameter: |
| 43 | + |
| 44 | +```csharp |
| 45 | +using System.IO; |
| 46 | +using TUnit.Assertions.Attributes; |
| 47 | + |
| 48 | +[CreateAssertion<string>(typeof(Path), nameof(Path.IsPathRooted), CustomName = "IsRootedPath")] |
| 49 | +public static partial class PathAssertionExtensions; |
| 50 | +``` |
| 51 | + |
| 52 | +Usage: |
| 53 | +```csharp |
| 54 | +await Assert.That(@"C:\Users\Documents").IsRootedPath(); |
| 55 | +``` |
| 56 | + |
| 57 | +## Advanced Features |
| 58 | + |
| 59 | +### Custom Names |
| 60 | + |
| 61 | +You can specify custom names for generated methods using the `CustomName` property: |
| 62 | + |
| 63 | +```csharp |
| 64 | +[CreateAssertion<char>(nameof(char.IsDigit))] |
| 65 | +[CreateAssertion<char>(nameof(char.IsDigit), CustomName = "IsNumeric")] // Alias |
| 66 | +public static partial class CharAssertionExtensions; |
| 67 | +``` |
| 68 | + |
| 69 | +### Negative Assertions |
| 70 | + |
| 71 | +Generate negative assertions by setting `NegateLogic = true`: |
| 72 | + |
| 73 | +```csharp |
| 74 | +[CreateAssertion<string>(nameof(string.Contains))] |
| 75 | +[CreateAssertion<string>(nameof(string.Contains), CustomName = "DoesNotContain", NegateLogic = true)] |
| 76 | +public static partial class StringAssertionExtensions; |
| 77 | +``` |
| 78 | + |
| 79 | +Usage: |
| 80 | +```csharp |
| 81 | +await Assert.That("Hello").Contains("ell"); // Passes |
| 82 | +await Assert.That("Hello").DoesNotContain("xyz"); // Passes |
| 83 | +``` |
| 84 | + |
| 85 | +### Multiple Assertions on One Class |
| 86 | + |
| 87 | +You can apply multiple attributes to generate a comprehensive set of assertions: |
| 88 | + |
| 89 | +```csharp |
| 90 | +[CreateAssertion<char>(nameof(char.IsDigit))] |
| 91 | +[CreateAssertion<char>(nameof(char.IsDigit), CustomName = "IsNotDigit", NegateLogic = true)] |
| 92 | +[CreateAssertion<char>(nameof(char.IsLetter))] |
| 93 | +[CreateAssertion<char>(nameof(char.IsLetter), CustomName = "IsNotLetter", NegateLogic = true)] |
| 94 | +[CreateAssertion<char>(nameof(char.IsLetterOrDigit))] |
| 95 | +[CreateAssertion<char>(nameof(char.IsLetterOrDigit), CustomName = "IsNotLetterOrDigit", NegateLogic = true)] |
| 96 | +[CreateAssertion<char>(nameof(char.IsWhiteSpace))] |
| 97 | +[CreateAssertion<char>(nameof(char.IsWhiteSpace), CustomName = "IsNotWhiteSpace", NegateLogic = true)] |
| 98 | +public static partial class CharAssertionExtensions; |
| 99 | +``` |
| 100 | + |
| 101 | +## Attribute Properties |
| 102 | + |
| 103 | +### Constructor Parameters |
| 104 | + |
| 105 | +1. **Single Parameter Constructor** - For instance methods on the target type: |
| 106 | + ```csharp |
| 107 | + [CreateAssertion<TTarget>(string methodName)] |
| 108 | + ``` |
| 109 | + |
| 110 | +2. **Two Parameter Constructor** - For static methods on a different type: |
| 111 | + ```csharp |
| 112 | + [CreateAssertion<TTarget>(Type containingType, string methodName)] |
| 113 | + ``` |
| 114 | + |
| 115 | +### Optional Properties |
| 116 | + |
| 117 | +- **`CustomName`**: Override the generated method name |
| 118 | +- **`NegateLogic`**: Invert the boolean result for negative assertions |
| 119 | +- **`RequiresGenericTypeParameter`**: For methods that need generic type handling |
| 120 | +- **`TreatAsInstance`**: Force treating a static method as instance (useful for extension methods) |
| 121 | + |
| 122 | +## Complete Examples |
| 123 | + |
| 124 | +### Example 1: DateTime Assertions |
| 125 | + |
| 126 | +```csharp |
| 127 | +using System; |
| 128 | +using TUnit.Assertions.Attributes; |
| 129 | + |
| 130 | +[CreateAssertion<DateTime>(nameof(DateTime.IsLeapYear), CustomName = "IsInLeapYear")] |
| 131 | +[CreateAssertion<DateTime>(nameof(DateTime.IsLeapYear), CustomName = "IsNotInLeapYear", NegateLogic = true)] |
| 132 | +[CreateAssertion<DateTime>(nameof(DateTime.IsDaylightSavingTime))] |
| 133 | +[CreateAssertion<DateTime>(nameof(DateTime.IsDaylightSavingTime), CustomName = "IsNotDaylightSavingTime", NegateLogic = true)] |
| 134 | +public static partial class DateTimeAssertionExtensions; |
| 135 | +``` |
| 136 | + |
| 137 | +### Example 2: File System Assertions |
| 138 | + |
| 139 | +```csharp |
| 140 | +using System.IO; |
| 141 | +using TUnit.Assertions.Attributes; |
| 142 | + |
| 143 | +[CreateAssertion<FileInfo>(nameof(FileInfo.Exists))] |
| 144 | +[CreateAssertion<FileInfo>(nameof(FileInfo.Exists), CustomName = "DoesNotExist", NegateLogic = true)] |
| 145 | +[CreateAssertion<FileInfo>(nameof(FileInfo.IsReadOnly))] |
| 146 | +[CreateAssertion<FileInfo>(nameof(FileInfo.IsReadOnly), CustomName = "IsNotReadOnly", NegateLogic = true)] |
| 147 | +public static partial class FileInfoAssertionExtensions; |
| 148 | +``` |
| 149 | + |
| 150 | +Usage: |
| 151 | +```csharp |
| 152 | +var file = new FileInfo(@"C:\temp\test.txt"); |
| 153 | +await Assert.That(file).Exists(); |
| 154 | +await Assert.That(file).IsNotReadOnly(); |
| 155 | +``` |
| 156 | + |
| 157 | +### Example 3: Custom Type Assertions |
| 158 | + |
| 159 | +```csharp |
| 160 | +// Your custom type |
| 161 | +public class User |
| 162 | +{ |
| 163 | + public bool IsActive { get; set; } |
| 164 | + public bool IsVerified { get; set; } |
| 165 | + public bool HasPremiumAccess() => /* logic */; |
| 166 | +} |
| 167 | + |
| 168 | +// Assertion extensions |
| 169 | +[CreateAssertion<User>(nameof(User.IsActive))] |
| 170 | +[CreateAssertion<User>(nameof(User.IsActive), CustomName = "IsInactive", NegateLogic = true)] |
| 171 | +[CreateAssertion<User>(nameof(User.IsVerified))] |
| 172 | +[CreateAssertion<User>(nameof(User.HasPremiumAccess))] |
| 173 | +public static partial class UserAssertionExtensions; |
| 174 | + |
| 175 | +// Usage |
| 176 | +var user = new User { IsActive = true, IsVerified = false }; |
| 177 | +await Assert.That(user).IsActive(); |
| 178 | +await Assert.That(user).IsNotVerified(); |
| 179 | +``` |
| 180 | + |
| 181 | +## Benefits |
| 182 | + |
| 183 | +1. **Reduced Boilerplate**: No need to write repetitive assertion methods |
| 184 | +2. **Consistency**: All generated assertions follow the same pattern |
| 185 | +3. **Type Safety**: Full compile-time checking and IntelliSense support |
| 186 | +4. **Maintainability**: Changes to the source method signature are automatically reflected |
| 187 | +5. **Performance**: Source-generated code has no runtime overhead |
| 188 | + |
| 189 | +## Requirements |
| 190 | + |
| 191 | +- The target method must return a `bool` |
| 192 | +- The containing class must be `partial` |
| 193 | +- The containing class must be `static` for extension methods |
| 194 | +- The method parameters must be compatible with the assertion pattern |
| 195 | + |
| 196 | +## Migration from Manual Assertions |
| 197 | + |
| 198 | +If you have existing manual assertion methods, you can gradually migrate to using `[CreateAssertion<T>]`: |
| 199 | + |
| 200 | +```csharp |
| 201 | +// Before - Manual implementation |
| 202 | +public static InvokableValueAssertionBuilder<string> StartsWith( |
| 203 | + this IValueSource<string> valueSource, |
| 204 | + string expected) |
| 205 | +{ |
| 206 | + return valueSource.RegisterAssertion( |
| 207 | + new StringStartsWithCondition(expected), |
| 208 | + [expected]); |
| 209 | +} |
| 210 | + |
| 211 | +// After - Using CreateAssertion |
| 212 | +[CreateAssertion<string>(nameof(string.StartsWith))] |
| 213 | +public static partial class StringAssertionExtensions; |
| 214 | +``` |
| 215 | + |
| 216 | +## Best Practices |
| 217 | + |
| 218 | +1. **Group Related Assertions**: Keep assertions for similar types in the same partial class |
| 219 | +2. **Consistent Naming**: Use `CustomName` to maintain consistent naming patterns |
| 220 | +3. **Provide Both Positive and Negative**: Where it makes sense, provide both forms |
| 221 | +4. **Document Complex Cases**: Add XML documentation comments to the partial class for complex scenarios |
| 222 | +5. **Test Generated Code**: Ensure generated assertions behave as expected |
| 223 | + |
| 224 | +## Limitations |
| 225 | + |
| 226 | +- Only works with methods that return `bool` |
| 227 | +- Cannot handle methods with complex parameter patterns |
| 228 | +- Generic constraints on the method itself may require manual implementation |
| 229 | +- Methods with optional parameters may need special handling |
| 230 | + |
| 231 | +For cases that can't be handled by `[CreateAssertion<T>]`, you can still write manual assertion methods alongside the generated ones in the same partial class. |
0 commit comments