0

I've created an application that uses the MS GitHttpClient class to read commits in an AzureDevOps project. I would like to make a unit test of the logic, so I need to mock the VssConnection and GitHttpClient. Neither of the two classes implements any interface.

I can mock the GitHttpClient and make it return commit refs when calling GitHttpClient.GetCommitsAsync(...) but when I try to mock VssConnection.GetClient<GitHttpClient>() I get the following exception

Test method mycli.Tests.Unit.Services.GitServiceTests.TestVssConnectionMock threw exception: 
System.NotSupportedException: Unsupported expression: conn => conn.GetClient<GitHttpClient>()
Non-overridable members (here: VssConnection.GetClient) may not be used in setup / verification expressions.

Here is my test class. The first test TestVssConnection fails with the above exception. The second test TestGitHttpClientMock passes.

    [TestClass]
    public class GitServiceTests
    {
        [TestMethod]
        public async Task TestVssConnectionMock()
        {
            var vssConnectionMock = new Mock<VssConnection>(new Uri("http://fake"), new VssCredentials());
            var gitHttpClientMock = new Mock<GitHttpClient>(new Uri("http://fake"), new VssCredentials());
            gitHttpClientMock.Setup(client => client.GetCommitsAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<GitQueryCommitsCriteria>(), null, null, null, It.IsAny<CancellationToken>())).Returns(Task.FromResult(new List<GitCommitRef> { new GitCommitRef { Comment = "abc" } }));
            vssConnectionMock.Setup(conn => conn.GetClient<GitHttpClient>()).Returns(gitHttpClientMock.Object);
            // EXCEPTION THROWN ABOVE ^

            var gitHttpClient = vssConnectionMock.Object.GetClient<GitHttpClient>();
            var commits = await gitHttpClient.GetCommitsAsync("", "", new GitQueryCommitsCriteria());

            Assert.IsTrue(commits.Count == 1);
        }

        [TestMethod]
        public async Task TestGitHttpClientMock()
        {
            var gitHttpClientMock = new Mock<GitHttpClient>(new Uri("http://fake"), new VssCredentials());
            gitHttpClientMock.Setup(client => client.GetCommitsAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<GitQueryCommitsCriteria>(), null, null, null, It.IsAny<CancellationToken>())).Returns(Task.FromResult(new List<GitCommitRef> { new GitCommitRef { Comment = "abc" } }));

            var commits = await gitHttpClientMock.Object.GetCommitsAsync("", "", new GitQueryCommitsCriteria());

            Assert.IsTrue(commits.Count == 1);
        }
    }

My question is, how do I mock VssConnection.GetClient<GitHttpClient>() so it returns my mock of GitHttpClient?

Is the workaround to make a wrapper of VssConnection? And if so, how is that best done?

I am using .NET 6, MsTest and MoQ.

Simon K
  • 215
  • 2
  • 11

2 Answers2

1

So far my own solution is to use the decorator pattern to wrap the VssConnection with an interface like this:

    //Using decorator pattern to wrap VssConnection so it can be mocked. VssConnection is not mock-able out of the box. 
    public interface IVssConnection : IDisposable
    {
        public T GetClient<T>() where T : VssHttpClientBase => this.GetClientAsync<T>().SyncResult<T>();
        public Task<T> GetClientAsync<T>(CancellationToken cancellationToken = default(CancellationToken)) where T : VssHttpClientBase;
    }

    public class VssConnectionWrapper : VssConnection, IVssConnection
    {
        public VssConnectionWrapper(Uri baseUrl, VssCredentials credentials) : base(baseUrl, credentials)
        {
        }
    }

This way I can test the VssConnection like this:

        [TestMethod]
        public async Task TestVssConnectionMock()
        {
            var vssConnectionMock = new Mock<IVssConnection>();
            var gitHttpClientMock = new Mock<GitHttpClient>(new Uri("http://fake"), new VssCredentials());
            gitHttpClientMock.Setup(client => client.GetCommitsAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<GitQueryCommitsCriteria>(), null, null, null, It.IsAny<CancellationToken>())).Returns(Task.FromResult(new List<GitCommitRef> { new GitCommitRef { Comment = "abc" } }));
            vssConnectionMock.Setup(conn => conn.GetClient<GitHttpClient>()).Returns(gitHttpClientMock.Object);

            var gitHttpClient = vssConnectionMock.Object.GetClient<GitHttpClient>();
            var commits = await gitHttpClient.GetCommitsAsync("", "", new GitQueryCommitsCriteria());

            Assert.IsTrue(commits.Count == 1);
        }

Only chenge is

var vssConnectionMock = new Mock<VssConnection>(new Uri("http://fake"), new VssCredentials());

// REPLACED WITH:

var vssConnectionMock = new Mock<IVssConnection>();

BUT if anyone have a solution where I can just use Moq without having to create a decorator then please let me know :-)

Simon K
  • 215
  • 2
  • 11
0

I feel your pain. The lack of an interface for either the VssConnection class or the GitHttpClient class makes unit testing difficult.

I offer you this possible alternative which may or may NOT be any better than your existing approach:

Use the C# 'dynamic' duck typing keyword to pretend that C# is Python. :)

I did it like this:

First in your client code that you want to unit test, define the externally connecting pieces like the VssConnection and the GitHttpClient as 'dynamic' types.

public class TFSCustomLib
{
    private dynamic gitClient;

    public TFSCustomLib(dynamic connection)
    {           
        gitClient = connection.GetClient<GitHttpClient>();
    }
    ...
}

Then, in the unit test class itself define an IMock interface along with a MoqTfsConnection property and a TestMethod that uses that property like this:

public class TFSCustomLibTests
{
    public interface IMock
    {
        public dynamic GetClient<GitHttpClient>();
        public dynamic GetRepositoriesAsync();
    }

    private static IMock MoqTfsConnection 
    {
        get
        {
            Mock<IMock> icc = new();
            icc.Setup(x => x.GetClient<GitHttpClient>()).Returns(icc.Object);
            icc.Setup(x => x.GetRepositoriesAsync()).Returns(
                Task<List<GitRepository>>.FromResult(
                    new List<GitRepository>() {
                    new GitRepository() })); //TODO: Add any number of GitRepository instances configured for unit testing.
            return icc.Object;
        }
    }

    [TestMethod()]
    public void TestTFSDependencyReporting()
    {        
        TFSDependencyReporter testReporter = new(MoqTfsConnection); 
        Assert.AreEqual(1, testReporter.Reports.Count);
    }
}

That should allow you to use Moq to mock out the VssConnection and GitHttpClient dependencies as needed.

Your milage may vary and your original approach may even be a better one. For me, having worked with Python for so long, I looked for a more pythonic approach. The 'dynamic' keyword fit the duck typed bill (pardon the pun.) The fact that C# eventually (after .Net 4.0 I think) provided the 'dynamic' keyword seems like maybe Python was/is ahead of the game in many ways.

Travis
  • 552
  • 1
  • 8
  • 14