├── .gitignore ├── CODEOWNERS ├── LICENSE ├── README.md ├── bdd.sln ├── src ├── AsyncSpecification.cs ├── BooleanAssertionExtensions.cs ├── CollectionAssertionExtensions.cs ├── CommonTasks.cs ├── DateTimeAssertionExtensions.cs ├── HandleExceptionsAttribute.cs ├── MethodThatThrows.cs ├── ObjectAssertExtensions.cs ├── ObservationAttribute.cs ├── Specification.cs ├── StringAssertionExtensions.cs └── bdd.csproj └── test ├── TestSpecification.cs └── test.csproj /.gitignore: -------------------------------------------------------------------------------- 1 | packages 2 | *.nupkg 3 | 4 | .vs/ 5 | .vscode/ 6 | 7 | *.suo 8 | *.user 9 | 10 | [Bb]in 11 | [Oo]bj -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # https://help.github.com/articles/about-codeowners/ 2 | 3 | * @chadly 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Archon Information Systems, LLC. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # xUnit.Net BDD Extensions 2 | 3 | Extends xUnit.Net with Behavior Driven Development style fixtures. 4 | 5 | Some of the design goals include: 6 | * Use natural C# constructs to control things such as BDD contexts and concerns. For example, the context of the specification is defined in the class constructor and the concern for the fixture is defined by its namespace. 7 | * Async tests are a first class citizen. 8 | 9 | See here for a [full introduction](https://www.chadly.net/bdd-with-xunit-net/) 10 | 11 | ## How to Use 12 | 13 | Install via [nuget](https://www.nuget.org/packages/xUnit.BDD/) 14 | 15 | ``` 16 | dotnet add package xUnit.BDD 17 | ``` 18 | 19 | * [Write a Scenario](#write-a-scenario) 20 | * [Async Tests](#async-tests) 21 | * [Handling Exceptions](#handling-exceptions) 22 | 23 | ### Write a Scenario 24 | 25 | ```cs 26 | using Xunit.Extensions; 27 | 28 | public class Calculator 29 | { 30 | public int Add(int x, int y) => x + y; 31 | } 32 | 33 | public class when_adding_two_numbers : Specification 34 | { 35 | readonly Calculator calc; 36 | int result; 37 | 38 | public when_adding_two_numbers() 39 | { 40 | calc = new Calculator(); 41 | } 42 | 43 | protected override void Observe() 44 | { 45 | result = calc.Add(1, 2); 46 | } 47 | 48 | [Observation] 49 | public void should_return_correct_result() 50 | { 51 | result.ShouldEqual(3); 52 | } 53 | } 54 | ``` 55 | 56 | This is a contrived example, but the idea is to run the code you're observing in the `Observe` method and have one or more `Observation`s on what you observed. This way, when a test (`Observation`) fails, the language of the class and method (the scenario) should be granular enough to let you know exactly what failed. 57 | 58 | ### Async Tests 59 | 60 | If you are testing something that is async, you can use the `AsyncSpecification`: 61 | 62 | ```cs 63 | using Xunit.Extensions; 64 | 65 | public class Calculator 66 | { 67 | public async Task CalculateMagicNumber(int x, int y) 68 | { 69 | int result = await SomeAsyncCallToAnExternalService(); 70 | return result + x + y; 71 | } 72 | } 73 | 74 | public class when_making_magic_numbers : AsyncSpecification 75 | { 76 | readonly Calculator calc; 77 | int result; 78 | 79 | public when_making_magic_numbers() 80 | { 81 | calc = new Calculator(); 82 | } 83 | 84 | protected override async Task ObserveAsync() 85 | { 86 | result = await calc.CalculateMagicNumber(1, 2); 87 | } 88 | 89 | [Observation] 90 | public void should_return_correct_result() 91 | { 92 | result.ShouldEqual(69); 93 | } 94 | } 95 | ``` 96 | 97 | If you have some async setup/teardown that also needs to run with the test, you can override `InitializeAsync` and/or `DisposeAsync`: 98 | 99 | ```cs 100 | using Xunit.Extensions; 101 | 102 | public class when_doing_more_stuff : AsyncSpecification, IAsyncLifetime 103 | { 104 | public async Task InitializeAsync() 105 | { 106 | await DoSomething(); 107 | } 108 | 109 | protected override async Task ObserveAsync() 110 | { 111 | result = await calc.CalculateMagicNumber(1, 2); 112 | } 113 | 114 | public async Task DisposeAsync() 115 | { 116 | await DoSomethingElse(); 117 | } 118 | 119 | [Observation] 120 | public void should_return_correct_result() 121 | { 122 | result.ShouldEqual(69); 123 | } 124 | } 125 | ``` 126 | 127 | ### Handling Exceptions 128 | 129 | If you have a method under test that throws an exception, you can make use of the `HandleExceptionsAttribute` to make assertions on it: 130 | 131 | ```cs 132 | using Xunit.Extensions; 133 | 134 | public class Calculator 135 | { 136 | public int Add(int x, int y) 137 | { 138 | if (x == 69) 139 | throw new ArgumentException("inappropriate"); 140 | 141 | return x + y; 142 | } 143 | } 144 | 145 | [HandleExceptions] 146 | public class when_adding_an_inappropriate_number : Specification 147 | { 148 | readonly Calculator calc; 149 | int result; 150 | 151 | public when_adding_an_inappropriate_number() 152 | { 153 | calc = new Calculator(); 154 | } 155 | 156 | protected override void Observe() 157 | { 158 | result = calc.Add(69, 1); 159 | } 160 | 161 | [Observation] 162 | public void should_not_return_any_result() 163 | { 164 | result.ShouldEqual(0); 165 | } 166 | 167 | [Observation] 168 | public void should_throw() 169 | { 170 | ThrownException.ShouldBeType().Message.ShouldEqual("inappropriate"); 171 | } 172 | } 173 | ``` 174 | 175 | The `HandleExceptionsAttribute` will cause the test harness to handle any exceptions thrown by the `Observe` method. You can then make assertions on the thrown exception via the `ThrownException` property. If you leave off the `HandleExceptions` on the test class, it will not handle any exceptions from `Observe`. Therefore, you should only add the attribute if you are expecting an exception so as not to hide test failures. 176 | 177 | ## Building Locally 178 | 179 | After cloning, run: 180 | 181 | ``` 182 | dotnet restore 183 | dotnet build 184 | ``` 185 | 186 | To run the test cases: 187 | 188 | ``` 189 | cd test 190 | dotnet test 191 | ``` 192 | -------------------------------------------------------------------------------- /bdd.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26730.3 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "bdd", "src\bdd.csproj", "{603ABF5C-858F-46E5-A3C0-FE9BCE13D9CD}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "test", "test\test.csproj", "{DB76DD39-E0F0-4CB3-84A6-80B8FD344A2C}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {603ABF5C-858F-46E5-A3C0-FE9BCE13D9CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {603ABF5C-858F-46E5-A3C0-FE9BCE13D9CD}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {603ABF5C-858F-46E5-A3C0-FE9BCE13D9CD}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {603ABF5C-858F-46E5-A3C0-FE9BCE13D9CD}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {DB76DD39-E0F0-4CB3-84A6-80B8FD344A2C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {DB76DD39-E0F0-4CB3-84A6-80B8FD344A2C}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {DB76DD39-E0F0-4CB3-84A6-80B8FD344A2C}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {DB76DD39-E0F0-4CB3-84A6-80B8FD344A2C}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {1B47DEA8-8AE1-4D31-9738-2F58753C2C0F} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /src/AsyncSpecification.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace Xunit.Extensions 9 | { 10 | /// 11 | /// The base async specification class 12 | /// 13 | public abstract class AsyncSpecification : IAsyncLifetime 14 | { 15 | Exception exception; 16 | static readonly ReaderWriterLockSlim sync = new ReaderWriterLockSlim(); 17 | static readonly Dictionary typeCache = new Dictionary(); 18 | 19 | /// 20 | /// The exception that was thrown when Observe was run; null if no exception was thrown. 21 | /// 22 | protected Exception ThrownException => exception; 23 | 24 | /// 25 | /// Initialize the test class all async-like. 26 | /// 27 | protected virtual Task InitializeAsync() => CommonTasks.Completed; 28 | 29 | /// 30 | /// Performs the action to observe the outcome of to validate the specification. 31 | /// 32 | protected abstract Task ObserveAsync(); 33 | 34 | /// 35 | /// Cleanup the test class all async-like. 36 | /// 37 | protected virtual Task DisposeAsync() => CommonTasks.Completed; 38 | 39 | Task IAsyncLifetime.DisposeAsync() => DisposeAsync(); 40 | 41 | async Task IAsyncLifetime.InitializeAsync() 42 | { 43 | await InitializeAsync(); 44 | 45 | try 46 | { 47 | await ObserveAsync(); 48 | } 49 | catch (Exception ex) 50 | { 51 | if (!HandleException(ex)) 52 | throw; 53 | } 54 | } 55 | 56 | bool HandleException(Exception ex) 57 | { 58 | exception = ex; 59 | return ShouldHandleException(); 60 | } 61 | 62 | bool ShouldHandleException() 63 | { 64 | Type type = GetType(); 65 | 66 | try 67 | { 68 | sync.EnterReadLock(); 69 | 70 | if (typeCache.ContainsKey(type)) 71 | return typeCache[type]; 72 | } 73 | finally 74 | { 75 | sync.ExitReadLock(); 76 | } 77 | 78 | try 79 | { 80 | sync.EnterWriteLock(); 81 | 82 | if (typeCache.ContainsKey(type)) 83 | return typeCache[type]; 84 | 85 | var attrs = type.GetTypeInfo().GetCustomAttributes(typeof(HandleExceptionsAttribute), true).OfType(); 86 | 87 | return typeCache[type] = attrs.Any(); 88 | } 89 | finally 90 | { 91 | sync.ExitWriteLock(); 92 | } 93 | } 94 | } 95 | } -------------------------------------------------------------------------------- /src/BooleanAssertionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Xunit.Sdk; 3 | 4 | namespace Xunit.Extensions 5 | { 6 | /// 7 | /// Extensions which provide assertions to classes derived from . 8 | /// 9 | public static class BooleanAssertionExtensions 10 | { 11 | /// 12 | /// Verifies that the condition is false. 13 | /// 14 | /// The condition to be tested 15 | /// Thrown if the condition is not false 16 | public static void ShouldBeFalse(this bool condition) 17 | { 18 | Assert.False(condition); 19 | } 20 | 21 | /// 22 | /// Verifies that the condition is false. 23 | /// 24 | /// The condition to be tested 25 | /// The message to show when the condition is not false 26 | /// Thrown if the condition is not false 27 | public static void ShouldBeFalse(this bool condition, 28 | string userMessage) 29 | { 30 | Assert.False(condition, userMessage); 31 | } 32 | 33 | /// 34 | /// Verifies that an expression is true. 35 | /// 36 | /// The condition to be inspected 37 | /// Thrown when the condition is false 38 | public static void ShouldBeTrue(this bool condition) 39 | { 40 | Assert.True(condition); 41 | } 42 | 43 | /// 44 | /// Verifies that an expression is true. 45 | /// 46 | /// The condition to be inspected 47 | /// The message to be shown when the condition is false 48 | /// Thrown when the condition is false 49 | public static void ShouldBeTrue(this bool condition, 50 | string userMessage) 51 | { 52 | Assert.True(condition, userMessage); 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /src/CollectionAssertionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using Xunit.Sdk; 5 | 6 | namespace Xunit.Extensions 7 | { 8 | /// 9 | /// Extensions which provide assertions to classes derived from and . 10 | /// 11 | public static class CollectionAssertExtensions 12 | { 13 | /// 14 | /// Verifies that a collection is empty. 15 | /// 16 | /// The collection to be inspected 17 | /// Thrown when the collection is null 18 | /// Thrown when the collection is not empty 19 | public static void ShouldBeEmpty(this IEnumerable collection) 20 | { 21 | Assert.Empty(collection); 22 | } 23 | 24 | /// 25 | /// Verifies that a collection contains a given object. 26 | /// 27 | /// The type of the object to be verified 28 | /// The collection to be inspected 29 | /// The object expected to be in the collection 30 | /// Thrown when the object is not present in the collection 31 | public static void ShouldContain(this IEnumerable collection, 32 | T expected) 33 | { 34 | Assert.Contains(expected, collection); 35 | } 36 | 37 | /// 38 | /// Verifies that a collection contains a given object, using a comparer. 39 | /// 40 | /// The type of the object to be verified 41 | /// The collection to be inspected 42 | /// The object expected to be in the collection 43 | /// The comparer used to equate objects in the collection with the expected object 44 | /// Thrown when the object is not present in the collection 45 | public static void ShouldContain(this IEnumerable collection, 46 | T expected, 47 | IEqualityComparer comparer) 48 | { 49 | Assert.Contains(expected, collection, comparer); 50 | } 51 | 52 | /// 53 | /// Verifies that a collection is not empty. 54 | /// 55 | /// The collection to be inspected 56 | /// Thrown when a null collection is passed 57 | /// Thrown when the collection is empty 58 | public static void ShouldNotBeEmpty(this IEnumerable collection) 59 | { 60 | Assert.NotEmpty(collection); 61 | } 62 | 63 | /// 64 | /// Verifies that a collection does not contain a given object. 65 | /// 66 | /// The type of the object to be compared 67 | /// The object that is expected not to be in the collection 68 | /// The collection to be inspected 69 | /// Thrown when the object is present inside the container 70 | public static void ShouldNotContain(this IEnumerable collection, 71 | T expected) 72 | { 73 | Assert.DoesNotContain(expected, collection); 74 | } 75 | 76 | /// 77 | /// Verifies that a collection does not contain a given object, using a comparer. 78 | /// 79 | /// The type of the object to be compared 80 | /// The object that is expected not to be in the collection 81 | /// The collection to be inspected 82 | /// The comparer used to equate objects in the collection with the expected object 83 | /// Thrown when the object is present inside the container 84 | public static void ShouldNotContain(this IEnumerable collection, 85 | T expected, 86 | IEqualityComparer comparer) 87 | { 88 | Assert.DoesNotContain(expected, collection, comparer); 89 | } 90 | } 91 | } -------------------------------------------------------------------------------- /src/CommonTasks.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Xunit.Extensions 4 | { 5 | static class CommonTasks 6 | { 7 | public static readonly Task Completed = Task.FromResult(0); 8 | } 9 | } -------------------------------------------------------------------------------- /src/DateTimeAssertionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Xunit.Extensions 4 | { 5 | /// 6 | /// Extensions which provide assertions to classes derived from . 7 | /// 8 | public static class DateTimeAssertionExtensions 9 | { 10 | /// 11 | /// Verifies that the specified DateTime is within one second from now. 12 | /// 13 | public static void ShouldBeWithinOneSecondFromNow(this DateTime actual) 14 | { 15 | ShouldBeWithinOneSecondFromNow((DateTime?)actual); 16 | } 17 | 18 | /// 19 | /// Verifies that the specified DateTime is within one second from now. 20 | /// 21 | public static void ShouldBeWithinOneSecondFromNow(this DateTime? actual) 22 | { 23 | if (!actual.HasValue) 24 | Assert.True(false, "The value is null; therefore not within one second from now."); 25 | 26 | DateTime dateValue = actual.Value; 27 | DateTime now = (dateValue.Kind == DateTimeKind.Utc) ? DateTime.UtcNow : DateTime.Now; 28 | 29 | ShouldBeWithinOneSecondFrom(actual, now); 30 | } 31 | 32 | /// 33 | /// Verifies that the actual DateTime is within one second from the expected DateTime. 34 | /// 35 | public static void ShouldBeWithinOneSecondFrom(this DateTime actual, DateTime expected) 36 | { 37 | ShouldBeWithinOneSecondFrom((DateTime?)actual, expected); 38 | } 39 | 40 | /// 41 | /// Verifies that the actual DateTime is within one second from the expected DateTime. 42 | /// 43 | public static void ShouldBeWithinOneSecondFrom(this DateTime? actual, DateTime expected) 44 | { 45 | if (!actual.HasValue) 46 | Assert.True(false, String.Format("The actual value is null; therefore not within one second from {0}.", expected)); 47 | 48 | DateTime dateValue = actual.Value; 49 | TimeSpan oneSecond = new TimeSpan(0, 0, 1); 50 | 51 | DateTime lower = expected.Subtract(oneSecond); 52 | DateTime upper = expected.Add(oneSecond); 53 | 54 | Assert.InRange(dateValue, lower, upper); 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /src/HandleExceptionsAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Xunit.Extensions 4 | { 5 | [AttributeUsage(AttributeTargets.Class)] 6 | public class HandleExceptionsAttribute : Attribute 7 | { 8 | } 9 | } -------------------------------------------------------------------------------- /src/MethodThatThrows.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Xunit.Extensions 4 | { 5 | public delegate void MethodThatThrows(); 6 | 7 | public static class MethodThatThrowsExtensions 8 | { 9 | public static Exception GetException(this MethodThatThrows method) 10 | { 11 | Exception exception = null; 12 | 13 | try 14 | { 15 | method(); 16 | } 17 | catch (Exception ex) 18 | { 19 | exception = ex; 20 | } 21 | 22 | return exception; 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /src/ObjectAssertExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Xunit.Sdk; 4 | 5 | namespace Xunit.Extensions 6 | { 7 | /// 8 | /// Extensions which provide assertions to classes derived from . 9 | /// 10 | public static class ObjectAssertExtensions 11 | { 12 | /// 13 | /// Verifies that a value is within a given range. 14 | /// 15 | /// The type of the value to be compared 16 | /// The actual value to be evaluated 17 | /// The (inclusive) low value of the range 18 | /// The (inclusive) high value of the range 19 | /// Thrown when the value is not in the given range 20 | public static void ShouldBeInRange(this T actual, 21 | T low, 22 | T high) where T : IComparable 23 | { 24 | Assert.InRange(actual, low, high); 25 | } 26 | 27 | /// 28 | /// Verifies that a value is within a given range, using a comparer. 29 | /// 30 | /// The type of the value to be compared 31 | /// The actual value to be evaluated 32 | /// The (inclusive) low value of the range 33 | /// The (inclusive) high value of the range 34 | /// The comparer used to evaluate the value's range 35 | /// Thrown when the value is not in the given range 36 | public static void ShouldBeInRange(this T actual, 37 | T low, 38 | T high, 39 | IComparer comparer) 40 | { 41 | Assert.InRange(actual, low, high, comparer); 42 | } 43 | 44 | /// 45 | /// Verifies that an object reference is null. 46 | /// 47 | /// The object to be inspected 48 | /// Thrown when the object reference is not null 49 | public static void ShouldBeNull(this object @object) 50 | { 51 | Assert.Null(@object); 52 | } 53 | 54 | /// 55 | /// Verifies that two objects are the same instance. 56 | /// 57 | /// The actual object instance 58 | /// The expected object instance 59 | /// Thrown when the objects are not the same instance 60 | public static void ShouldBeSameAs(this object actual, 61 | object expected) 62 | { 63 | Assert.Same(expected, actual); 64 | } 65 | 66 | /// 67 | /// Verifies that an object is exactly the given type (and not a derived type). 68 | /// 69 | /// The type the object should be 70 | /// The object to be evaluated 71 | /// The object, casted to type T when successful 72 | /// Thrown when the object is not the given type 73 | public static T ShouldBeType(this object @object) 74 | { 75 | return Assert.IsType(@object); 76 | } 77 | 78 | /// 79 | /// Verifies that an object is exactly the given type (and not a derived type). 80 | /// 81 | /// The object to be evaluated 82 | /// The type the object should be 83 | /// Thrown when the object is not the given type 84 | public static void ShouldBeType(this object @object, 85 | Type expectedType) 86 | { 87 | Assert.IsType(expectedType, @object); 88 | } 89 | 90 | /// 91 | /// Verifies that two objects are equal, using a default comparer. 92 | /// 93 | /// The type of the objects to be compared 94 | /// The value to be compared against 95 | /// The expected value 96 | /// Thrown when the objects are not equal 97 | public static void ShouldEqual(this T actual, 98 | T expected) 99 | { 100 | Assert.Equal(expected, actual); 101 | } 102 | 103 | /// 104 | /// Verifies that two objects are equal, using a custom comparer. 105 | /// 106 | /// The type of the objects to be compared 107 | /// The value to be compared against 108 | /// The expected value 109 | /// The comparer used to compare the two objects 110 | /// Thrown when the objects are not equal 111 | public static void ShouldEqual(this T actual, 112 | T expected, 113 | IEqualityComparer comparer) 114 | { 115 | Assert.Equal(expected, actual, comparer); 116 | } 117 | 118 | /// 119 | /// Verifies that a value is not within a given range, using the default comparer. 120 | /// 121 | /// The type of the value to be compared 122 | /// The actual value to be evaluated 123 | /// The (inclusive) low value of the range 124 | /// The (inclusive) high value of the range 125 | /// Thrown when the value is in the given range 126 | public static void ShouldNotBeInRange(this T actual, 127 | T low, 128 | T high) where T : IComparable 129 | { 130 | Assert.NotInRange(actual, low, high); 131 | } 132 | 133 | /// 134 | /// Verifies that a value is not within a given range, using a comparer. 135 | /// 136 | /// The type of the value to be compared 137 | /// The actual value to be evaluated 138 | /// The (inclusive) low value of the range 139 | /// The (inclusive) high value of the range 140 | /// The comparer used to evaluate the value's range 141 | /// Thrown when the value is in the given range 142 | public static void ShouldNotBeInRange(this T actual, 143 | T low, 144 | T high, 145 | IComparer comparer) 146 | { 147 | Assert.NotInRange(actual, low, high, comparer); 148 | } 149 | 150 | /// 151 | /// Verifies that an object reference is not null. 152 | /// 153 | /// The object to be validated 154 | /// Thrown when the object is not null 155 | public static void ShouldNotBeNull(this object @object) 156 | { 157 | Assert.NotNull(@object); 158 | } 159 | 160 | /// 161 | /// Verifies that two objects are not the same instance. 162 | /// 163 | /// The actual object instance 164 | /// The expected object instance 165 | /// Thrown when the objects are the same instance 166 | public static void ShouldNotBeSameAs(this object actual, 167 | object expected) 168 | { 169 | Assert.NotSame(expected, actual); 170 | } 171 | 172 | /// 173 | /// Verifies that an object is not exactly the given type. 174 | /// 175 | /// The type the object should not be 176 | /// The object to be evaluated 177 | /// Thrown when the object is the given type 178 | public static void ShouldNotBeType(this object @object) 179 | { 180 | Assert.IsNotType(@object); 181 | } 182 | 183 | /// 184 | /// Verifies that an object is not exactly the given type. 185 | /// 186 | /// The object to be evaluated 187 | /// The type the object should not be 188 | /// Thrown when the object is the given type 189 | public static void ShouldNotBeType(this object @object, 190 | Type expectedType) 191 | { 192 | Assert.IsNotType(expectedType, @object); 193 | } 194 | 195 | /// 196 | /// Verifies that two objects are not equal, using a default comparer. 197 | /// 198 | /// The type of the objects to be compared 199 | /// The actual object 200 | /// The expected object 201 | /// Thrown when the objects are equal 202 | public static void ShouldNotEqual(this T actual, 203 | T expected) 204 | { 205 | Assert.NotEqual(expected, actual); 206 | } 207 | 208 | /// 209 | /// Verifies that two objects are not equal, using a custom comparer. 210 | /// 211 | /// The type of the objects to be compared 212 | /// The actual object 213 | /// The expected object 214 | /// The comparer used to examine the objects 215 | /// Thrown when the objects are equal 216 | public static void ShouldNotEqual(this T actual, 217 | T expected, 218 | IEqualityComparer comparer) 219 | { 220 | Assert.NotEqual(expected, actual, comparer); 221 | } 222 | } 223 | } -------------------------------------------------------------------------------- /src/ObservationAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace Xunit.Extensions 2 | { 3 | /// 4 | /// Identifies a method as an observation which asserts the specification 5 | /// 6 | public class ObservationAttribute : FactAttribute 7 | { 8 | } 9 | } -------------------------------------------------------------------------------- /src/Specification.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Xunit.Extensions 4 | { 5 | /// 6 | /// The base specification class for non-async scenarios 7 | /// 8 | public abstract class Specification : AsyncSpecification 9 | { 10 | protected sealed override Task InitializeAsync() => base.InitializeAsync(); 11 | protected sealed override Task DisposeAsync() => base.DisposeAsync(); 12 | 13 | protected sealed override Task ObserveAsync() 14 | { 15 | Observe(); 16 | return CommonTasks.Completed; 17 | } 18 | 19 | /// 20 | /// Performs the action to observe the outcome of to validate the specification. 21 | /// 22 | protected abstract void Observe(); 23 | } 24 | } -------------------------------------------------------------------------------- /src/StringAssertionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Xunit.Extensions 4 | { 5 | /// 6 | /// Extensions which provide assertions to classes derived from . 7 | /// 8 | public static class StringAssertionExtensions 9 | { 10 | /// 11 | /// Verifies that a string contains a given sub-string, using the current culture. 12 | /// 13 | /// The string to be inspected 14 | /// The sub-string expected to be in the string 15 | /// Thrown when the sub-string is not present inside the string 16 | public static void ShouldContain(this string actualString, 17 | string expectedSubString) 18 | { 19 | Assert.Contains(expectedSubString, actualString); 20 | } 21 | 22 | /// 23 | /// Verifies that a string contains a given sub-string, using the given comparison type. 24 | /// 25 | /// The string to be inspected 26 | /// The sub-string expected to be in the string 27 | /// The type of string comparison to perform 28 | /// Thrown when the sub-string is not present inside the string 29 | public static void ShouldContain(this string actualString, 30 | string expectedSubString, 31 | StringComparison comparisonType) 32 | { 33 | Assert.Contains(expectedSubString, actualString, comparisonType); 34 | } 35 | 36 | /// 37 | /// Verifies that a string does not contain a given sub-string, using the current culture. 38 | /// 39 | /// The string to be inspected 40 | /// The sub-string which is expected not to be in the string 41 | /// Thrown when the sub-string is present inside the string 42 | public static void ShouldNotContain(this string actualString, 43 | string expectedSubString) 44 | { 45 | Assert.DoesNotContain(expectedSubString, actualString); 46 | } 47 | 48 | /// 49 | /// Verifies that a string does not contain a given sub-string, using the current culture. 50 | /// 51 | /// The string to be inspected 52 | /// The sub-string which is expected not to be in the string 53 | /// The type of string comparison to perform 54 | /// Thrown when the sub-string is present inside the given string 55 | public static void ShouldNotContain(this string actualString, 56 | string expectedSubString, 57 | StringComparison comparisonType) 58 | { 59 | Assert.DoesNotContain(expectedSubString, actualString, comparisonType); 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /src/bdd.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net452;netstandard1.1 5 | 1.0.0 6 | Xunit BDD Extensions 7 | Chad Lee 8 | Xunit.Extensions 9 | Extends xUnit.Net with Behavior Driven Development style fixtures. 10 | xUnit.BDD 11 | xUnit.BDD 12 | Xunit.Bdd 13 | 14 | https://github.com/civicsource/xunit-bdd 15 | https://github.com/civicsource/xunit-bdd/blob/master/LICENSE 16 | Copyright © 2018 Archon Information Systems, LLC. 17 | https://github.com/civicsource/xunit-bdd.git 18 | git 19 | bdd xunit behavior-driven-development should 20 | https://github.com/civicsource/xunit-bdd/releases 21 | Archon Information Systems, LLC 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /test/TestSpecification.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace Xunit.Extensions.Test 5 | { 6 | public class behaves_like_async_specification : AsyncSpecification 7 | { 8 | bool observedInBase = false; 9 | 10 | protected override Task ObserveAsync() 11 | { 12 | observedInBase = true; 13 | return Task.CompletedTask; 14 | } 15 | 16 | [Observation] 17 | public void should_call_base_observe() 18 | { 19 | observedInBase.ShouldBeTrue("Observe should be called in the base class"); 20 | } 21 | } 22 | 23 | public class behaves_like_specification : Specification 24 | { 25 | bool observedInBase = false; 26 | 27 | protected override void Observe() 28 | { 29 | observedInBase = true; 30 | } 31 | 32 | [Observation] 33 | public void should_call_base_observe() 34 | { 35 | observedInBase.ShouldBeTrue("Observe should be called in the base class"); 36 | } 37 | } 38 | 39 | public class behaves_like_a_polymorphic_specification : behaves_like_async_specification 40 | { 41 | protected bool observedInDerived = false; 42 | 43 | protected override async Task ObserveAsync() 44 | { 45 | await base.ObserveAsync(); 46 | observedInDerived = true; 47 | } 48 | 49 | [Observation] 50 | public void should_call_derived_observe() 51 | { 52 | observedInDerived.ShouldBeTrue("Observe should be called in the derived class"); 53 | } 54 | } 55 | 56 | public class TestException : Exception { } 57 | 58 | [HandleExceptions] 59 | public class behaves_like_a_specification_that_throws_when_observed : AsyncSpecification 60 | { 61 | protected override Task ObserveAsync() 62 | { 63 | throw new TestException(); 64 | } 65 | 66 | [Observation] 67 | public void should_handle_exception() 68 | { 69 | ThrownException.ShouldNotBeNull(); 70 | ThrownException.ShouldBeType(); 71 | } 72 | } 73 | 74 | public class behaves_like_a_specification_that_unexpectedly_throws_when_observed : AsyncSpecification 75 | { 76 | protected override Task ObserveAsync() 77 | { 78 | throw new TestException(); 79 | } 80 | 81 | [Observation(Skip = "This test should fail")] 82 | public void should_fail() 83 | { 84 | // This test will fail because of the exception thrown in Observe(). 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /test/test.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.0 5 | 6 | false 7 | 8 | Xunit.Extensions.Test 9 | 10 | Xunit.Extensions.Test 11 | 12 | 1.0.0 13 | 14 | 15 | 16 | 17 | 18 | 19 | all 20 | runtime; build; native; contentfiles; analyzers 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | --------------------------------------------------------------------------------