riezebosch/Unmockable

New User struggling to understand usage from documentation

Closed this issue ยท 2 comments

I am trying to use Unmockable to intercept the call to MinioClient.PutObject within the UploadAsync of my S3Service class listed below.

S3Service Class

    public class S3Service : IS3Service, IDisposable
    {
        const string JpegContentType = "image/jpeg";

        private bool _Disposed { get; set; }
        private ILogger<S3Service> _Logger { get; set; }
        private MinioClient _MinioClient { get; set; }
        private S3Config _Config { get; set; }


        /// <summary>
        /// <param name="config">S3 config from appsettings</param>
        /// <param name="minioFactory">
        /// Function to create a <see cref="MinioClient"/>. Accepts the following parameters:
        /// 1. endpoint: string
        /// 2. username: string
        /// 3. password: string
        /// </param>
        /// <param name="logger">Logger instance</param>
        /// </summary>
        public S3Service(
            IOptions<S3Config> config,
            Func<string, string, string, MinioClient> minioFactory,
            ILogger<S3Service> logger
        )
        {
            _Config = config.Value;
            _Disposed = false;
            _Logger = logger;

            _MinioClient = minioFactory(_Config.Endpoint, _Config.AccessKey, _Config.SecretKey);

            _Logger.LogInformation($"Minio client created for endpoint {_Config.Endpoint}");
        }

        public async Task UploadAsync(BufferedStream byteStream, string objectName)
        {
            // validate args are not null
            if (byteStream == null)
            {
                throw new ArgumentNullException(nameof(byteStream));
            }

            if (objectName == null)
            {
                throw new ArgumentNullException(nameof(objectName));
            }

            // Try and upload, let minio handle the validation
            try
            {
                await _MinioClient.PutObjectAsync(_Config.Bucket, objectName, byteStream, byteStream.Length, JpegContentType);
            }
            catch (MinioException e)
            {
                _Logger.LogError(e, "Error occurred while uploading to S3 storage");
            }
        }
}

Test so far - Does not compile

        [Fact]
        public async Task S3Service_UploadAsync_Calls_PutObjectAsync_With_Expected_Args()
        {
            const string objName = "objName";
            const string testData = "testData";

            var config = S3TestUtils.CreateDefaultS3Config();
            var options = Options.Create<S3Config>(config);

            using (var buffer = new BufferedStream(new MemoryStream(Encoding.UTF8.GetBytes(testData))))
            {
                var mockClient = Interceptor.For<MinioClient>()
                    .Setup(m =>
                        m.PutObjectAsync(
                            config.Bucket,
                            objName,
                            buffer,
                            buffer.Length,
                            JpegContentType,
                            null, null, default(CancellationToken)
                        )
                    );

               /**
                       Compile Error: A lambda expression with a statement body cannot be converted to an expression tree

                      I need to inject client mock into S3Service via factory method
                */
                var sut = mockClient.As<IUnmockable<MinioClient>>().Execute<Task>(client =>
               {
                   using (S3Service svc = new S3Service(
                       options, (endpoint, login, passwd) => client, new NullLogger<S3Service>()))
                   {

                       return svc.UploadAsync(buffer, objName);
                   }
               });

                // test that the method call was intercepted
                mockClient.Verify();
            }
        }

Is this possible with Unmockable library as opposed to manually writing my own interface to wrap the unmockable MinioClient?

You can when you structure your code slightly differently. Pull out the configuration from this class en provide a configured instance of the client as constructor parameter. That instance can than be wrapped with the IUnmockable<MinioClient> interface.

In your test you then create an Interceptor.For<MinioClient>(), do some Setup's en Verify afterwards.

For your production code you inject a wrapped configured MinioClient by either wrapping the object yourself or add wrapped objects to the ServiceCollection when you use that for dependency resolving.

Something along the lines of: https://dotnetfiddle.net/vhnZv5

Many thanks @riezebosch , ok understand now. Many thanks for your help, much appreciated :)