【问题标题】:Mocking Hangfire RecurringJob Dependency in .Net Core 2在.Net Core 2中模拟Hangfire RecurringJob依赖
【发布时间】:2018-08-14 14:58:06
【问题描述】:

考虑以下控制器:

public class SubmissionController : Controller
{ 
    public SubmissionController()
    { }

    public IActionResult Post()
    {
        RecurringJob.AddOrUpdate(() => InitiateSubmission(), Cron.Minutely);

        return Ok("Periodic submission triggered");
    }
}

Hangfire 是否提供抽象注入 RecurringJob 类的依赖项?我做了一些研究,唯一可用的抽象是IBackgroundJobClient,它没有安排重复工作的选项。

我需要验证作业是否已添加到单元测试中。

【问题讨论】:

    标签: c# dependency-injection asp.net-core-2.0 hangfire


    【解决方案1】:

    如果您检查RecurringJob 类的source code,您会看到它的静态方法导致调用RecurringJobManager 类:

    public static class RecurringJob
    {
        private static readonly Lazy<RecurringJobManager> Instance = new Lazy<RecurringJobManager>(
            () => new RecurringJobManager());
    
        //  ...
    
        public static void AddOrUpdate(
            Expression<Action> methodCall,
            string cronExpression,
            TimeZoneInfo timeZone = null,
            string queue = EnqueuedState.DefaultQueue)
        {
            var job = Job.FromExpression(methodCall);
            var id = GetRecurringJobId(job);
    
            Instance.Value.AddOrUpdate(id, job, cronExpression, timeZone ?? TimeZoneInfo.Utc, queue);
        }
    
        //  ...
    }
    

    RecurringJobManager 实现了IRecurringJobManager 接口,您可以使用该接口在 UT 中进行依赖注入和模拟。

    但是RecurringJob 具有从 lambda 获取工作并构建工作 ID 的内部逻辑:

    var job = Job.FromExpression(methodCall);
    var id = GetRecurringJobId(job);
    

    Job.FromExpression() 是您可以安全使用的公共方法。但是GetRecurringJobId 是一个私有方法,定义如下:

    private static string GetRecurringJobId(Job job)
    {
        return $"{job.Type.ToGenericTypeString()}.{job.Method.Name}";
    }
    

    GetRecurringJobId 基本上以SubmissionController.InitiateSubmission 的形式返回作业方法的名称。它基于内部类TypeExtensions 以及Type 的扩展方法。你不能直接使用这个类,因为它是内部的,所以你应该复制那个逻辑。

    如果您采用这种方法,您的最终解决方案将是:

    TypeExtensions(复制自Hangfire sources):

    static class TypeExtensions
    {
        public static string ToGenericTypeString(this Type type)
        {
            if (!type.GetTypeInfo().IsGenericType)
            {
                return type.GetFullNameWithoutNamespace()
                    .ReplacePlusWithDotInNestedTypeName();
            }
    
            return type.GetGenericTypeDefinition()
                .GetFullNameWithoutNamespace()
                .ReplacePlusWithDotInNestedTypeName()
                .ReplaceGenericParametersInGenericTypeName(type);
        }
    
        private static string GetFullNameWithoutNamespace(this Type type)
        {
            if (type.IsGenericParameter)
            {
                return type.Name;
            }
    
            const int dotLength = 1;
            // ReSharper disable once PossibleNullReferenceException
            return !String.IsNullOrEmpty(type.Namespace)
                ? type.FullName.Substring(type.Namespace.Length + dotLength)
                : type.FullName;
        }
    
        private static string ReplacePlusWithDotInNestedTypeName(this string typeName)
        {
            return typeName.Replace('+', '.');
        }
    
        private static string ReplaceGenericParametersInGenericTypeName(this string typeName, Type type)
        {
            var genericArguments = type.GetTypeInfo().GetAllGenericArguments();
    
            const string regexForGenericArguments = @"`[1-9]\d*";
    
            var rgx = new Regex(regexForGenericArguments);
    
            typeName = rgx.Replace(typeName, match =>
            {
                var currentGenericArgumentNumbers = int.Parse(match.Value.Substring(1));
                var currentArguments = string.Join(",", genericArguments.Take(currentGenericArgumentNumbers).Select(ToGenericTypeString));
                genericArguments = genericArguments.Skip(currentGenericArgumentNumbers).ToArray();
                return string.Concat("<", currentArguments, ">");
            });
    
            return typeName;
        }
    
        public static Type[] GetAllGenericArguments(this TypeInfo type)
        {
            return type.GenericTypeArguments.Length > 0 ? type.GenericTypeArguments : type.GenericTypeParameters;
        }
    }
    

    RecurringJobManagerExtensions:

    public static class RecurringJobManagerExtensions
    {
        public static void AddOrUpdate(this IRecurringJobManager manager, Expression<Action> methodCall, Func<string> cronExpression, TimeZoneInfo timeZone = null, string queue = EnqueuedState.DefaultQueue)
        {
            var job = Job.FromExpression(methodCall);
            var id = $"{job.Type.ToGenericTypeString()}.{job.Method.Name}";
    
            manager.AddOrUpdate(id, job, cronExpression(), timeZone ?? TimeZoneInfo.Utc, queue);
        }
    }
    

    注入IRecurringJobManager的控制器:

    public class SubmissionController : Controller
    {
        private readonly IRecurringJobManager recurringJobManager;
    
        public SubmissionController(IRecurringJobManager recurringJobManager)
        {
            this.recurringJobManager = recurringJobManager;
        }
    
        public IActionResult Post()
        {
            recurringJobManager.AddOrUpdate(() => InitiateSubmission(), Cron.Minutely);
    
            return Ok("Periodic submission triggered");
        }
    
        public void InitiateSubmission()
        {
            // ...
        }
    }
    

    嗯,这种方法可行,但我不喜欢它。它基于一些内部的 Hangfire 东西,将来可能会改变。

    这就是为什么我建议使用另一种方法。您可以添加新的外观接口(例如IRecurringJobFacade),它将模仿您将要使用的RecurringJob 中的方法。这个接口的实现只会调用相应的RecurringJob 方法。然后你将这个IRecurringJobFacade 注入到控制器中,并且可以很容易地在 UT 中模拟它。这是一个示例:

    IRecurringJobFacade:

    public interface IRecurringJobFacade
    {
        void AddOrUpdate(Expression<Action> methodCall, Func<string> cronExpression);
    
        //  Mimic other methods from RecurringJob that you are going to use.
        // ...
    }
    

    RecurringJobFacade:

    public class RecurringJobFacade : IRecurringJobFacade
    {
        public void AddOrUpdate(Expression<Action> methodCall, Func<string> cronExpression)
        {
            RecurringJob.AddOrUpdate(methodCall, cronExpression);
        }
    }
    

    注入IRecurringJobFacade的控制器:

    public class SubmissionController : Controller
    {
        private readonly IRecurringJobFacade recurringJobFacade;
    
        public SubmissionController(IRecurringJobFacade recurringJobFacade)
        {
            this.recurringJobFacade = recurringJobFacade;
        }
    
        public IActionResult Post()
        {
            recurringJobFacade.AddOrUpdate(() => InitiateSubmission(), Cron.Minutely);
    
            return Ok("Periodic submission triggered");
        }
    
        public void InitiateSubmission()
        {
            // ...
        }
    }
    

    如您所见,这种方法更简单,最重要的是它更可靠,因为它不会深入研究 Hangfire 内部结构,而是像往常一样调用 RecurringJob 方法。

    当无法直接模拟代码(静态方法或不基于接口的类)时,通常会使用这种外观接口。我在实践中使用的其他一些示例:模拟 System.IO.FileDateTime.NowSystem.Timers.Timer 等。

    【讨论】:

    • 您的回答帮助我将 IRecurringJobFacade 注入我的组件(顺便说一句,服务而不是控制器)并模拟 RecurringJob 功能。不过还有一个补充:在我的情况下,.AddOrUpdate 方法的 methodCall 参数包含参数本身:recurringJobFacade.AddOrUpdate(activity.Id.ToString(), () => RunTask(file, activity), cron);结果我得到了以下错误:“无法将 lambda 表达式转换为预期的委托类型,因为块中的某些返回类型不能隐式转换为委托返回类型”
    • 解决方案是在facade接口的实现中使用RecurringJobManager: public class RecurringJobFacade : IRecurringJobFacade { public void AddOrUpdate(string recurringJobId, Expression methodCall, string cronExpression) { RecurringJobManager manager = new RecurringJobManager(); manager.AddOrUpdate(recurringJobId, methodCall, cronExpression); } } 因此可以传递参数/参数。为糟糕的格式道歉。
    【解决方案2】:

    我有一个类似的案例:RecurringJob.RemoveIfExists。我试试这个(我在 github 中看到了原始代码并设置了我的模拟):

        private void SetupHangfire()
        {
            Mock<JobStorage> _jobStorageMock = new Mock<JobStorage>();
            Mock<IStorageConnection> _storageConnectionMock = new Mock<IStorageConnection>();
            Mock<IWriteOnlyTransaction> _transactionConnectionMock = new Mock<IWriteOnlyTransaction>();
    
            JobStorage.Current = _jobStorageMock.Object;
    
            _jobStorageMock
                .Setup(y => y.GetConnection())
                .Returns(_storageConnectionMock.Object);
    
            _storageConnectionMock
                .Setup(y => y.AcquireDistributedLock(It.IsAny<string>(), It.IsAny<TimeSpan>()))
                .Returns(_transactionConnectionMock.Object);
    
            _storageConnectionMock
                .Setup(y => y.CreateWriteTransaction())
                .Returns(_transactionConnectionMock.Object);
    
            _transactionConnectionMock
                .Setup(y => y.RemoveHash(It.IsAny<string>()));
    
            _transactionConnectionMock
                .Setup(y => y.RemoveFromSet(It.IsAny<string>(), It.IsAny<string>()));
    
            _transactionConnectionMock
                .Setup(y => y.Commit());
        }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2017-06-09
      • 1970-01-01
      • 1970-01-01
      • 2021-04-17
      • 2021-03-22
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多