-
Notifications
You must be signed in to change notification settings - Fork 6
How To Create New Test
This article will guide you through the process of creating new test project.
We have a set of classes which help to properly instantiate test infrastructure so author of the test can focus test logic.
Features:
-
instantiate IoC Container as close to production as possible including all dependency registrations used by system
-
pick up all AutoMapper configurations
-
detect test target and provide facilities to properly initialize and manage lifecycle of the fixture
-
Create regular test project for xUnit
-
Add following references:
SpecFlow
SpecFlow.Tools.MsBuild.Generation
SpecFlow.xUnit
Version>=3.1.42-beta
Example 1.1: Add following packages to
*.csproj
<PackageReference Include="SpecFlow" Version="3.1.42-beta" /> <PackageReference Include="SpecFlow.Tools.MsBuild.Generation" Version="3.1.42-beta" /> <PackageReference Include="SpecFlow.xUnit" Version="3.1.42-beta" />
-
Add
test-config.json
file to provide required configuration. Example 1.2:{ "TestTarget": "Unit", "TestLogName": "ia-test-run.log", //specific log name for test project "IdentityContextTableStorageOptions": { //configuration options needed for tests. Read during test start "ConnectionString": "localconnectionstring", "UsersTable": "users0azurecloudtests" } }
It is used to hook up current test project with common setup logic used by VolleyM.
-
Create class:
<Context>StepsBase.cs
. -
Inherit from
DomainTestSetupBase
class and add[Binding]
attribute. Example 2.1:[Binding] public class IdentityAndAccessTestSetup : DomainTestSetupBase { // define ctor to satislfy base public IdentityAndAccessTestSetup(IObjectContainer objectContainer) : base(objectContainer) { } }
-
Add OneTimeSetup hooks. Example 2.2:
[BeforeTestRun] public static void BeforeTestRun() { // skip this if your test project does not need Integration tests. Should go first TestRunFixtureBase.OneTimeFixtureCreator = CreateOneTimeTestFixture; // this is required TestRunFixtureBase.BeforeTestRun(); } [AfterTestRun] public static void AfterTestRun() { TestRunFixtureBase.AfterTestRun(); } private static IOneTimeTestFixture CreateOneTimeTestFixtur(TestTarget target) { return target switch { // if you don't need One Time setup use NoOpOneTimeTestFixture TestTarget.Unit => NoOpOneTimeTestFixture.Instance, TestTarget.AzureCloud => new AzureCloudIdentityAndAccessOneTimeFixture(), TestTarget.OnPremSql => throw new NotSupportedException(), _ => throw new ArgumentOutOfRangeException(nameof(target), target, null) }; }
-
Configure
IAuthFixture
hook. In almost all of the cases you need it turned on.Example 2.3:
protected override bool RequiresAuthorizationFixture => true;
-
Configure
I<Context>TestFixture
type. It is needed so test fixture can be resolved by specific type instead of baseITestFixture
interface.Example 2.4:
protected override Type GetConcreteTestFixtureType => typeof(IIdentityAndAccessFixture);
Skip this if you don't need test fixture for this test project
-
Provide
IAssemblyBootstrapper
instances used by your code. It is needed to have DI configuration as close to production as possible.Example 2.5:
protected override IEnumerable<IAssemblyBootstrapper> GetAssemblyBootstrappers(TestTarget target) { var result = new List<IAssemblyBootstrapper> { new DomainIdentityAndAccessAssemblyBootstrapper() }; if (target == TestTarget.AzureCloud) { result.Add(new InfrastructureIdentityAndAccessAzureStorageBootstrapper()); } return result; }
3. Add I<Context>TestFixture
interface (Optional: If you need common logic setup. You should have a reason to skip it)
It is used to properly setup state of the test
-
Implement
ITestFixture
interface for each target.Example 3.1:
public interface IIdentityAndAccessFixture : ITestFixture {} internal class UnitTestIdentityAndAccessFixture : IIdentityAndAccessFixture { private IUserRepository _repositoryMock; public void RegisterScenarioDependencies(Container container) { // hook to register dependencies in container _repositoryMock = Substitute.For<IUserRepository>(); container.Register(() => _repositoryMock, Lifestyle.Scoped); } public Task ScenarioSetup() { // logic to run before scenario } public Task ScenarioTearDown() { // logic to run after scenario } }
-
Implement fixture factory.
Use type created in Example 2.4.
Example 3.2: Override create method.
protected override ITestFixture CreateTestFixture(TestTarget target) { return target switch { TestTarget.Unit => (IIdentityAndAccessFixture)new UnitTestIdentityAndAccessFixture(), TestTarget.AzureCloud => new AzureCloudIdentityAndAccessFixture(Container), TestTarget.OnPremSql => throw new NotSupportedException(), _ => throw new ArgumentOutOfRangeException(nameof(target), target, null) }; }
-
Implement
IOneTimeTestFixture
interface for each target.Example 4.1:
public class AzureCloudIdentityAndAccessOneTimeFixture : IOneTimeTestFixture { private IdentityContextTableStorageOptions _options; public void OneTimeSetup(IConfiguration configuration) { // run your setup logic } public void OneTimeTearDown() { // run your teardown logic } }
-
Provide
IOneTimeTestFixture
instance factory beforeTestRunFixtureBase.OneTimeSetup
. See Example 2.2
- Add manually or use VS menu to add new item.
- Describe feature using Gherkin.
- Right click inside a feature file =>
Generate Step Definitions
- Save file
- Add
Scope
attribute so SpecFlow won't try to reuse similar step definitions from different features.
Example 6.1:
[Binding]
[Scope(Feature = "Get User by ID")]
public class GetUserSteps
{
...
}
- Inject required dependencies via constructor.
Example 6.2:
public GetUserSteps(IIdentityAndAccessFixture testFixture, IAuthFixture authFixture, Container container)
{
// Available only if you completed step 2.5
_testFixture = testFixture;
// Available only if you completed step 2.4
_authFixture = authFixture;
// Always available
_container = container;
}
- Register Dependencies needed for this particular feature.
Example 6.3:
[BeforeScenario(Order = Constants.BEFORE_SCENARIO_REGISTER_DEPENDENCIES_ORDER)]
public void RegisterDependencies()
{
...
}
- Hook before scenario method to run scenario setup logic
Example 6.4:
[BeforeScenario(Order = Constants.BEFORE_SCENARIO_STEPS_ORDER)]
public void ScenarioSetup()
{
...
}
- Setup authorization
In order for Authorization to authorize test user you have to mock permission you need.
Example 6.5:
[BeforeScenario(Order = Constants.BEFORE_SCENARIO_STEPS_ORDER)]
public void ScenarioSetup()
{
_authFixture.SetTestUserPermission(new Permission(Permissions.Context, Permissions.User.GetUser));
}
-
Inherit from
VolleyM.Domain.UnitTests.Framework.EventAssertionsSteps
class. SeeVolleyM.Domain.Players.UnitTests.EventAssertionStepsHook
as an example. -
(Optional) Create required transformations for Event properties that are not supported by SpecFlow.
-
(Optional) Register those transformations in
<Context>StepsBase.cs
. SeeVolleyM.Domain.Players.UnitTests.PlayersTestSetup::GetAssemblyTransforms
method as an example.
During test run scenarios are running in parallel. When you store them in real storage, like Azure Storage, you might run into race condition. That's why it's important to have unique IDs for entities you manipulate during test.
We leveraged Bogus
library to generate random data and provide uniqueness during tests. It also has features for deterministic configuration so we have a way to debug some corner cases.
See VolleyM.Domain.IdentityAndAccess.UnitTests.Fixture.UserBuilder
as an example.