Search code examples
c#unit-testingrhino-mocksazure-durable-functionsdbup

Unit Test (Rhino) DBUp in Azure Durable HTTPStart Method


Technology Stack

  1. DBUP for DB upgrades
  2. Azure Durable for activities
  3. Rhino mocks for unit testing.

Situation

Currently, I have placed my DB Upgrade (DBUp) statements in the HTTPStart method as its the entry point of my durable azure function.

DeployChanges.To
.SqlDatabase(connectionString)
.WithScriptsEmbeddedInAssembly(Assembly.GetExecutingAssembly())
.LogToConsole()
.Build(); 

Problem

Problem with this approach is that DBUp uses a static class to upgrade DB and I can't use Rhino to mock methods on a static class.

Question

I thought of wrapping the DBUp part in a non-static class but then I would need to mock constructor initialization. Not sure if this would work

Code - Helper Class which upgrades DB

public class DBUPHelper
    {
        public bool UpgradeDB()
        {
            bool status = true;
            var connectionString = "Data Source=localhost;Initial Catalog=master;Integrated Security=True;Connect Timeout=15";
            var upgrader =
                DeployChanges.To
                    .SqlDatabase(connectionString)
                    .WithScriptsEmbeddedInAssembly(Assembly.GetExecutingAssembly())
                    .LogToConsole()
                    .Build();

            var result = upgrader.PerformUpgrade();

            if (!result.Successful)
            {
                status = false;
                Console.ForegroundColor = ConsoleColor.Red;
                Console.WriteLine(result.Error);
                Console.ResetColor();
            }
            Console.ForegroundColor = ConsoleColor.Green;
            Console.WriteLine("Success!");
            Console.ResetColor();
            return status;
        }
    }

Code - HTTPStart Method which calls Helper class

private static ILogger logObj;
           [FunctionName("HttpStart")]
           public static async Task<HttpResponseMessage> Run(
               [HttpTrigger(AuthorizationLevel.Function, methods: "post", Route = "orchestrators/{functionName}")] HttpRequestMessage req,
               [OrchestrationClient] DurableOrchestrationClientBase starter,
               string functionName,
               ILogger log, ExecutionContext context)
       {
           HttpResponseMessage response = null;            
           var config = new ConfigurationBuilder()
           .SetBasePath(context.FunctionAppDirectory)
           .AddJsonFile("local.settings.json", optional: true, reloadOnChange: true)
           .AddEnvironmentVariables()
           .Build();
           Helper.Helper helper = new Helper.Helper(config.GetConnectionString("ConnString"););
           if (helper.UpgradeDB())
           {
               log.LogInformation("DB Upgraded Successfully");
               logObj = log;
               try
               {
                   var provider = new MultipartMemoryStreamProvider();
                   await req.Content.ReadAsMultipartAsync(provider);
                   Application policy = await GeneratePolicyObject(provider);
                   string instanceId = await starter.StartNewAsync(functionName, policy);
                   log.LogInformation($"Started orchestration with ID = '{instanceId}'.");
                   response = starter.CreateCheckStatusResponse(req, instanceId);
                   response.Headers.RetryAfter = new RetryConditionHeaderValue(TimeSpan.FromSeconds(10));
               }
               catch (Exception ex)
               {
                   response = new HttpResponseMessage();
                   log.LogCritical(ex.ToString());
                   log.LogCritical(ex.InnerException.ToString());
                   log.LogCritical(ex.StackTrace);
                   response.Content = new StringContent(ex.ToString());
                   response.StatusCode = System.Net.HttpStatusCode.InternalServerError;
               }
           }
           else log.LogCritical("DB Upgrade Failed. Check logs for exception");
           return response;
       }

enter image description here

See the highlighted area. I want to mock the constructor initialization so that DB calls does not happen while Unit Testing.

Can anyone help, please.

Regards Tarun


Solution

  • Use an abstraction to avoid the tight coupling to implementation concerns.

    public interface IDBHelper {
        bool UpgradeDB();
    }
    
    public class DBUPHelper: IDBHelper {
        //...code omitted for brevity
    }
    

    Also, as the method under test is static expose a static field/property

    public static class MyFunction {
        //At run time this will use default helper
        public static IDBHelper Helper = new DBUPHelper();
    
        private static ILogger logObj;
        [FunctionName("HttpStart")]
        public static async Task<HttpResponseMessage> Run(
            [HttpTrigger(AuthorizationLevel.Function, methods: "post", Route = "orchestrators/{functionName}")] HttpRequestMessage req,
            [OrchestrationClient] DurableOrchestrationClientBase starter,
            string functionName,
            ILogger log, ExecutionContext context)
        {
           HttpResponseMessage response = null;      
           if (helper.UpgradeDB()) {
               log.LogInformation("DB Upgraded Successfully");
               logObj = log;
               try
               {
                   var provider = new MultipartMemoryStreamProvider();
                   await req.Content.ReadAsMultipartAsync(provider);
                   Application policy = await GeneratePolicyObject(provider);
                   string instanceId = await starter.StartNewAsync(functionName, policy);
                   log.LogInformation($"Started orchestration with ID = '{instanceId}'.");
                   response = starter.CreateCheckStatusResponse(req, instanceId);
                   response.Headers.RetryAfter = new RetryConditionHeaderValue(TimeSpan.FromSeconds(10));
               }
               catch (Exception ex)
               {
                   response = new HttpResponseMessage();
                   log.LogCritical(ex.ToString());
                   log.LogCritical(ex.InnerException.ToString());
                   log.LogCritical(ex.StackTrace);
                   response.Content = new StringContent(ex.ToString());
                   response.StatusCode = System.Net.HttpStatusCode.InternalServerError;
               }
           }
           else log.LogCritical("DB Upgrade Failed. Check logs for exception");
           return response;
        }
    }
    

    that can be replaced when testing in isolation

    public async Task TestFunction {
        //Arrange
        var helper = MockRepository.GenerateMock<IDBHelper>();        
        MyFunction.helper = helper; //<<--override default helper with mock
        helper.Stub(_ => _.UpgradeDB()).Return(false);//or true is that is what you desire
    
        //...arrange other parameters / dependencies
    
        //Act
        var actual = await MyFunction.Run(...);
    
        //Assert
        //...
    }