B.Quaink
B.Quaink

Reputation: 586

Verify BackgroundJob.Delete in Hangfire in mstest unit testing

I have a CancelScheduledJob method, taking in the Id of a backgroundjob and using the JobStorage to retrieve this Id and cancelling the matching hangfire job:

var mon = JobStorage.Current.GetMonitoringApi();
var scheduledJobs = mon.ScheduledJobs(0, int.MaxValue);
var jobsToDelete = scheduledJobs.Where(job => job.Value.Job?.Args?.Any(arg => arg is Guid guid && guid == id) == true).ToList();

jobsToDelete?.ForEach(job => _backgroundJobClient.Delete(job.Key));

Verifying on an Enqueue(), or Schedule() method is possible by verifying the Create method called in the backgroundJobClient mock, such as here:

_backgroundJobClientMock.Verify(x => x.Create(
                    It.Is<Job>(job => job.Method.Name == "Run" && Guid.Parse(job.Args[0].ToString()) == input),
                    It.IsAny<ScheduledState>()));

But how would I go about verifying the Delete method? I am already mocking the JobStorage, but cannot seem to find a way to verify the Delete() method. Currently I have this:

_backgroundJobClientMock.Verify(
                x => x.Delete(It.Is<string>(jobId => jobId == "job1")),
                Times.Once
            );

But I run into the common issue that Delete is an extension method and cannot be used in a setup/verification expression.

Upvotes: 2

Views: 56

Answers (3)

Ivan Petrov
Ivan Petrov

Reputation: 4987

You can do something "flaky" and test for the instance method that is called from the extension method - (current source code):

public static bool Delete(
    [NotNull] this IBackgroundJobClient client,
    [NotNull] string jobId,
    [CanBeNull] string fromState) {
    if (client == null) throw new ArgumentNullException(nameof(client));

    var state = new DeletedState();
    return client.ChangeState(jobId, state, fromState);
}

This would be ChangeState, so Verify against it.

EDIT:

If you cannot or (don't want to) wrap things in a custom interface in your actual code as the other answers suggest, you can check this answer of mine for an alternative approach. It was for verifying ILoggers extension methods like LogInformation, but it should work here too if you are depending on IBackgroundJobClient. In a nutshell, You create a IBackgroundJobClientExt:IBackgroundJobClient interface that includes the extension methods explicitly. Then use DispatchProxy to create an instance of it and pass it to our sut. That way you can still Verify against Delete in your testing code and maintain the connection between extension methods and instance methods they call in a single place - in the DispatchProxy's Invoke method.

Upvotes: 3

Michał Turczyn
Michał Turczyn

Reputation: 37500

To add to existing answers, especially Mark's answer, you should define interface around _backgroundJobClient's type.

Something like:

public interface IBackgroundJobClientWrapper
{
    void Delete(string key);
}

public class BackgroundJobClientWrapper : IBackgroundJobClientWrapper
{
    private readonly IBackgroundJobClient _backgroundJobClient;

    // Assuming that's how you get _backgroundJobClient
    public BackgroundJobClientWrapper(IBackgroundJobClient backgroundJobClient)
    {
        _backgroundJobClient = backgroundJobClient;
    }

    public void Delete(string key);
    {
        _backgroundJobClient.Delete(job.Key);
    }
}

Then you can easily mock IBackgroundJobClientWrapper interface.

Of course, you can adjust the interface definition to better suit your code.

Upvotes: 2

Mark Seemann
Mark Seemann

Reputation: 233447

As Ivan Petrov writes, you can test an extension method when you know how it's implemented. This enables you to verify against the underlying API that the extension method uses.

It does, however, lead to brittle tests. Your tests are now vulnerable to changes in a third-party library.

A better option is to define a polymorphic API (interface or base class) that's defined based on your client code's needs, rather than on implementation details. Here, we consider the entire Hangfire API an implementation detail.

This would more properly follow the Dependency Inversion Principle. You define the abstraction that you're programming against based on what your client code needs, rather than on what some third-party API affords.

Once you've defined the API that you need, you implement the interface or base class with the implementation detail: The Hangfire API.

Upvotes: 3

Related Questions