Demo application ToDoList - Galad/tranquire GitHub Wiki

Overview

The repository contains an application that demonstrates how to use Tranquire with a BDD framework (in this case, SpecFlow), and how to build an automation library that exposes a set of business actions as an API for a consumer. It also shows how we can, from behavior descriptions, verify the system behavior at different levels (UI, API, etc.).

The demo application is a simple to-do list app, similar to this one. It has only been extended to use web APIs.

This app is composed of 3 projects:

  • src/ToDoList: the main application.
  • src/ToDoList.Automation: the automation library that exposes an API to instrument the system under test
  • tests/ToDoList.Specifications: the test project, that contains the scenarios to automate.

Prerequisites

Before running the UI tests, you will need to compile the front end application:

cd src\ToDoList\ClientApp
npm install
npm run build

Running the tests

Running the tests does not require any particular environment. It takes advantage of the ability of ASP.NET Core to create a web server for the application. This allows to write and run the test directly from Visual Studio, without the need to go add data or configure an environment outside the repository.

Scenarios

The project ToDoList.Specification uses Gherkin to describes the scenarios that should be supported by our application, in a pure BDD style, and uses SpecFlow to automate them. The scenarios are located in the Features folder. They are composed to steps, which are mapped to C# functions which are located in the Steps folder. It is those functions that uses execute the actions created with Tranquire.

For instance, the step:

When I add the item "buy some milk"

Is mapped to

[Given(@"I add the item ""(.*)""")]
public async Task GivenIAddTheItem(string item)
{
    await Context.Actor().Given(Add.TheToDoItem(item));
}

Test levels

The demo app shows 2 different levels of test: API and UI. The idea is to take advantage of the behavior scenarios written in Gherkin. This this is BDD, we only focus on the behavior of the system, and not how the user interacts with it. This allows us to define with which kind of interaction we want to verify our interactions, what is called here a test level. For instance I can verify the scenario Add an item using only the web API, in which case I'll use the API to add the item, and get the list of items to verify the result. But I can also verify this scenario through the user interface, in which case I'll use the user interface to add an item, and get the list of items from the user interface as well.

Every scenarios do not fit for all levels. In the scenarios, this is represented by a tag (@UI, @Api) that allows to filter the tests to run for the current level.

To run the tests at the API level, change the default test level to TestLevel.Api.

To run the tests at the UI level, change it back to TestLevel.UI.

Using tagged action to expose the same behavior on different levels

You may have notice that the step When I add an item "Buy some milk" was mapped to only one action in Tranquire:

await Context.Actor().Given(Add.TheToDoItem(item));

So how is it able to be executed through the UI and the web API ?

The answer is by using Tagged actions, that identify an action for a particular level, and expose them as single, transparent action.

public static IAction<Task> TheToDoItem(string title) => Actions.CreateTagged(
            $"Add the to-do item {title}",
            (TestLevel.UI, UI.Add.TheToDoItem(title).Select(_ => Task.CompletedTask)),
            (TestLevel.Api, Api.Add.TheToDoItem(title))
            );

We use Actions.CreateTagged to create the action that wrap the other actions. We can give to the action the same title $"Add the to-do item {title}", whether it is a UI action or an API action, because they both will have the same behavior on the system.

Then we declare each action for its level:

(TestLevel.UI, UI.Add.TheToDoItem(title).Select(_ => Task.CompletedTask))

Note that this action must return Task instead of Unit because the API action returns a Task.

(TestLevel.Api, Api.Add.TheToDoItem(title))

Using lower level actions to speed up the Given step

A benefit of Tranquire is to be able to select a faster action for the Given step. In a BDD scenario, what is important to control in the test run are the When and the Then steps. But for the Given step, it does not matter how we end with the result of the step, as long as it is there. So for instance, for the scenario Remove an item while executing the test run on the UI level, is to execute the I have a list with the items "buy some milk,feed the dog,prepare the lunch" with the API. This is way faster and reliable than using the user interface to add the items, and we can do that because the scenario verifies the behavior of the Remove feature.

This is the default behavior of the tagged actions. It uses a priority between the tags to identify the appropriate action to execute in a given context (Given, When or Then).

The priority is configured in an actor ability in the Setup class:

var levels = new[] { TestLevel.UI, TestLevel.Api }.Where(l => l >= TestLevel).ToArray();
actor = actor.CanUse<IActionTags<TestLevel>>(ActionTags.Create(levels));

The class created from ActionTags.Create(levels) contains what tags are available, and their priority. We filter the tags from the original list with .Where(l => l >= TestLevel) so that when running the tests at the Api level, only the API level actions are executed. The priority here is determined by the position in the array, so in { TestLevel.UI, TestLevel.Api }, a UI action will more likely be executed in a When or Then context, while an API action will more likely be executed in the Given context.

This allows some flexibility. You can decide to implement only the UI action action. In this case it will be executed, even in the Given context, since there is no API action available.

Organizing the automation API

The API is composed of static classes that provide static factories of actions and questions. It is organized around 2 layers: a top layer, which is the entry point for consumers, and compose actions of a lower layer that represents the test levels. This lower layer represents the test levels as separate namespaces, but they can very well be represented by separate projects.

Top layer

The top layer provides factories in static classes that take a verb as a name. They provide methods that represents the action that is performed on the system. The API follows the pattern [Verb].[Action](parameters). The goal is to provide a natural API for the consumer, that looks like a sentence.

  • Add.TheToDoItem(title)
  • Add.TheToDoItems(title)
  • Remove.TheToDoItem(title)
  • Get.ToDoItems
  • Get.RemainingItems Those methods do only the composition of the test level layer actions. The data model is also provided by this layer (here in the Model namespace).

Test level layer

The test level layer provides also a set of static factories following the same pattern, but the implementation of those methods will return actions that do actual work. For instance for the API, the method Add.TheToDoItem is:

public static IAction<Task> TheToDoItem(string title) => new AddToDoItem(title);

public class AddToDoItem : ActionBase<HttpClient, Task>
{
    public AddToDoItem(string title)
    {
        Title = title;
    }

    public override string Name => $"Add the to-do item {Title}";

    public string Title { get; }

    protected override async Task ExecuteWhen(IActor actor, HttpClient ability)
    {
        var response = await ability.PostAsync("api/todoitem", new StringContent(JsonConvert.SerializeObject(Title), Encoding.UTF8, "application/json"));
        response.EnsureSuccessStatusCode();
    }
}

UI Test level layer

The UI level is more complex because it requires more interaction with the system. The actions and questions are still represented as static factories. Internally though, we need to represent how the UI is designed in order to build the actions.

For instance, adding an item is done on the to-do page, typing text in an input and pressing the enter key. So we represent the UI component, in this case the ToDoPage, where we make available the possible actions and questions, in this case the method AddToDoItem. This method uses the target NewToDoItemInput that represents the input. Note that the target should be kept private to the component class, which should only expose actions and questions.

Generally you will have several level of components, for instance page -> business component -> framework component.

⚠️ **GitHub.com Fallback** ⚠️