Skip to content

Commit 17db8a3

Browse files
authored
Merge pull request #303 from csf-dev:269-di-docco
Resolve #269 - DI docco
2 parents 6e42207 + d1a1abe commit 17db8a3

15 files changed

Lines changed: 209 additions & 103 deletions

CSF.Screenplay.Abstractions/IPersona.cs

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,22 @@
22

33
namespace CSF.Screenplay
44
{
5-
/// <summary>A persona is a factory for a commonly-used actor</summary>
5+
/// <summary>A persona is a factory for a commonly-used <see cref="Actor"/></summary>
66
/// <remarks>
77
/// <para>
8-
/// In Screenplay is is recommended to use memorable actors which are widely understood and recognisable
9-
/// by the team. This is easier if the composition of an actor is the same across every <see cref="IPerformance"/> in which
10-
/// they participate.
8+
/// In Screenplay is is recommended to use &amp; reuse memorable actors, which are widely understood and recognisable
9+
/// to the development team. This is easier if the composition of an actor is the same across every
10+
/// <see cref="IPerformance"/> in which they participate. Personas facilitate this; the persona class serves
11+
/// as a consistent factory which creates the same named actor in the same manner every time.
1112
/// </para>
1213
/// <para>
13-
/// By using a separate persona implementation for each named actor, the developer can ensure consistent creation
14-
/// for instances of those actors.
14+
/// Developers should create an implementation of this interface for each actor which they wish to
15+
/// define, ideally named the same as the name as the Actor. Each persona implementation should
16+
/// name the actor and grant them <xref href="AbilityGlossaryItem?text=the+abilities"/> which are appropriate.
17+
/// </para>
18+
/// <para>
19+
/// Instance of persona classes are resolved from the Dependency Injection container, so it is appropriate
20+
/// and correct to constructor-inject any services which are required in order to get the ability instances.
1521
/// </para>
1622
/// </remarks>
1723
public interface IPersona : IHasName
@@ -26,4 +32,4 @@ public interface IPersona : IHasName
2632
/// <param name="performanceIdentity">A unique identity for the currently-executing performance</param>
2733
Actor GetActor(Guid performanceIdentity);
2834
}
29-
}
35+
}

CSF.Screenplay.Docs/docs/GettingReports.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ The default filename includes a representation of the current timestamp, so each
1717
This path may be altered by specifying a different set of options to the [`AddScreenplay`] service collection extension method.
1818

1919
[`ScreenplayOptions.ReportPath`]: xref:CSF.Screenplay.ScreenplayOptions.ReportPath
20-
[`AddScreenplay`]: xref:CSF.Screenplay.ScreenplayServiceCollectionExtensions.AddScreenplay(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.Action{CSF.Screenplay.ScreenplayOptions})
20+
[`AddScreenplay`]: xref:CSF.Screenplay.ScreenplayServiceCollectionExtensions.AddScreenplay(Microsoft.Extensions.DependencyInjection.IServiceCollection)
2121

2222
## Converting JSON to a human-readable report
2323

CSF.Screenplay.Docs/docs/bestPractice/WritingTests.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Advice for writing tests with Screenplay
22

3-
Whichever testing integration you choose to use, there are some general pieces of advice and best practice which apply.
3+
Whichever testing integration you choose to use, there are some general pieces of advice and best practice which apply.
44

55
## Use high-level tasks
66

@@ -38,16 +38,16 @@ Assertion libraries to consider include [Shouldly] and [Fluent Assertions].
3838
## Do not include assertions in performables
3939

4040
It might be tempting to include assertion syntax within [performables] such as [tasks].
41-
This is not recommended.
41+
This is not recommended.
4242
Where assertions appear within performables:
4343

4444
* They reduce the reusability of the performables
4545
* They create a dependency between your performables and your testing framework/assertion library
4646
* They can make test logic harder to read and understand
4747

48-
It is recommended to keep your performables/tasks free from assertions.
49-
In your test code, use [questions] or question-like tasks to get values/data from the system under test.
48+
It is recommended to keep your performables/tasks free from assertions.
49+
In your test code, use [questions] or question-like tasks to get values/data from the system under test.
5050
Write your assertions in your main test logic, asserting that the values retrieved are as-expected.
5151

5252
[tasks]: ../../glossary/Task.md
53-
[questions]: ../../glossary/Question.md
53+
[questions]: ../../glossary/Question.md

CSF.Screenplay.Docs/docs/concepts/HowScreenplayAndPerformanceRelate.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ If Screenplay is not being used for testing, then the consuming logic has the fo
2525

2626
[Performance]: xref:CSF.Screenplay.IPerformance
2727
[`Screenplay`]: xref:CSF.Screenplay.Screenplay
28-
[`Create`]: xref:CSF.Screenplay.Screenplay.Create(System.Action{Microsoft.Extensions.DependencyInjection.IServiceCollection},System.Action{CSF.Screenplay.ScreenplayOptions})
28+
[`Create`]: xref:CSF.Screenplay.Screenplay.Create(System.Action{Microsoft.Extensions.DependencyInjection.IServiceCollection})
2929
[`BeginScreenplay`]: xref:CSF.Screenplay.Screenplay.BeginScreenplay
3030
[`ExecuteAsPerformanceAsync<TPerformance>`]: xref:CSF.Screenplay.ScreenplayExtensions.ExecuteAsPerformanceAsync``1(CSF.Screenplay.Screenplay,System.Collections.Generic.IList{CSF.Screenplay.Performances.IdentifierAndName},System.Threading.CancellationToken)
3131
[`ExecuteAsPerformance`]: xref:CSF.Screenplay.ScreenplayExtensions.ExecuteAsPerformance(CSF.Screenplay.Screenplay,System.Func{System.IServiceProvider,System.Nullable{System.Boolean}},System.Collections.Generic.IList{CSF.Screenplay.Performances.IdentifierAndName},System.Threading.CancellationToken)

CSF.Screenplay.Docs/docs/dependencyInjection/AddingServices.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
There are two techniques to add dependency services to Screenplay.
44
You may either integrate Screenplay into an existing container for your application or tests or you may add additional services via the static [`Create`] factory method.
55

6-
[`Create`]: xref:CSF.Screenplay.Screenplay.Create(System.Action{Microsoft.Extensions.DependencyInjection.IServiceCollection},System.Action{CSF.Screenplay.ScreenplayOptions})
6+
[`Create`]: xref:CSF.Screenplay.Screenplay.Create(System.Action{Microsoft.Extensions.DependencyInjection.IServiceCollection})
77

88
## Integrating with an existing container
99

@@ -29,7 +29,7 @@ var screenplay = Screenplay.Create(services => {
2929
});
3030
```
3131

32-
[`Screenplay.Create`]: xref:CSF.Screenplay.Screenplay.Create(System.Action{Microsoft.Extensions.DependencyInjection.IServiceCollection},System.Action{CSF.Screenplay.ScreenplayOptions})
32+
[`Screenplay.Create`]: xref:CSF.Screenplay.Screenplay.Create(System.Action{Microsoft.Extensions.DependencyInjection.IServiceCollection})
3333

3434
## A reminder on lifetime scopes
3535

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Injecting services into Personas
2+
3+
Types which derive from [`IPersona`] support constructor-injected dependencies.
4+
Personas are typically used by either [the cast] or [the stage] to get an [Actor].
5+
The technique in which they are used means that they are resolved, along with their constructor-injected dependencies, from DI.
6+
7+
Use constructor-injected dependencies in persona classes to provide access to the APIs required to resolve [Abilities] that the actor is to be granted.
8+
9+
[`IPersona`]: xref:CSF.Screenplay.IPersona
10+
[the cast]: xref:CSF.Screenplay.ICast
11+
[the stage]: xref:CSF.Screenplay.IStage
12+
[Actor]: xref:CSF.Screenplay.Actor
13+
[Abilities]: ../../glossary/Ability.md
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Constructor injection for bindings
2+
3+
For frameworks which are based on **binding classes** such as [Reqnroll], services are constructor-injected into binding classes.
4+
Use dependencies injected in this way to get access to [commonly-used Screenplay services] and anything else required at the root level of your test logic.
5+
6+
[Reqnroll]: https://reqnroll.net/
7+
[commonly-used Screenplay services]: InjectableServices.md
8+
9+
## Example
10+
11+
This is an example of a Reqnroll-style binding class with a constructor-injected dependency.
12+
This would require [installing the Reqnroll test integration] to run.
13+
Take note of the `stage` parameter in the primary constructor.
14+
15+
```csharp
16+
[Binding]
17+
public class SampleSteps(IStage stage)
18+
{
19+
[Given("Simon shows a sample step")]
20+
public async Task GivenSimonShowsASampleStep()
21+
{
22+
var simon = stage.Spotlight<Simon>();
23+
24+
// ... use the actor with some performables, then make assertions etc
25+
}
26+
}
27+
```
28+
29+
[installing the Reqnroll test integration]: ../gettingStarted/reqnroll/index.md

CSF.Screenplay.Docs/docs/dependencyInjection/DependencyInjectionScope.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,18 @@ uid: DependencyInjectionScopeArticle
55
# Dependency injection scope
66

77
Developers familiar with dependency injection are likely to be familiar with the concept of [DI Scopes].
8-
That is - some services which are designated as _scoped_ or _instance per scope_ use a common/shared instance for the lifetime/duration of the scope.
8+
Screenplay uses this concept; a number of its services are added to the container with _a per-scope lifetime_.
9+
Screenplay creates **a new DI scope per [Performance]**.
10+
As you can see on [the diagram of how Actors, Abilities and Performables relate to one another], each [`Screenplay`] contains and executes many performances.
11+
This scope-creation is handled automatically by the Screenplay framework logic.
912

10-
Within Screenplay logic, a DI scope is automatically created, with a lifetime matching that of the current [Performance].
1113
Within a performance, when any of the scoped services (listed below) are injected, each point of injection will receive the same shared instance of that service.
1214
Instances are independent per-performance; each performance gets its own shared instance of each of the listed services.
1315

1416
[DI Scopes]: https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection#service-lifetimes
1517
[Performance]: xref:CSF.Screenplay.Performance
18+
[the diagram of how Actors, Abilities and Performables relate to one another]: ../concepts/MakeupOfAScreenplay.md
19+
[`Screenplay`]: xref:CSF.Screenplay.Screenplay
1620

1721
## List of scoped services
1822

@@ -25,6 +29,7 @@ The following services are added to DI "per lifetime scope".
2529
## List of singleton services
2630

2731
The following services are added to DI as singletons.
32+
There is only ever a single instance of these services per Screenplay.
2833

2934
* [The Screenplay](xref:CSF.Screenplay.Screenplay)
3035
* [The event bus](xref:CSF.Screenplay.Performances.PerformanceEventBus)
Lines changed: 16 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,24 @@
11
---
22
uid: InjectingServicesArticle
33
---
4-
54
# Injecting services
65

7-
There are a few techniques in which injecting dependencies is relevant. These are summarised below.
8-
All but one of these techniques provide access to [the services which are added to the container].
9-
10-
[the services which are added to the container]: InjectableServices.md
11-
12-
## Into test logic
13-
14-
When using automated test logic based upon Screenplay, the use of dependency injection typically takes one of two forms.
15-
Which of these depends upon the nature and paradigm of the test framework.
16-
17-
For frameworks which are based on **test methods** such as [NUnit], services are typically injected via _method parameter injection_ into the test methods.
18-
[If Screenplay were to be extended] to work with frameworks such as xUnit or MSTest then this is likely to be the technique used.
19-
20-
For frameworks which are based on **binding classes** such as [Reqnroll], services are constructor-injected into binding classes.
21-
22-
Use dependencies injected in this way to get access to [commonly-used Screenplay services] and anything else required at the root level of your test logic.
23-
24-
[NUnit]: https://nunit.org/
25-
[If Screenplay were to be extended]: ../extendingScreenplay/TestIntegrations.md
26-
[Reqnroll]: https://reqnroll.net/
27-
[commonly-used Screenplay services]: InjectableServices.md
28-
29-
## Into standalone performance logic
6+
The table below summarises how to get services, from various contexts. In the first column are contexts/situations in which a developer could require services from DI. The second column indicates and links to the appropriate technique for that scenario.
307

31-
If you are [using Screenplay standalone] then the [`Screenplay.ExecuteAsPerformanceAsync`] permits resolution of dependencies via its parameter.
32-
That parameter is a `Func<IServiceProvider,CancellationToken,Task<bool?>>`.
33-
The service provider may be used to resolve dependency services for the performance's logic.
8+
All but one of these techniques (the last) provides full access to [the services which are added to the container].
349

35-
Developers are urged to consider encapsulating their performance logic in implementations of [`IHostsPerformance`].
36-
Through an overload (extension method) named [`ExecuteAsPerformanceAsync<T>`], developers may specify the concrete implementation of that interface.
37-
This extension method will resolve that implementation type along with any of its constructor-injected dependencies.
38-
This avoids the service locator anti-pattern and provides a convenient pattern by which to write performance logic.
10+
| Context | Technique |
11+
| ---------------------------------------------------- | -------------------------------------------------------- |
12+
| NUnit style test method | [Parameter inject into the test] method |
13+
| Reqnroll style binding class | [Constructor inject into the binding] class |
14+
| Standalone Screenplay: [`ExecuteAsPerformanceAsync`] | [Create a performance host, or use the service provider] |
15+
| Persona class | [Constructor inject into the Persona] class |
16+
| Performable class, such as a Task | [Use the actor's abilities] |
3917

40-
Use services resolved from the service provider, or injected into your [`IHostsPerformance`] implementation, to get access to [commonly-used Screenplay services] and anything else required at the root level of your performance logic.
41-
42-
[using Screenplay standalone]: ../gettingStarted/nonTesting/index.md
43-
[`Screenplay.ExecuteAsPerformanceAsync`]: xref:CSF.Screenplay.Screenplay.ExecuteAsPerformanceAsync(System.Func{System.IServiceProvider,System.Threading.CancellationToken,System.Threading.Tasks.Task{System.Nullable{System.Boolean}}},System.Collections.Generic.IList{CSF.Screenplay.Performances.IdentifierAndName},System.Threading.CancellationToken)
44-
[`IHostsPerformance`]: xref:CSF.Screenplay.IHostsPerformance
45-
[`ExecuteAsPerformanceAsync<T>`]: xref:CSF.Screenplay.ScreenplayExtensions.ExecuteAsPerformanceAsync``1(CSF.Screenplay.Screenplay,System.Collections.Generic.IList{CSF.Screenplay.Performances.IdentifierAndName},System.Threading.CancellationToken)
46-
47-
## Into personas
48-
49-
Types which derive from [`IPersona`] support constructor-injected dependencies.
50-
Personas are typically used by either [the cast] or [the stage] to get an [Actor].
51-
The technique in which they are used means that they are resolved, along with their constructor-injected dependencies, from DI.
52-
53-
Use constructor-injected dependencies in persona classes to provide access to the APIs required to resolve [Abilities] that the actor is to be granted.
54-
55-
[`IPersona`]: xref:CSF.Screenplay.IPersona
56-
[the cast]: xref:CSF.Screenplay.ICast
57-
[the stage]: xref:CSF.Screenplay.IStage
58-
[Actor]: xref:CSF.Screenplay.Actor
59-
[Abilities]: ../../glossary/Ability.md
60-
61-
## Into performables
62-
63-
_See the article explaining [how performables get their dependencies]_.
64-
65-
[how performables get their dependencies]: Performables.md
18+
[the services which are added to the container]: InjectableServices.md
19+
[Parameter inject into the test]: ParameterInjectionForTests.md
20+
[Constructor inject into the binding]: ConstructorInjectionForBindings.md
21+
[Create a performance host, or use the service provider]: InjectionForStandaloneScreenplay.md
22+
[Constructor inject into the Persona]: ConstructorInjectIntoThePersona.md
23+
[`ExecuteAsPerformanceAsync`]: xref:CSF.Screenplay.Screenplay.ExecuteAsPerformanceAsync(System.Func{System.IServiceProvider,System.Threading.CancellationToken,System.Threading.Tasks.Task{System.Nullable{System.Boolean}}},System.Collections.Generic.IList{CSF.Screenplay.Performances.IdentifierAndName},System.Threading.CancellationToken)
24+
[Use the actor's abilities]: Performables.md
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Dependency injection for standalone Screenplay
2+
3+
If you are [using Screenplay standalone] then you are likely using the method [`Screenplay.ExecuteAsPerformanceAsync`] or one of its overloads.
4+
5+
[using Screenplay standalone]: ../gettingStarted/nonTesting/index.md
6+
[`Screenplay.ExecuteAsPerformanceAsync`]: xref:CSF.Screenplay.Screenplay.ExecuteAsPerformanceAsync(System.Func{System.IServiceProvider,System.Threading.CancellationToken,System.Threading.Tasks.Task{System.Nullable{System.Boolean}}},System.Collections.Generic.IList{CSF.Screenplay.Performances.IdentifierAndName},System.Threading.CancellationToken)
7+
8+
## Recommended: Use `IHostsPerformance`
9+
10+
Developers are urged to consider encapsulating their performance logic in a class which derives from [`IHostsPerformance`].
11+
Through an overload (extension method) named [`ExecuteAsPerformanceAsync<T>`], developers may specify the concrete implementation type of that class as a generic type parameter.
12+
This overload of `ExecuteAsPerformanceAsync` will resolve your class which derives from `IHostsPerformance` from dependency injection (_remember to add it to the container!_).
13+
This resolution will include all the class' dependencies which are constructor-injected and leads to very clean code.
14+
The service provider is never exposed, thus removing the temptation to use the service locator anti-pattern.
15+
16+
[`IHostsPerformance`]: xref:CSF.Screenplay.IHostsPerformance
17+
[`ExecuteAsPerformanceAsync<T>`]: xref:CSF.Screenplay.ScreenplayExtensions.ExecuteAsPerformanceAsync``1(CSF.Screenplay.Screenplay,System.Collections.Generic.IList{CSF.Screenplay.Performances.IdentifierAndName},System.Threading.CancellationToken)
18+
19+
### Example of the performance host technique
20+
21+
This example is functionally identical to the example below.
22+
23+
```csharp
24+
public class MyPerformance(ICast cast) : IHostsPerformance
25+
{
26+
public Task<bool?> ExecutePerformanceAsync(CancellationToken cancellationToken)
27+
{
28+
var aaron = cast.GetActor<Aaron>();
29+
// Further performance logic ...
30+
}
31+
}
32+
33+
// To consume the above:
34+
await screenplay.ExecuteAsPerformanceAsync<MyPerformance>();
35+
```
36+
37+
## Alternative: Use a function
38+
39+
The original [`Screenplay.ExecuteAsPerformanceAsync`] overload accepts up to three parameters, the first of which is a function.
40+
That function should contain the logic which makes up the performance.
41+
The function (which drives the performance logic) provides two parameters itself:
42+
43+
* An [`IServiceProvider`](xref:System.IServiceProvider)
44+
* A [`CancellationToken`](xref:System.Threading.CancellationToken)
45+
46+
The serivce provider may be used to resolve services which may be used by the performance logic.
47+
48+
### Example of the function-based technique
49+
50+
This example is functionally identical to the example above.
51+
52+
```csharp
53+
await screenplay.ExecuteAsPerformanceAsync((services, cancellationToken) => {
54+
var cast = services.GetRequiredService<ICast>();
55+
var aaron = cast.GetActor<Aaron>();
56+
// Further performance logic ...
57+
});
58+
59+
```

0 commit comments

Comments
 (0)