最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

unit testing - Returning different values with multiple calls to a method in MOQ - Stack Overflow

programmeradmin1浏览0评论

I'm working on a unit test for a controller that calls a handler to retrieve a list of consumers multiple times.

The handler calls GetItemsAsync() and returns either a null value or a list of consumers.

I'm having an issue with the Setup() in my unit test. I'm not sure how to distinguish the two calls to the same handler returning different values. The only difference between the two calls are the values in the parameters provided to the specification for the select from the database.

I tried the following SetupSequence() statement, but then I found that the entire sequence is returned with each call. I need a solution where I can return one value per call.

_mockConsumerRepo.SetupSequence(m => m.GetItemsAsync(It.IsAny<ISpecification<Consumer>>(), It.IsAny<string>(), It.IsAny<int?>()))
    .ReturnsAsync((GetItemsResult<IEnumerable<Consumer>>)null)
    .ReturnsAsync(new GetItemsResult<IEnumerable<Consumer>>
    {
        Results = consumers
    });

I realize that if I define 2 Setups (below) for the same method, it will take the latest, so that is not an option.

_mockConsumerRepo.Setup(m => m.GetItemsAsync(It.IsAny<ISpecification<Consumer>>(), It.IsAny<string>(), It.IsAny<int?>()))
  .ReturnsAsync((GetItemsResult<IEnumerable<Consumer>>)null);

_mockConsumerRepo.Setup(m => m.GetItemsAsync(It.IsAny<ISpecification<Consumer>>(), It.IsAny<string>(), It.IsAny<int?>()))
    .ReturnsAsync(new GetItemsResult<IEnumerable<Consumer>>
    {
        Results = consumers
    });

This is the call to the handler in the controller that is being executed, once with email and brand/region, and again later in the code with phone and brand/region.

var query = new GetConsumerByParametersQuery
{
    EmailAddress = dto.EmailAddress,
    EnrollmentBrandRegion = dto.EnrollmentBrandRegion
};

var data = await _consumerParametersQueryHandler.HandleAsync(query);

This is the code for the handler that retrieves the consumer records:

namespace cscentconsumerapi.Common.RequestHandlers
{
    public class GetConsumerByParametersQuery
    {
        private static readonly int DEFAULT_LIMIT = 20;
        private static readonly int MAX_LIMIT = 40;
        private int _limit;

        public string Brand { get; set; }
        public string EnrollmentBrandRegion { get; set; }
        public string EmailAddress { get; set; }
        public string PhoneNumber { get; set; }


        /// <summary>
        /// The token when continuating a query
        /// </summary>
        public string NextLink { get; set; }

        /// <summary>
        /// The number of purchase orders to return
        /// Default value is 20, max value is 40
        /// </summary>
        public int Limit
        {
            get { return (_limit > 0 && _limit < MAX_LIMIT) ? _limit : DEFAULT_LIMIT; }
            set { _limit = (int)value; }
        }
    }

    public class GetConsumerByParametersHandler : IRequestHandler<GetConsumerByParametersQuery, (IEnumerable<ConsumerDto> consumers, string token)>
    {
        private readonly IModelMasterRepository<Consumer> _masterRepository;
        private readonly IModelMasterRepository<SmsConsentDocument> _smsConsentRepository;
        private readonly IMapper _mapper;
        private readonly IDistributedCacheWrapper _cacheWrapper;

        public GetConsumerByParametersHandler(
            IModelMasterRepository<Consumer> masterRepository,
            IModelMasterRepository<SmsConsentDocument> smsConsentRepository,
            IMapper mapper, 
            IDistributedCacheWrapper distributedCacheWrapper)
        {
            _masterRepository = masterRepository;
            _smsConsentRepository = smsConsentRepository;
            _mapper = mapper;
            _cacheWrapper = distributedCacheWrapper;
        }

        public async Task<(IEnumerable<ConsumerDto> consumers, string token)> HandleAsync(GetConsumerByParametersQuery request)
        {
            var normalizedPhone = ConsumerProfile.ParsePhoneNumber(request.EnrollmentBrandRegion, request.PhoneNumber);
            var specification = new GetConsumerByFilterSpecification(request.Brand, request.EnrollmentBrandRegion, request.EmailAddress, normalizedPhone);

            // Get token from memory cache
            string? token;

            if (!string.IsNullOrEmpty(request.NextLink))
                token = await _cacheWrapper.GetStringAsync(request.NextLink);
            else
                token = null;

            IEnumerable<ConsumerDto> consumerDtos = Enumerable.Empty<ConsumerDto>();
            var response = await _masterRepository.GetItemsAsync(specification, token, request.Limit);

            if (response == null)
                return (consumerDtos, token);

            consumerDtos = response.Results.Select(r => _mapper.Map<Consumer, ConsumerDto>(r));

            consumerDtos = await RequestHandlerHelper.HydrateSmsConsentsAsync(consumerDtos.ToList(), response.Results, _smsConsentRepository, _mapper);

            // Insert the new token
            string newContinuationToken;
            if (!string.IsNullOrEmpty(response.ContinuationToken))
                newContinuationToken = await _cacheWrapper.SetStringAsync(response.ContinuationToken);
            else
                newContinuationToken = null;

            return (consumerDtos, newContinuationToken);
        }
    }
}

This is the specification that is being called within the handler to select consumers.

using Ardalis.Specification;
using cscentconsumer.domain.Models.Consumer;

namespace CscEntConsumer.Infrastructure.Specifications
{
    public class GetConsumerByFilterSpecification : Specification<Consumer>
    {
        public enum OrderBy
        {
            ASC, 
            DESC
        }

        public GetConsumerByFilterSpecification(
            string brand = null,
            string siteId = null,
            string email = null,
            string phoneNumber = null,
            OrderBy? order = null
            )
        {
            if (brand != null)
                Query.Where(x => x.Brand == brand);
            if (siteId != null)
                Query.Where(x => x.EnrollmentBrandRegion == siteId);
            if (email != null)
                Query.Where(x => x.EmailAddress.ToLower() == email.ToLower());
            if (phoneNumber != null)
            {
                Query.Where(x =>  x.PhoneNumberSearch == phoneNumber);
            }
                    


            if (order != null) 
            {
                switch (order)
                {
                    case OrderBy.DESC:
                        Query.OrderByDescending(x => x.UpdateDateTime);
                        break;
                    case OrderBy.ASC:
                        Query.OrderBy(x => x.UpdateDateTime);
                        break;
                }
            }
            else
            {
                Query.OrderByDescending(x => x.UpdateDateTime);
            }
        }
    }
}

Is there a way I can distinguish the two calls when they call the exact same handler with the same parameter (query, just different values)? Any suggestions would be greatly appreciated.

I'm working on a unit test for a controller that calls a handler to retrieve a list of consumers multiple times.

The handler calls GetItemsAsync() and returns either a null value or a list of consumers.

I'm having an issue with the Setup() in my unit test. I'm not sure how to distinguish the two calls to the same handler returning different values. The only difference between the two calls are the values in the parameters provided to the specification for the select from the database.

I tried the following SetupSequence() statement, but then I found that the entire sequence is returned with each call. I need a solution where I can return one value per call.

_mockConsumerRepo.SetupSequence(m => m.GetItemsAsync(It.IsAny<ISpecification<Consumer>>(), It.IsAny<string>(), It.IsAny<int?>()))
    .ReturnsAsync((GetItemsResult<IEnumerable<Consumer>>)null)
    .ReturnsAsync(new GetItemsResult<IEnumerable<Consumer>>
    {
        Results = consumers
    });

I realize that if I define 2 Setups (below) for the same method, it will take the latest, so that is not an option.

_mockConsumerRepo.Setup(m => m.GetItemsAsync(It.IsAny<ISpecification<Consumer>>(), It.IsAny<string>(), It.IsAny<int?>()))
  .ReturnsAsync((GetItemsResult<IEnumerable<Consumer>>)null);

_mockConsumerRepo.Setup(m => m.GetItemsAsync(It.IsAny<ISpecification<Consumer>>(), It.IsAny<string>(), It.IsAny<int?>()))
    .ReturnsAsync(new GetItemsResult<IEnumerable<Consumer>>
    {
        Results = consumers
    });

This is the call to the handler in the controller that is being executed, once with email and brand/region, and again later in the code with phone and brand/region.

var query = new GetConsumerByParametersQuery
{
    EmailAddress = dto.EmailAddress,
    EnrollmentBrandRegion = dto.EnrollmentBrandRegion
};

var data = await _consumerParametersQueryHandler.HandleAsync(query);

This is the code for the handler that retrieves the consumer records:

namespace cscentconsumerapi.Common.RequestHandlers
{
    public class GetConsumerByParametersQuery
    {
        private static readonly int DEFAULT_LIMIT = 20;
        private static readonly int MAX_LIMIT = 40;
        private int _limit;

        public string Brand { get; set; }
        public string EnrollmentBrandRegion { get; set; }
        public string EmailAddress { get; set; }
        public string PhoneNumber { get; set; }


        /// <summary>
        /// The token when continuating a query
        /// </summary>
        public string NextLink { get; set; }

        /// <summary>
        /// The number of purchase orders to return
        /// Default value is 20, max value is 40
        /// </summary>
        public int Limit
        {
            get { return (_limit > 0 && _limit < MAX_LIMIT) ? _limit : DEFAULT_LIMIT; }
            set { _limit = (int)value; }
        }
    }

    public class GetConsumerByParametersHandler : IRequestHandler<GetConsumerByParametersQuery, (IEnumerable<ConsumerDto> consumers, string token)>
    {
        private readonly IModelMasterRepository<Consumer> _masterRepository;
        private readonly IModelMasterRepository<SmsConsentDocument> _smsConsentRepository;
        private readonly IMapper _mapper;
        private readonly IDistributedCacheWrapper _cacheWrapper;

        public GetConsumerByParametersHandler(
            IModelMasterRepository<Consumer> masterRepository,
            IModelMasterRepository<SmsConsentDocument> smsConsentRepository,
            IMapper mapper, 
            IDistributedCacheWrapper distributedCacheWrapper)
        {
            _masterRepository = masterRepository;
            _smsConsentRepository = smsConsentRepository;
            _mapper = mapper;
            _cacheWrapper = distributedCacheWrapper;
        }

        public async Task<(IEnumerable<ConsumerDto> consumers, string token)> HandleAsync(GetConsumerByParametersQuery request)
        {
            var normalizedPhone = ConsumerProfile.ParsePhoneNumber(request.EnrollmentBrandRegion, request.PhoneNumber);
            var specification = new GetConsumerByFilterSpecification(request.Brand, request.EnrollmentBrandRegion, request.EmailAddress, normalizedPhone);

            // Get token from memory cache
            string? token;

            if (!string.IsNullOrEmpty(request.NextLink))
                token = await _cacheWrapper.GetStringAsync(request.NextLink);
            else
                token = null;

            IEnumerable<ConsumerDto> consumerDtos = Enumerable.Empty<ConsumerDto>();
            var response = await _masterRepository.GetItemsAsync(specification, token, request.Limit);

            if (response == null)
                return (consumerDtos, token);

            consumerDtos = response.Results.Select(r => _mapper.Map<Consumer, ConsumerDto>(r));

            consumerDtos = await RequestHandlerHelper.HydrateSmsConsentsAsync(consumerDtos.ToList(), response.Results, _smsConsentRepository, _mapper);

            // Insert the new token
            string newContinuationToken;
            if (!string.IsNullOrEmpty(response.ContinuationToken))
                newContinuationToken = await _cacheWrapper.SetStringAsync(response.ContinuationToken);
            else
                newContinuationToken = null;

            return (consumerDtos, newContinuationToken);
        }
    }
}

This is the specification that is being called within the handler to select consumers.

using Ardalis.Specification;
using cscentconsumer.domain.Models.Consumer;

namespace CscEntConsumer.Infrastructure.Specifications
{
    public class GetConsumerByFilterSpecification : Specification<Consumer>
    {
        public enum OrderBy
        {
            ASC, 
            DESC
        }

        public GetConsumerByFilterSpecification(
            string brand = null,
            string siteId = null,
            string email = null,
            string phoneNumber = null,
            OrderBy? order = null
            )
        {
            if (brand != null)
                Query.Where(x => x.Brand == brand);
            if (siteId != null)
                Query.Where(x => x.EnrollmentBrandRegion == siteId);
            if (email != null)
                Query.Where(x => x.EmailAddress.ToLower() == email.ToLower());
            if (phoneNumber != null)
            {
                Query.Where(x =>  x.PhoneNumberSearch == phoneNumber);
            }
                    


            if (order != null) 
            {
                switch (order)
                {
                    case OrderBy.DESC:
                        Query.OrderByDescending(x => x.UpdateDateTime);
                        break;
                    case OrderBy.ASC:
                        Query.OrderBy(x => x.UpdateDateTime);
                        break;
                }
            }
            else
            {
                Query.OrderByDescending(x => x.UpdateDateTime);
            }
        }
    }
}

Is there a way I can distinguish the two calls when they call the exact same handler with the same parameter (query, just different values)? Any suggestions would be greatly appreciated.

Share Improve this question edited Mar 14 at 21:35 ChrisP asked Mar 13 at 23:02 ChrisPChrisP 479 bronze badges
Add a comment  | 

1 Answer 1

Reset to default 0

All of the attempts described in the OP use It.IsAny<T>, so of course Moq can't distinguish one method invocation from another. Using It.IsAny<T> is like defining a predicate that always returns true.

Try doing separate Setups that Moq can distinguish:

_mockConsumerRepo.Setup(m => m.GetItemsAsync(
    new GetConsumerByParametersQuery
    {
        EmailAddress = "foo",
        EnrollmentBrandRegion = "bar"
    }, 
    It.IsAny<string>(),
    It.IsAny<int?>()))
   .ReturnsAsync((GetItemsResult<IEnumerable<Consumer>>)null);

_mockConsumerRepo.Setup(m => m.GetItemsAsync(
    new GetConsumerByParametersQuery
    {
        Phone = "1234",
        EnrollmentBrandRegion = "baz"
    },
    It.IsAny<string>(), 
    It.IsAny<int?>()))
    .ReturnsAsync(new GetItemsResult<IEnumerable<Consumer>>
    {
         Results = consumers
    });

You may also want to replace the other It.IsAny<string>() and It.IsAny<int?>() with more specific values.

If, however, GetConsumerByParametersQuery has reference equality (which objects in C# do by default), this isn't going to work, because the GetConsumerByParametersQuery objects created in the test aren't the same objects used by the System Under Test.

If so, consider making GetConsumerByParametersQuery immutable so that you can give it structural equality. The easiest way to do this in C# is to make it a record.

If this is not possible, you can use It.Is<T> to define custom predicates. You may also consider the Resemblance idiom, but only use this if all else fails.

发布评论

评论列表(0)

  1. 暂无评论