MichaCo / DnsClient.NET

DnsClient.NET is a simple yet very powerful and high performant open source library for the .NET Framework to do DNS lookups
https://dnsclient.michaco.net
Apache License 2.0
762 stars 136 forks source link

mock-friendly API for ResolveService(Async) #187

Closed bacongobbler closed 1 year ago

bacongobbler commented 1 year ago

I'm working on a service that will resolve Consul SRV records, and I want to mock out the ILookupClient's call to ResolveServiceAsync. Better yet would be providing my own NameServer so I could simulate a real-world SRV record lookup.

Do you have an example for mocking out ResolveServiceAsync? This is as close as I've gotten:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using DnsClient;
using DnsClient.Protocol;
using Moq;
using Xunit;

namespace IntegrationTests;

public class ServiceDiscoveryClient
{
    private readonly ILookupClient _lookupClient;

    public ServiceDiscoveryClient(ILookupClient lookupClient)
    {
        _lookupClient = lookupClient;
    }

    public async Task<string> GetServiceAddressAsync(string serviceName, string scheme = "http")
    {
        var entries = await _lookupClient.ResolveServiceAsync("service.consul", serviceName);
        if (!entries.Any())
        {
            throw new Exception($"Service {serviceName} not found");
        }

        var entry = entries.First();
        return $"{scheme}://{entry.HostName}:{entry.Port}";
    }
}

public class ServiceDiscoveryClientTests
{
    [Fact]
    public async Task GetServiceAddressAsync_WithValidServiceName_ReturnsServiceAddress()
    {
        var scheme = "http";
        var serviceName = "myservice";
        var baseName = "service.consul";
        var fullQuery = $"{serviceName}.{baseName}";
        ushort prio = 99;
        ushort weight = 69;
        ushort port = 88;
        var record = new SrvRecord(
                new ResourceRecordInfo(fullQuery, ResourceRecordType.SRV, QueryClass.IN, 1000, 0),
                prio,
                weight,
                port,
                DnsString.Parse(serviceName));

        var dnsResponseMock = new Mock<IDnsQueryResponse>();
        dnsResponseMock.Setup(x => x.Answers).Returns(new List<DnsResourceRecord> { record });

        var mockQuery = new Mock<ILookupClient>();

        mockQuery.Setup(p => p.QueryAsync(It.IsAny<string>(), It.IsAny<QueryType>(), It.IsAny<QueryClass>(), It.IsAny<CancellationToken>())).ReturnsAsync(dnsResponseMock.Object);

        // mockQuery.Setup(p => p.ResolveServiceAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>())).ReturnsAsync(DnsQueryExtensions.ResolveServiceProcessResult(dnsResponseMock.Object));

        // Arrange
        var serviceDiscoveryService = new ServiceDiscoveryClient(mockQuery.Object);

        // Act
        var serviceAddress = await serviceDiscoveryService.GetServiceAddressAsync(serviceName, scheme);

        // Assert
        Assert.Equal($"{scheme}://{fullQuery}:{port}", serviceAddress);
    }
}

But I get the following result:

IntegrationTests.ServiceDiscoveryClientTests.GetServiceAddressAsync_WithValidServiceName_ReturnsServiceAddress:
    Outcome: Failed
    Error Message:
    System.ArgumentNullException : Value cannot be null. (Parameter 'source')
    Stack Trace:
       at System.Linq.ThrowHelper.ThrowArgumentNullException(ExceptionArgument argument)
   at System.Linq.Enumerable.OfType[TResult](IEnumerable source)
   at DnsClient.DnsQueryExtensions.ResolveServiceProcessResult(IDnsQueryResponse result)
   at DnsClient.DnsQueryExtensions.ResolveServiceAsync(IDnsQuery query, String baseDomain, String serviceName, String tag)
   at IntegrationTests.ServiceDiscoveryClient.GetServiceAddressAsync(String serviceName, String scheme)
[...]

Total tests: 1. Passed: 0. Failed: 1. Skipped: 0

Uncommenting the ResolveServiceAsync setup code returns the following error:

IntegrationTests.ServiceDiscoveryClientTests.GetServiceAddressAsync_WithValidServiceName_ReturnsServiceAddress:
    Outcome: Failed
    Error Message:
    System.NotSupportedException : Unsupported expression: p => p.ResolveServiceAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>())
Extension methods (here: DnsQueryExtensions.ResolveServiceAsync) may not be used in setup / verification expressions.
    Stack Trace:
       at Moq.Guard.IsOverridable(MethodInfo method, Expression expression) in C:\projects\moq4\src\Moq\Guard.cs:line 87
   at Moq.MethodExpectation..ctor(LambdaExpression expression, MethodInfo method, IReadOnlyList`1 arguments, Boolean exactGenericTypeArguments, Boolean skipMatcherInitialization, Boolean allowNonOverridable) in C:\projects\moq4\src\Moq\MethodExpectation.cs:line 86
   at Moq.ExpressionExtensions.<Split>g__Split|5_0(Expression e, Expression& r, MethodExpectation& p, Boolean assignment, Boolean allowNonOverridableLastProperty) in C:\projects\moq4\src\Moq\ExpressionExtensions.cs:line 235
   at Moq.ExpressionExtensions.Split(LambdaExpression expression, Boolean allowNonOverridableLastProperty) in C:\projects\moq4\src\Moq\ExpressionExtensions.cs:line 149
   at Moq.Mock.SetupRecursive[TSetup](Mock mock, LambdaExpression expression, Func`4 setupLast, Boolean allowNonOverridableLastProperty) in C:\projects\moq4\src\Moq\Mock.cs:line 643
   at Moq.Mock.Setup(Mock mock, LambdaExpression expression, Condition condition) in C:\projects\moq4\src\Moq\Mock.cs:line 498
   at Moq.Mock`1.Setup[TResult](Expression`1 expression) in C:\projects\moq4\src\Moq\Mock`1.cs:line 452
   at IntegrationTests.ServiceDiscoveryClientTests.GetServiceAddressAsync_WithValidServiceName_ReturnsServiceAddress()
[...]

Total tests: 1. Passed: 0. Failed: 1. Skipped: 0
MichaCo commented 1 year ago

The code expects that all lists on the response are initialized, at least with an empty result, so if you add

            dnsResponseMock.Setup(x => x.Additionals).Returns(new List<DnsResourceRecord>());

That should do.

bacongobbler commented 1 year ago

Oh thank you! That did it.