DEV Community

Shantanu
Shantanu

Posted on

xUnit - Run async code specific to test before test

xUnit allows you to run code before each test using the BeforeAfterTestAttribute.

But, you cannot run asynchronous code using this attribute. This is needed in many situations.

To solve this problem, I have created a custom abstract xUnit attribute BeforeAsyncAfterSyncTestAttribute,

inheriting from BeforeAfterTestAttribute, that allows you to run asynchronous code before each test or group of tests.

    public abstract class BeforeAsyncAfterSyncTestAttribute : BeforeAfterTestAttribute
    {
        private static string? _lastTestStamp = null;
        private static Type? _lastType = null;


        public BeforeAsyncAfterSyncTestAttribute(Type specificAttributeType, string stamp) : base()
        {
            if (!typeof(IRunBeforeTest).IsAssignableFrom(specificAttributeType))
                throw new ArgumentException($"{specificAttributeType.Name} must implement ITestCondition");

            var specificAttribute = (IRunBeforeTest)Activator.CreateInstance(specificAttributeType)!;

            if (specificAttribute != null && specificAttribute.Run != null && (string.IsNullOrEmpty(_lastTestStamp)
                        || ((specificAttributeType != _lastType) && (Guid.Parse(stamp) != Guid.Parse(_lastTestStamp)))))
            {
                specificAttribute.Run();
                _lastTestStamp = stamp;
                _lastType = specificAttributeType;
            }
        }
    }
Enter fullscreen mode Exit fullscreen mode

Inherit and create a class for each test.

   public class LoadModelBeforeTestAttribute : BeforeAsyncAfterSyncTestAttribute
   {
       public LoadModelBeforeTestAttribute(Type specificAttributeType, string stamp) : base(specificAttributeType, stamp)
       {
       }

       public override void After (MethodInfo methodUnderTest)
       {
           // This method runs synchronously after the test. You can use it to clean up resources after the test, if necessary.
       }
   }

   public class SetModelPathBeforeTestAttribute : BeforeAsyncAfterSyncTestAttribute
   {
       public SetModelPathBeforeTestAttribute(Type specificAttributeType, string stamp) : base(specificAttributeType, stamp)
       {
       }

       public override void After(MethodInfo methodUnderTest)
       {
           // This method runs synchronously after the test. You can use it to clean up resources after the test, if necessary.
       }
   }
Enter fullscreen mode Exit fullscreen mode

There is an interface your specific Test attribute has to implement.

    public interface IRunBeforeTest
    {
        public Action Run { get; }
    }
Enter fullscreen mode Exit fullscreen mode

In the interface implementation, specific to each test, put your code specific to the Test in the Run Action, as shown below.

Here you put your asynchronous code.

Examples

    public class LoadAIModel : IRunBeforeTest
    {
        public Action Run => async () =>
        {
            // Arrange
            // Path to load model
            string modelPath = Path.Combine(Environment.CurrentDirectory, "SampleWebsite-AI-Model.zip");

            await PredictionEngine.LoadModelAsync(modelPath);
        };
    }

    public class SetAIModelPath : IRunBeforeTest
    {
        public Action Run => async () =>
        {
            // Arrange
            // Path to load model
            string modelPath = Path.Combine(Environment.CurrentDirectory, "SampleWebsite-AI-Model.zip");
            // Provide the path to the AI model
            PredictionEngine.AIModelLoadFilePath = modelPath;
        };
    }
Enter fullscreen mode Exit fullscreen mode

Then, you can decorate those specific tests.
Provide a Guid (as a string) as a parameter. This Guid should be unique to the test.

        [LoadModelBeforeTest(typeof(LoadAIModel), "5bb02c70-01d1-4987-8a6e-ab7fc8b1dcc4")]
        [Theory]
        [InlineData("What are the requisites for carbon credits?", Scheme.ACCU)]
        [InlineData("How do I calculate net emissions?", Scheme.SafeguardMechanism)]
        [InlineData("What is the colour of a rose?", Scheme.None)]
        public async Task Load_Predict(string userInput, Scheme expectedResult)
        {
            var input = new ModelInput { Feature = userInput };

            // Act
            var prediction = await PredictionEngine.PredictAsync(input);            

            // Assert
            Assert.NotNull(prediction);
            Assert.Equal(expectedResult, (Scheme)prediction.PredictedLabel);
        }

        [SetModelPathBeforeTest(typeof(SetAIModelPath), "d54e2920-ad42-4acc-a6e2-37aad8e9ac3f")]
        [Theory]
        [InlineData("What are the requisites for carbon credits?", Scheme.ACCU)]
        [InlineData("How do I calculate net emissions?", Scheme.SafeguardMechanism)]
        [InlineData("What is the colour of a rose?", Scheme.None)]
        public async Task AutoLoad_Predict(string userInput, Scheme expectedResult)
        {
            var input = new ModelInput { Feature = userInput };

            // Act
            var prediction = await PredictionEngine.PredictAsync(input);

            // Assert
            Assert.NotNull(prediction);
            Assert.Equal(expectedResult, (Scheme)prediction.PredictedLabel);
        }
Enter fullscreen mode Exit fullscreen mode

Run all the tests in the class.

Your specific code will run ONLY ONCE before each group of Theory Tests.

So, for example, your specific code in LoadAIModel will run asynchronously only once for the 3 Tests in the Theory group.

Top comments (0)