Only this pageAll pages
Powered by GitBook
1 of 29

Bard

Loading...

Getting Started

Loading...

Loading...

Loading...

Loading...

Anatomy Of A Scenario

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Advanced

Loading...

Miscellaneous

Loading...

Loading...

gRPC

Loading...

Loading...

Loading...

Tips & Tricks

Loading...

Installation

Before writing any tests, you will need to add a reference to Bard.dll in your project.

The simplest way to do this is to use either the NuGet package manager, or the dotnet CLI.

Using the NuGet package manager console within Visual Studio run the following command:

Install-Package Bard

Or using the .net core CLI from a terminal window:

dotnet add package Bard

Introduction

A .NET library for functional API testing.

Why Bard?

Bard is a test library written by Developers for Developers. It is as much a development tool as it is a test library. Although Bard is a .NET library it can be used to test any API if you want.

Build Your Own Domain Specific Language

Bard provides you with the building blocks to allow you to build your own Domain Specific Language (DSL) for test arrangement. This allows the developer to build up a library of stories that can be composed via a fluent interface.

The fluent interface guides the developer how to construct the scenario in a logical sequence making it easier for new tests to be written. This arguably involves a little more effort up front but can pay dividends in the long run when working against a complex domain that involves intricate test arrangement.

Bard uses a functional approach to test arrangement and employs the Collection Pipeline Pattern

Collection pipelines are a programming pattern where you organize some computation as a sequence of operations which compose by taking a collection as output of one operation and feeding it into the next. - Martin Fowler

This means that when there are a number of sequential steps performed during the arrangement of the test data can flow from one step to the next step.

[Fact]
public void Retrieving_a_bankAccount_by_id_should_return_a_200_ok_response()
{
    Given       
       .BankAccount_has_been_created(account => account.CustomerName = "Dougal")
       .Deposit_has_been_made(100)
       .Withdrawal_has_been_made(50)
       .GetResult(out BankingStoryData bankAccount);
       
    When
       .Get($"api/bankaccounts/{bankAccount.BankAccountId}");
      
    Then.Response
       .ShouldBe
       .Ok<BankAccount>();       
}

First Class Logging

Bard generates exceptional automatically generated log from your tests. This means you can write your tests first and then build your APIs. This gives the developer the opportunity to 'Dog Food' their API whilst writing their tests. No more eyeballing API responses in tools such as Postman.

**********************************************************************
*              GIVEN THAT BANK ACCOUNT HAS BEEN CREATED              *
**********************************************************************

REQUEST: POST http://localhost/api/bankaccounts
{
  "id": 0,
  "isActive": false,
  "customerName": "Fred",
  "balance": 0.0
}

RESPONSE: Http Status Code:  Created (201)
Header::Location http://localhost/api/bankaccounts/1
{
  "id": 1,
  "isActive": false,
  "customerName": "Fred",
  "balance": 0.0
}

**********************************************************************
*                        DEPOSIT HAS BEEN MADE                       *
**********************************************************************

REQUEST: POST http://localhost/api/bankaccounts/1/deposits
{
  "id": null,
  "amount": 100.0
}

RESPONSE: Http Status Code:  OK (200)

**********************************************************************
*                      WITHDRAWAL HAS BEEN MADE                      *
**********************************************************************

REQUEST: POST http://localhost/api/bankaccounts/1/withdrawals
{
  "id": null,
  "amount": 50.0
}

RESPONSE: Http Status Code:  OK (200)

**********************************************************************
*                                WHEN                                *
**********************************************************************

REQUEST: GET http://localhost/api/bankaccounts/1

**********************************************************************
*              THEN THE RESPONSE SHOULD BE HTTP 200 OK               *
**********************************************************************

RESPONSE: Http Status Code:  OK (200)
{
  "id": 1,
  "isActive": false,
  "customerName": "Fred",
  "balance": 75.0
}

Smart HTTP Response Assertions

A fluent API response helper is built directly into Bard. This hides away the boiler plate code needed to work with the HTTP Response and makes the intent of the test easier to comprehend.

    Then
        .Response
        .ShouldBe
        .BadRequest
        .WithMessage("Insufficient Funds to make withdrawal.");

Headers

Bard allows you to assert that the correct headers are present in your API Response.

[Fact]
public void Should_included_content_type_header_with_correct_value()
{
   When.Get(URL);

   Then
       .Response
       .Headers
       .ShouldInclude("Content-Type", "application/json; charset=utf-8");
           
}

When

Execute your tests against your API by calling When on your scenario.

This is essentially a wrapper over the standard .NET HttpClient but that adds additional logging.

When
    .Post("api/bankaccounts/1234/withdrawals", new Deposit {Amount = 100});

Then

This is the test assertion part of your test.

Bard provides a fluent API for working with your API responses. You can Assert that the response is as expected.

Writing A Test

var creditRequest = new CreditRequest {CustomerId = "id0201", Credit = 7000};

When
   .Grpc(client => client.CheckCreditRequest(creditRequest));

Then.Response.ShouldBe.Ok();

Bad Requests

The response can be checked to ensure the correct error message or error code is returned by the API.

When your API returns an HTTP 400 Bard give you the ability to interrogate the API response in more detail.

Configuration

Basic Configuration

Basic configuration of Bard means the Arrangement part of the test cannot be utilized.

To configure our Scenario a few thing need to be set.

  • Client: our HTTP Client to call our API with (Required)

  • LogMessage: An action to instruct Bard where to log test output to. This is optional but important because Bard provides detailed logs to the test output windows to help with writing and debugging test failures. If not supplied Bard will output all log messages to the Console window which may not appear in your test output. (Optional)

  • Services: An instance of a .NET service provider to allow Bard access to use Dependency Injection. (Optional)

  • BadRequestProvider: Override the default bad request provider with your own implementation. (Optional)

Advanced Configuration

Advanced configuration of Bard means that a StoryBook is specified which allows all the features of Bard to be used within the scenario.

The configuration of our Scenario is almost the same as the basic configuration apart from that we specify that we are going to use a StoryBook.

Our StoryBook might look something like this.

And our StoryData is just a plain old c# object (POCO) of your choice.

StoryData

StoryData is a plain old C# object (POCO) of your choice. An instance of this class will be instantiated at the beginning of your test arrangement and is accessible in every story. This means you can enrich your StoryData as your stories progress.

You can only have one instance of StoryData per configured scenario.

The story data can be accessed from within a story from the scenario context.

The example below demonstrates accessing the story data to get the correct id to use in the URL and updating the story data with the result of the API call.

Migration Guide

Version 2 to 3

does not contain a definition for 'Then'

does not contain a definition for 'That'

Version 1 to 2

[CS0246] The type or namespace name 'IScenarioContext' could not be found

Replace all references of IScenarioContext with ScenarioContext

The type arguments for method 'IChapterGiven<..... cannot be inferred

Example this code:

Should change to:

Cannot resolve UseResult

refactored to:

HTTP Response

HTTP Response Assertion

Bard natively supports out of the box the following HTTP responses

  • HTTP 200 OK

  • HTTP 201 Created

  • HTTP 204 No Content

  • HTTP 403 Forbidden

  • HTTP 404 Not Found

If you need to Assert another HTTP code was returned then you can specify the HTTP code as well.

Dependency Injection

In most of the examples in our Stories we have called our API directly to perform the test arrangement. Sometimes this isn't good enough, we may need to simulate an incoming message on a Message Queue or want to insert some data directly into our API Database.

If this is the case we can tell Bard how to resolve services when we configure the Scenario and access them within the Story through the ScenarioContext. This is particularly easy if you are testing a .NET API.

Dependency Injection Configuration

Using ServiceProvider In A Story

FAQ

xUnit.net

No output from Bard when using xUnit & dotnet test.

Try this.

var scenario = ScenarioConfiguration
                .Configure(options =>
                {
                     options.Client = httpClient;
                     options.LogMessage = output.WriteLine;
                     options.Services = host.Services;
                     options.BadRequestProvider = new MyBadRequestProvider();
                });
var scenario = ScenarioConfiguration
                .WithStoryBook<BankingStory, BankingStoryData>()
                .Configure(options =>
                {
                    options.Client = httpClient;
                    options.LogMessage = output.WriteLine;
                    options.Services = host.Services;
                    options.BadRequestProvider = new MyBadRequestProvider();
                });
public class BankingStory : StoryBook<BankingStoryData>
{
    // Stories would go here.
}        
 public class BankingStoryData
 {
    // Put whatever properties you want here 
 }
// BEFORE
.Then<TransferInstructionCreatedChapter>();

// NEW
.ProceedToChapter<TransferInstructionCreatedChapter>();
// BEFORE
Scenario.Given.That
    .An_investor_has_been_created();

// AFTER
Scenario.Given
    .An_investor_has_been_created();
public static readonly Func<ScenarioContext, StoryInput, StoryParameters, StoryOuput> AccountTransactionCreated =
    (context, input, parameters) =>
    {        
        StoryOutput output = // Do Some work here
        return ouput;
    };
public static readonly Func<ScenarioContext<StoryInput>, StoryParameters, StoryOuput> AccountTransactionCreated =
    (context, parameters) =>
    {       
        // Story Input is now accessed through the Scenario Context
        var storyInput = context.StoryInput; 
        // Do work here
        StoryOutput output = // Do Some work here
        return ouput;
    };
 .UseResult(clientRegistered => newClient = clientRegistered );
.GetResult(out NewClientRegistered? newClient);
 Then
     .Response
     .ShouldBe
     .Ok();
 
 // OR
 
 Then
     .Response
     .ShouldBe
     .Forbidden();
 Then
     .Response
     .ShouldBe
     .StatusCodeShouldBe(HttpStatusCode.Ambiguous);
var hostBuilder = new HostBuilder()
    .ConfigureWebHost(builder =>
        builder
            .UseStartup<Startup>()
            .UseTestServer()
            .UseEnvironment("development"));

var host = hostBuilder.Start();

var httpClient = host.GetTestClient();

Scenario = ScenarioConfiguration
    .Configure(options =>
    {
        options.Client = httpClient;
        options.LogMessage = output.WriteLine;
        options.Services = host.Services;
    });
public BankAccountHasBeenCreated BankAccount_has_been_created()
{
    return When(context =>
        {
            var bankAccountMessage = new BankAccount
            {
                CustomerName = "Ranulph Fiennes"
            };

            var serviceBus = context.Services.GetService<IServiceBus>()
            
            serviceBus.AddMessage(message);
            
            context.StoryData.BankAccount = bankAccountMessage;
            
        })
        .ProceedToChapter<BankAccountHasBeenCreated>();
}
dotnet test --logger:"console;verbosity=detailed"

Story

As described in the introduction the story is where the work is actually done where we can perform our test arrangement.

We can have a few different flavors of stories depending upon where we are in the scenario and how we want to use the story.

Starting Story

A starting story is a story that is written in the opening chapter of a StoryBook. It is different from other stories in the fact that it does not have any input from other stories.

Simple Story

A simple story has come from another chapter and therefore has input from the previous story.

Notice we have access to the context and the bankAccount on line 3.

public DepositMade Make_deposit(decimal amount)
{
    return When((context) =>
        {
            var request = new Deposit{ Amount = amount};
            var response = context.Api.Post($"api/bankaccounts/{bankAccount.Id}/deposits", request);
            context.StoryData.Deposit = response;           
        })
        .ProceedToChapter<DepositMade>();
}

Advanced Story

Sometimes in the course of a scenario that it makes sense that we want to reuse our story in another chapter. For example with our Banking example after we create a bank account we can either make a withdrawal or a deposit. If we make a withdrawal we should be able to make a further withdrawal or a further deposit. In essence we want to be able to recursively make withdrawals and deposits using our fluent interface.

We could just copy and paste the code into each chapter but that could present a maintenance problem in the future.

However because Bard takes a functional approach to describing it's stories we can define a common library of Story functions and compose our Chapter with those functions.

Lets demonstrate with an example:

public static class BankingScenarioFunctions
{
    public static readonly Action<ScenarioContext<BankingStoryData>, Deposit> MakeADeposit =
            (context, request) =>
            {
                context.Api.Post($"api/bankaccounts/{context.StoryData?.BankAccountId}/deposits",
                    request);
            };
}

What does this mean?

  • ScenarioContext this is our test context.

  • DepositRequest this is the API parameter that is passed into the function which means we can pass it from our story rather than hard code it in this function.

Now in our Chapter we write our Story by referencing our Function

public DepositMade Deposit_has_been_made(decimal amount)
{
    return
        Given((storyData) => new Deposit {Amount = amount})
            .When(BankingScenarioFunctions.MakeADeposit)
            .ProceedToChapter<DepositMade>();
}

Notice that on line 4 we are calling the base Method Given and passing in a function that describes how to create our API request object.

StoryBook

As described in the introduction a StoryBook is the starting Chapter of our Scenario.

Lets illustrate what that really means, with an example. I will use my fictitious Banking API to demonstrate.

We want to test the scenario in our Banking API that the balance of our account is correct after we make a cash deposit.

Before we can do that there are a couple of steps we need to do to test that scenario.

  1. Create the bank account

  2. Deposit the money into the correct account.

In Bard after we have created the bank account we can pass the results from the 'Create Bank Account' story into the 'Deposit Money' story this is useful because we probably need to know the Id of the newly created bank account.

Lets demonstrate what that would look like with a code example.

public BankAccountHasBeenCreated BankAccount_has_been_created()
{
    return When(context =>
        {
            var bankAccount = new BankAccount
            {
                CustomerName = "Ranulph Fiennes"
            };

            var response = context.Api.Post("api/bankaccounts", bankAccount);
                
            context.StoryData.BankAccountId = response.Id;
        })
        .ProceedToChapter<BankAccountHasBeenCreated>();
}

Lets talk about what's going on in that example. The first thing to note is we are calling the base method When and this is where we do the 'work' of the story is done by calling our create bank account endpoint. Notice that we are making our API call using our TestContext on line 10.

Now on line 12 we are returning we are updating our StoryData with the response from the API call, making that data available to the next story.

context.StoryData.BankAccountId = response.Id;

Finally on line 14 we call the generic ProceedToChapter base method. This instructs our fluent API what the next Chapter is that will help us build out our Fluent Test interface.

.ProceedToChapter<BankAccountHasBeenCreated>();

 public DepositMade Deposit_has_been_made(Deposit depositRequest)
 {
    return  
           When(context => 
           {
               var response = context.Api.Post($"api/bankaccounts/{context.StoryData?.BankAccountId}/deposits",
                    depositRequest);
                    
               response.ShouldBe.Ok();
               
               context.StoryData.DepositAmount = depositRequest.Amount;
           })
           .ProceedToChapter<DepositMade>();
 }
Story Data flow

Given

Given

This section how to perform test arrangement with Bard. This is the part of the test that tells the 'story' of the state of the system for the test to run.

In my opinion this is the most important part of an integration test and probably the part that is most overlooked when writing tests.

When testing a complex domain test arrangement can become convoluted. A framework like Bard encourages the developers to invest more time upfront to building a library of stories. However these stories be reused and composed into different scenarios. Overtime this will make your tests easier to write, easier to understand and easier to maintain.

In summary the goal of Bard is to make your tests:

  1. Easy to read and understand.

  2. Easy to reuse.

  3. More maintainable

  4. Easy to compose new tests.

Bard has the concept of a StoryBook. The Story Book describes the way you can interact with you API/Domain in order to put it in the required state. Think of a StoryBookas the opening chapter of your scenario.

A StoryBook is the entry point into a Scenario.

StoryBooks, Chapters & Stories

A StoryBook is made up of Chapters & Stories.

Story Books, Chapters & Stories

The StoryBook is the starting Chapter of our Scenario and from there we can select a Story that take us to the next Chapter that contains other stories.

Chapters are our decision points. What can we do given what has happened already.

Stories are the building blocks. This is where the work is done and we make changes to our domain, usually through making an API call.

What makes Bard different from other BDD style testing frameworks is that the output from one Story is the input to the next chapter and the containing stories.

Bard uses a functional approach to test arrangement and employs the Collection Pipeline Pattern

Collection pipelines are a programming pattern where you organize some computation as a sequence of operations which compose by taking a collection as output of one operation and feeding it into the next. - Martin Fowler

This is a powerful feature which means the output of one Story can be the input to the next Story. This can make your tests easier to write and easier to understand.

.NET Fiddles

This page links to some .NET Fiddles to demonstrate. More to be added.

Writing Your First Test

This section will describe how to configure Bard in order to write your first API test. This example demonstrates the simplest way to use Bard and does not use the more advanced StoryBook feature which we will cover in more detail in this section of the documentation.

Configure The Scenario

So before we can start we need to configure our Scenario. To do this we are going to need provide two things.

  • A System.Net.Http.HttpClient in order to call our API

  • A function to tell our Scenario how to log. This is optional but important because Bard provides detailed logs to the test output windows to help with writing and debugging test failures. If not supplied Bard will ouput all log messages to the Console window which may not appear in your test output.

var scenario = ScenarioConfiguration
                .Configure(options =>
                {
                     options.Client = httpClient;
                     options.LogMessage = output.WriteLine;
                });

The GitHub repository has some example tests you can see a working example here.

Write The First Test

We've configured our Scenario now. Assuming our API is running now we can start by calling it.

So lets give it a spin by writing a test that calls our fictitious Banking API and creating a new bank account. We expect our API to return a 201 Created response.

[Fact]
public void When_creating_a_bank_account()
{
      Scenario
              .When
              .Post("/api/bankaccount/, new BankAccount
                {
                    CustomerName = "Kilian Jornet"
                });

      Scenario
              .Then
              .Response
              .ShouldBe
              .Created();
}

And we're done. If we look at the output from our Test we should see some nice logging that includes the request and the response.

****************************************
*             WHEN                     *
****************************************

REQUEST: POST api/bankaccounts
{
  "id": null,
  "isActive": false,
  "customerName": "Kilian Jornet",
  "balance": 0.0
}

RESPONSE: Http Status Code:  Created (201)
Header::Location http://localhost/api/bankaccounts/1
{
  "id": 1,
  "isActive": false,
  "customerName": "Kilian Jornet",
  "balance": 0.0
}

Configuration

Configure The Scenario

So before we can start we need to configure our Scenario. To do this we are going to need provide a number of things.

 var scenario = GrpcScenarioConfiguration
                .UseGrpc<MyGrpcClient>()
                .WithStoryBook<MyStoryBook, MyStoryData>()
                .Configure(options =>
                {
                    options.Services = _host.Services;
                    options.LogMessage = s => _output.WriteLine(s);
                    options.GrpcClient = c => new MyGrpcClient(c);
                    options.Client = _httpClient;
                });
  1. Line 2 we specify our grpc generated client.

  2. Line 3 we specify our StoryBook & Story Data

  3. Line 6 we provide an instance of our Service Provider so we can use dependency injection within our Stories (Optional)

  4. Line 7 we specify how to log our output (Recommended)

  5. Line 8 provides a delegate function to instantiate an instance of our gRPC client (Required)

  6. Line 9 provides an instance of our HTTP Client.

Change Log

2.0.0 - 1-08-2020

Changed

  • IScenarioContext now removed. Should be replaced with ScenarioContext instead.

  • Changed method signatures on Chapters to make them simpler. The Funcs used to require the Chapter Input in the method signature. Now the Chapter input is accessible on the ScenarioContext

  • Changed UseResult method to access tests output. Used to be called UseResult but is now called GetResult.

.UseResult(account => bankAccount = account);

Now uses:

.GetResult(out BankAccount? bankAccount);

Note: UseResult used to terminate the test arrangement. Now GetResult allows you to continue with the test arrangement.

  • Removed CommonExtensions so now cannot use .And .A etc between test arrangements.

1.2.0 - 28-07-2020

Added

  • Added better logging when building a chapter and not calling the Context's API. Previously nothing was logged to the console. Now Bard tracks if the API has been called or not and if it hasn't it defaults to outputing the response from the story.

1.1.1 - 26-07-2020

Fixed

  • Bug when asserting HTTP code returned from API due to Shouldly not working in Release build.

Changed

  • Removed dependency on Shouldly library.

1.1.1 - 26-07-2020

Fixed

  • Bug when asserting HTTP code returned from API due to Shouldly not working in Release build.

Changed

  • Removed dependency on Shouldly library.

1.0.0 - 25-07-2020

Initial Release

Basic 200 OK Success | C# Online Compiler | .NET Fiddle

Chapter

A Chapter is used to navigate through our Scenario. It contains Stories. It allows us to guide the test author what they can do next.

The important thing to remember is that the Chapter can receive the output of the previous Story as an input via the StoryData. We do that by inheriting from the base Class Chapter and specify our StoryData class as the Generic Type. Our class should look like this.

public class BankAccountHasBeenCreated : Chapter<MyStoryData>
{
}

Now we have our Chapter we can add our Stories.

Logo

Installation

To test a gRPC API with Bard you will need to install the Bard.gRPC extension .

The simplest way to do this is to use either the NuGet package manager, or the dotnet CLI.

Using the NuGet package manager console within Visual Studio run the following command:

Install-Package Bard.gRPC 

Or using the .net core CLI from a terminal window:

dotnet add package Bard.gRPC 

Performance Testing

Bard provides the ability to assert that the API responses are returned within a specified timeframe. This can be asserted at a test level or globally for an entire Scenario.

Individual API Call Assertion

The test below show an individual assertion for a specific end point.

[Fact]
public void The_slow_running_endpoint_should_return_within_two_seconds()
{
    When
        .Get("api/slow-running-endpoint");
   
    Then
        .Response
        .Time
        .LessThan(2000); // Time in milliseconds.
    
}

Global Configuration

The scenario configuration below demonstrates how to set a global benchmark for all API calls so that they do not exceed 2000 milliseconds.

var scenario = ScenarioConfiguration
    .Configure(options =>
    {
        options.MaxApiResponseTime = 2000;
        // Other configuration goes here..
    });