-
Notifications
You must be signed in to change notification settings - Fork 0
NUnitTutorial
Please note: This tutorial is not yet complete. Issue #154 is open to complete it.
This tutorial will take you through writing your first Screenplay test with NUnit. Throughout this tutorial we will be writing tests for a sample To-Do list application.
These sample tests will use web browser automation to test the deployed application just like a normal user would.
We will start by creating a new .NET project to contain our Screenplay tests. Into this project we must install the Screenplay for NUnit NuGet package. We have also decided that these tests will use browser automation, which means that we will also need to install the Screenplay for Selenium plugin package.
Screenplay tests can coexist in the same project with non-Screenplay tests. It's best to keep them separate though.
Next, we need to configure Screenplay using an integration configuration class. We only need a minimal one at the moment; here's an example.
using CSF.Screenplay.NUnit;
using CSF.Screenplay.Integration;
[assembly: ScreenplayAssembly(typeof(IntegrationConfig))]
public class IntegrationConfig : IIntegrationConfig
{
public void Configure(IIntegrationConfigBuilder builder)
{
}
}Before writing actual test logic it's important to have decided what we're testing. For the sake of this tutorial we'll write a pair of empty test methods with the high-level steps, which describe the intention, written in comments.
Even though these are empty there are a few things to point out:
- The step comments are written from an imaginary user's perspective. In this tutorial we call that user Joe.
- We used a given/when/then style of categorising those test steps into preconditions, the tested action & the expected outcome.
- We have decorated the test methods with the
[Screenplay]attribute. - We have added an
ICastparameter to each.
using NUnit.Framework;
using CSF.Screenplay;
using CSF.Screenplay.NUnit;
[TestFixture]
public class SampleTest
{
[Test, Screenplay]
public void A_new_item_can_be_added_to_an_empty_list(ICast cast, BrowseTheWeb browseTheWeb)
{
// Given Joe opens an empty to-do list
// When Joe adds a new item called "Wash dishes"
// Then the top item should be called "Wash dishes"
}
[Test, Screenplay]
public void A_new_item_is_added_to_the_top_of_the_list(ICast cast, BrowseTheWeb browseTheWeb)
{
// Given Joe opens an empty to-do list
// And he has added an item called "Buy bread"
// When Joe adds a new item called "Buy shampoo"
// Then the top item should be called "Buy shampoo"
}
}The starting point for almost any test is the actor - in this case Joe. Actors in NUnit tests come from the ICast. We also need to give this actor the ability to use a web browser. Web browser automation is non-trivial to configure and we don't want to repeat that configuration in every test. We'll get it from dependency injection so we can centralise that config later.
Let's see the two tests with our actor in place:
// One extra 'using' needed:
using CSF.Screenplay.Selenium.Abilities;
[Test, Screenplay]
public void A_new_item_can_be_added_to_an_empty_list(ICast cast, BrowseTheWeb browseTheWeb)
{
var joe = cast.Get("Joe");
joe.IsAbleTo(browseTheWeb);
// The rest of the test is unchanged
}
[Test, Screenplay]
public void A_new_item_is_added_to_the_top_of_the_list(ICast cast, BrowseTheWeb browseTheWeb)
{
var joe = cast.Get("Joe");
joe.IsAbleTo(browseTheWeb);
// The rest of the test is unchanged
}This could be improved-upon by using a persona class for the "Joe" actor. That would immediately realise two benefits. Firstly the string name and the assignment of the ability to the actor would be de-duplicated. Secondly, there would be no need to inject the
BrowseTheWebability into the test method.
Both test designs share the same first step: "Given Joe opens an empty to-do list". In our simple app, accomplishing this is just a case of opening a web browser at the correct URL and waiting for the page to show. Let's write a task class to encapsulate this interaction concept.
using CSF.Screenplay.Actors;
using CSF.Screenplay.Performables;
using CSF.Screenplay.Selenium.Builders;
using CSF.Screenplay.Selenium.Models;
public class OpenAnEmptyToDoList : Performable
{
protected override string GetReport(INamed actor)
{
return $"{actor.Name} opens an empty to-do list.";
}
protected override void PerformAs(IPerformer actor)
{
// You may need to update the URL based on your test hosting
actor.Perform(OpenTheirBrowserOn.TheUrl("http://localhost/"));
var thePage = new CssSelector("body", "the page");
actor.Perform(Wait.Until(thePage).IsVisible());
}
}What have we done here?
- In the
PerformAsmethod, we described the steps required to open an empty to-do list. - In the
GetReportmethod, we created a human-readable description of this step.- Strictly-speaking this is optional, but it's good practice.
Notice though, that we didn't write any logic which directly uses a Selenium WebDriver. The two interactions with the web page are delegated to action objects, created by the builder classes OpenTheirBrowserOn & Wait. These actions & builders are built into the Screenplay Selenium plugin.
This could be further improved by moving the URL and the CSS selector to a class which represents the to-do list page. This would keep the 'static' information about that page centralised.
Now we have a task, we can replace the comment in our two tests with a usage of that task.
The lines in each of our tests which currently read:
// Given Joe opens an empty to-do listmay be replaced with an actual usage of the task as follows:
Given(joe).WasAbleTo<OpenAnEmptyToDoList>();To make this work you will need one extra using declaration:
using static CSF.Screenplay.StepComposer;Let's create a task to add a new to-do item. This task will build on the previous example because it will require a parameter: the name of the to-do item.
using CSF.Screenplay.Actors;
using CSF.Screenplay.Performables;
using CSF.Screenplay.Selenium.Builders;
using CSF.Screenplay.Selenium.Models;
public class AddAToDoItem : Performable
{
readonly string item;
protected override string GetReport(INamed actor)
{
return $"{actor.Name} adds a to-do item named '{item}'.";
}
protected override void PerformAs(IPerformer actor)
{
var newItemTextbox = new CssSelector("#newItemText", "the new-item text box");
actor.Perform(Enter.TheText(item).Into(newItemTextbox));
var theAddButton = new CssSelector("#newItemButton", "the add-item button");
actor.Perform(Click.On(theAddButton));
}
public AddAToDoItem(string item)
{
this.item = item;
}
public static IPerformable Named(string item)
{
return new AddAToDoItem(item);
}
}There's a lot going on here; some of it is similar to the previous example and some is new. Let's just look at the fundamental differences.
- The parameter which represents the item we are adding:
itemwas passed to our task's constructor and stored as areadonlyfield.- In screenplay it is normal for all such task parameters to be constructor injected.
- Two actions were used once again, invoked from builders.
Enter.TheTextClick.On
- The
itemvalue was used with the relevant action. It was also used inGetReport. - We created a static builder method to provide a natural API for creating the task.
- For example
AddAToDoItem.Named("name here"). - Usage of the builder pattern like this is common in Screenplay.
- For example
You might notice that because parameters are constructor-injected, each task instance is designed to be used only once.
Let's use that new task in our test. The process is very similar to step 6, above. Replace the following line:
// When Joe adds a new item called "Wash dishes"... with the following:
When(joe).AttemptsTo(AddAToDoItem.Named("Wash dishes"));So far, the two tasks we have written have done something but haven't returned a response. Now we are going to write a task which reads and returns some information from the page.
using CSF.Screenplay.Actors;
using CSF.Screenplay.Performables;
using CSF.Screenplay.Selenium.Builders;
using CSF.Screenplay.Selenium.Models;
public class TheTopToDoItem : Performable<string>, IQuestion<string>
{
protected override string GetReport(INamed actor)
{
return $"{actor.Name} reads the top to-do item.";
}
protected override string PerformAs(IPerformer actor)
{
var topItem = new CssSelector("#toDoList :first-child", "the top to-do item");
var text = actor.Perform(TheText.Of(topItem));
return text;
}
public static IQuestion<string> FromTheList() => new TheTopToDoItem();
}Notice how this class derives from the generic Performable<TReturn> and implements IQuestion<string>? This is how we create a task which returns a value. The other difference between this and previous examples is that the Perform method returns a value rather than being void.
Integrating this into the test and finishing off this scenario is now easy. Replace the last pseudo-code comment with the following.
var theText = Then(joe).ShouldRead(TheTopToDoItem.FromTheList());
Assert.That(theText, Is.EqualTo("Wash dishes"));Notice that the assertion logic remains in the NUnit test case. It is a bad idea to put assertion logic into your Screenplay questions because it makes them less reusable.
Let's see the second test case (from step three), completed using Screenplay. We won't need to write any extra tasks for this, because the ones we already have are sufficient.
[Test, Screenplay]
public void A_new_item_is_added_to_the_top_of_the_list(ICast cast, BrowseTheWeb browseTheWeb)
{
var joe = cast.Get("Joe");
joe.IsAbleTo(browseTheWeb);
Given(joe).WasAbleTo<OpenAnEmptyToDoList>();
Given(joe).WasAbleTo(AddAToDoItem.Named("Buy bread"));
When(joe).AttemptsTo(AddAToDoItem.Named("Buy shampoo"));
var theText = Then(joe).ShouldRead(TheTopToDoItem.FromTheList());
Assert.That(theText, Is.EqualTo("Buy shampoo"));
}