DrillSergeant.Xunit2
1.2.0
See the version list below for details.
dotnet add package DrillSergeant.Xunit2 --version 1.2.0
NuGet\Install-Package DrillSergeant.Xunit2 -Version 1.2.0
<PackageReference Include="DrillSergeant.Xunit2" Version="1.2.0" />
paket add DrillSergeant.Xunit2 --version 1.2.0
#r "nuget: DrillSergeant.Xunit2, 1.2.0"
// Install DrillSergeant.Xunit2 as a Cake Addin #addin nuget:?package=DrillSergeant.Xunit2&version=1.2.0 // Install DrillSergeant.Xunit2 as a Cake Tool #tool nuget:?package=DrillSergeant.Xunit2&version=1.2.0
DrillSergeant
.net behavior driven testing written by developers, for developers.
Introduction
DrillSergeant is a behavior testing library that empowers developers to apply BDD practices with minimal amount of friction. Simply import the package and write your behaviors in familiar C# syntax. Unlike other behavior testing frameworks which rely on external feature files to write scenarios, DrillSergeant lets you write behavior tests 100% in C# code.
Getting Started
For a complete example of a feature, see the following example. For something more complex, see the DemoStore repo.
The Obligatory Hello World Example
Lets say we have a Calculator
service. For this example we'll define it as a simple class.
public class Calculator
{
public int Add(int a, int b) => a + b;
}
We can write a behavior test like so:
public class CalculatorTests
{
[Behavior]
[InlineData(1,2,3)] // [TestCase] for NUnit or [DataRow] for MSTest.
[InlineData(2,3,5)]
public void TestAdditionBehavior(int a, int b, int expected)
{
var calculator = Given("Create a calculator", () => new Calculator());
var result = When($"Add {a} and {b}", () => calculator.Resolve().Add(a, b));
Then(CheckResult("Check result", () => Assert.Equal(expected, result));
}
}
And the test runner output should look like this:
Behaviors are written in same fashion as a normal unit test. The only difference is that it is marked using the [Behavior]
attribute.
A More Advanced Example
From the StoreDemo project we define a behavior test to verify that we can create a new shopping cart, add items to it, and then purchase its contents:
[Behavior]
public void PurchasingItemsInCartCreatesNewOrder()
{
var client = _api.CreateClient();
Given(CartSteps.NewCart(client));
Given(CartSteps.LoadProducts(client));
Given(CartSteps.AddRandomProductToCart(client));
When(OrderingSteps.PlaceOrder(client));
Then(OrderingSteps.CheckOrderId());
}
Where client
is an instance of HttpClient
. Within CartSteps
we define the following steps:
public static class CartSteps
{
private static readonly Random random = new();
public static LambdaStep NewCart(HttpClient client) =>
new LambdaStep("Create new cart")
.HandleAsync(async () =>
{
var url = $"api/cart/new";
var response = await client.GetStringAsync(url);
CurrentBehavior.Context.CartId = int.Parse(response);
});
public static LambdaStep LoadProducts(HttpClient client) =>
new LambdaStep("Get product list")
.HandleAsync(async () =>
{
var url = "api/products";
var response = await client.GetFromJsonAsync<Product[]>(url);
CurrentBehavior.Context.Products = response;
});
public static LambdaStep AddRandomProductToCart(HttpClient client) =>
new LambdaStep("Add random product to cart")
.HandleAsync(async () =>
{
var cartId = (int)CurrentBehavior.Context.CartId;
var products = (Product[])CurrentBehavior.Context.Products;
var product = products[random.Next(0, products.Length)];
var url = "api/cart/add";
await client.PostAsJsonAsync(url, new AddProductRequest(cartId, product.Id, 1));
});
}
And within OrderingSteps
we define the steps:
public static LambdaStep PlaceOrder(HttpClient client) =>
new LambdaStep("Place order")
.HandleAsync(async () =>
{
var cartId = (int)CurrentBehavior.Context.CartId;
var order = new PlaceOrderRequest(cartId);
var url = "api/order/place";
var response = await client.PostAsJsonAsync(url, order);
if (response.StatusCode == HttpStatusCode.OK)
{
var body = await response
.Content
.ReadFromJsonAsync<PlaceOrderResponse>();
CurrentBehavior.Context.OrderId = body?.OrderNumber;
}
else
{
CurrentBehavior.Context.OrderId = null;
}
});
public static LambdaStep CheckOrderId() =>
new LambdaStep("Check order id is set")
.Handle(() => Assert.NotNull(CurrentBehavior.Context.OrderId));
}
This time when we run the test we get the following output in our test runner:
Why Write Tests This Way?
Unlike in normal unit tests, which are intended to test the correctness of individual methods, behaviors tests validate whether one or more components actually behave in the way expected when given "normal" inputs. Because of this, behaviors are composed of a series of pluggable steps that can be re-used in different scenarios. See the Cucumber documentation for an introduction into behavior testing.
Comparison with 3rd Party Acceptance Testing Tools (e.g., SpecFlow, Fitnesse, Gauge)
DrillSergeant was borne out of frustration of using 3rd party testing tools. While tools such as SpecFlow and Gauge have gotten easier to use over time, they require installing 3rd party plugins/runners in the developer environment. Additionally they require separate files for authoring the tests themselves (.feature
for Specflow, .wiki
for FitNesse, and .md
for Gauge). This relies on a mixture of code generation and reflection magic in order to bind the test specifications with the code that actually runs them, which adds a layer of complexity.
DrillSergeant takes a different approach to this problem. Rather than rely on DSLs and complex translation layers, it engrafts additional capabilities to the xunit framework to make it easy to write behavior-driven with familiar C# syntax. No new DSLs to learn, no build task fussiness, no reflection shenanigans. Just a simple API written entirely in C# code that can be tested/debugged the exact same way as all of your other unit tests.
For a longer-winded explanation, see the following blog post.
Test Runner Compatibility
Originally DrillSergeant was built around xunit and has been well tested with it. As of version 0.2.0 support has been added for NUnit and MSTest.
The NUnit integration is likely to be fairly stable since the framework was designed with extensibility support in mind. This made adding hooks for DrillSergeant fairly trivial.
The MSTest integration on the other hand should be considered experimental. This is because that framework has very limited support for extensibility and needed several somewhat invasive hacks to get working. If anyone has experience with MSTest and would like to help with this please let us know!
Installation
DrillSergeant is a regular library and can be installed via package manager with either the Install-Package
or dotnet add package
commands. Note that because DrillSergeant is still in beta that you will need check the 'Include Prelease' checkbox to find it in nuget manager.
Framework | Package | Example |
---|---|---|
Xunit | DrillSergeant.Xunit2 | dotnet add package DrillSergeant.Xunit2 |
NUnit | DrillSergeant.NUnit3 | dotnet add package DrillSergeant.NUnit3 |
MSTest | DrillSergeant.MSTest | dotnet add package DrillSergeant.MSTest |
Analyzers | DrillSergeant.Analyzers | dotnet add package DrillSergeant.Analyzers |
Analyzer Support
As of version 1.1.0, DrillSergeant now has an optional analyzers package (DrillSergeant.Analyzers
) that can provide real-time static analysis of behaviors to look for common mistakes and ensure best practices are being followed. You can read more about them in the wiki section:
Note: The analyzers are still in beta and have only had limited testing. If you encounter any issues, please report them here.
Support
If you encounter any issues running tests or would like a feature added please do so here. DrillSergeant is still fairly new and under active development.
And if you like the project, be sure to give it a star!
More Information
For more information, please see the wikis.
For an introduction, please see this Medium article.
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net6.0 is compatible. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 is compatible. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. |
-
net6.0
- DotNet.ReproducibleBuilds (>= 1.1.1)
- DrillSergeant (>= 1.2.0)
- GitVersion.MsBuild (>= 5.12.0)
- xunit.extensibility.execution (>= 2.5.0)
-
net7.0
- DotNet.ReproducibleBuilds (>= 1.1.1)
- DrillSergeant (>= 1.2.0)
- GitVersion.MsBuild (>= 5.12.0)
- xunit.extensibility.execution (>= 2.5.0)
-
net8.0
- DotNet.ReproducibleBuilds (>= 1.1.1)
- DrillSergeant (>= 1.2.0)
- GitVersion.MsBuild (>= 5.12.0)
- xunit.extensibility.execution (>= 2.5.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
Version | Downloads | Last updated |
---|---|---|
1.2.2 | 56 | 4/21/2024 |
1.2.1 | 44 | 4/21/2024 |
1.2.0 | 59 | 2/20/2024 |
1.2.0-alpha.40 | 81 | 1/20/2024 |
1.2.0-alpha.39 | 53 | 1/20/2024 |
1.2.0-alpha.38 | 51 | 1/20/2024 |
1.2.0-alpha.37 | 54 | 1/20/2024 |
1.2.0-alpha.35 | 136 | 11/19/2023 |
1.2.0-alpha.34 | 60 | 11/19/2023 |
1.2.0-alpha.33 | 61 | 11/13/2023 |
1.1.8 | 53 | 2/20/2024 |
1.1.2 | 80 | 1/20/2024 |
1.1.1 | 194 | 11/12/2023 |
1.1.0-alpha.42 | 60 | 11/12/2023 |
1.1.0-alpha.41 | 61 | 11/12/2023 |
1.1.0-alpha.39 | 59 | 11/12/2023 |
1.1.0-alpha.38 | 63 | 11/12/2023 |
1.1.0-alpha.37 | 61 | 11/12/2023 |
1.1.0-alpha.35 | 60 | 11/12/2023 |
1.0.3 | 137 | 10/21/2023 |
1.0.1 | 119 | 10/12/2023 |
1.0.0-beta.53 | 72 | 9/30/2023 |
1.0.0-beta.52 | 68 | 9/29/2023 |
0.6.2 | 133 | 8/20/2023 |
0.6.1-beta | 96 | 8/20/2023 |
0.6.0-beta | 94 | 8/20/2023 |
0.5.0 | 132 | 7/20/2023 |
0.4.0 | 140 | 7/16/2023 |
0.3.0-beta | 120 | 7/12/2023 |
0.2.0-beta | 115 | 7/9/2023 |