Skip to content

How To Create New Test

Sergii Diachenko edited this page Jun 6, 2020 · 9 revisions

How To: Create new test

This article will guide you through the process of creating new test project.

Benefits

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

1. Create Test Project

  1. Create regular test project for xUnit

  2. 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" />
  3. 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"
        }
    }

2. Create <Context>TestSetup.cs class

It is used to hook up current test project with common setup logic used by VolleyM.

  1. Create class: <Context>StepsBase.cs.

  2. 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) { }
        }
  3. 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)
        };
    }
  4. Configure IAuthFixture hook. In almost all of the cases you need it turned on.

    Example 2.3:

    protected override bool RequiresAuthorizationFixture => true;
  5. Configure I<Context>TestFixture type. It is needed so test fixture can be resolved by specific type instead of base ITestFixture interface.

    Example 2.4:

    protected override Type GetConcreteTestFixtureType => typeof(IIdentityAndAccessFixture);

    Skip this if you don't need test fixture for this test project

  6. 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

  1. 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
        }
    }
  2. 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)
        };
    }

4. Add IOneTimeTestFixture (Optional: If you need Integration tests)

  1. 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
        }
    }
  2. Provide IOneTimeTestFixture instance factory before TestRunFixtureBase.OneTimeSetup. See Example 2.2

5. Create feature file

  1. Add manually or use VS menu to add new item.
  2. Describe feature using Gherkin.

6. Implement steps

  1. Right click inside a feature file => Generate Step Definitions
  2. Save file
  3. 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
{
    ...
}
  1. 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;
}
  1. Register Dependencies needed for this particular feature.

Example 6.3:

[BeforeScenario(Order = Constants.BEFORE_SCENARIO_REGISTER_DEPENDENCIES_ORDER)]
public void RegisterDependencies()
{
    ...
}
  1. Hook before scenario method to run scenario setup logic

Example 6.4:

[BeforeScenario(Order = Constants.BEFORE_SCENARIO_STEPS_ORDER)]
public void ScenarioSetup()
{
    ...
}
  1. 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));
}

7. Add bindings for Events

  1. Inherit from VolleyM.Domain.UnitTests.Framework.EventAssertionsSteps class. See VolleyM.Domain.Players.UnitTests.EventAssertionStepsHook as an example.

  2. (Optional) Create required transformations for Event properties that are not supported by SpecFlow.

  3. (Optional) Register those transformations in <Context>StepsBase.cs. See VolleyM.Domain.Players.UnitTests.PlayersTestSetup::GetAssemblyTransforms method as an example.

Preventing race conditions during integration tests

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.